Merge branch 'stable-3.0' into stable-3.1

* stable-3.0:
  Set version to 3.0.6-SNAPSHOT
  Set version to 3.0.5
  Set version to 2.16.15-SNAPSHOT
  Set version to 2.16.14
  Add event interface to Gerrit

Change-Id: Iad934b1abe45ddea91f89c5735eaeef4fba22b6e
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js
new file mode 100644
index 0000000..d024bb2
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface.js
@@ -0,0 +1,142 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function(window) {
+  'use strict';
+
+  // Avoid duplicate registeration
+  if (window.EventEmitter) return;
+
+  /**
+   * An lite implementation of
+   * https://nodejs.org/api/events.html#events_class_eventemitter.
+   *
+   * This is unrelated to the native DOM events, you should use it when you want
+   * to enable EventEmitter interface on any class.
+   *
+   * @example
+   *
+   * class YourClass extends EventEmitter {
+   *   // now all instance of YourClass will have this EventEmitter interface
+   * }
+   *
+   */
+  class EventEmitter {
+    constructor() {
+      /**
+       * Shared events map from name to the listeners.
+       * @type {!Object<string, Array<eventCallback>>}
+       */
+      this._listenersMap = new Map();
+    }
+
+    /**
+     * Register an event listener to an event.
+     *
+     * @param {string} eventName
+     * @param {eventCallback} cb
+     * @returns {Function} Unsubscribe method
+     */
+    addListener(eventName, cb) {
+      if (!eventName || !cb) {
+        console.warn('A valid eventname and callback is required!');
+        return;
+      }
+
+      const listeners = this._listenersMap.get(eventName) || [];
+      listeners.push(cb);
+      this._listenersMap.set(eventName, listeners);
+
+      return () => {
+        this.off(eventName, cb);
+      };
+    }
+
+    // Alias for addListener.
+    on(eventName, cb) {
+      return this.addListener(eventName, cb);
+    }
+
+    // Attach event handler only once. Automatically removed.
+    once(eventName, cb) {
+      const onceWrapper = (...args) => {
+        cb(...args);
+        this.off(eventName, onceWrapper);
+      };
+      return this.on(eventName, onceWrapper);
+    }
+
+    /**
+     * De-register an event listener to an event.
+     *
+     * @param {string} eventName
+     * @param {eventCallback} cb
+     */
+    removeListener(eventName, cb) {
+      let listeners = this._listenersMap.get(eventName) || [];
+      listeners = listeners.filter(listener => listener !== cb);
+      this._listenersMap.set(eventName, listeners);
+    }
+
+    // Alias to removeListener
+    off(eventName, cb) {
+      this.removeListener(eventName, cb);
+    }
+
+    /**
+     * Synchronously calls each of the listeners registered for
+     * the event named eventName, in the order they were registered,
+     * passing the supplied detail to each.
+     *
+     * Returns true if the event had listeners, false otherwise.
+     *
+     * @param {string} eventName
+     * @param {*} detail
+     */
+    emit(eventName, detail) {
+      const listeners = this._listenersMap.get(eventName) || [];
+      for (const listener of listeners) {
+        try {
+          listener(detail);
+        } catch (e) {
+          console.error(e);
+        }
+      }
+      return listeners.length !== 0;
+    }
+
+    // Alias to emit.
+    dispatch(eventName, detail) {
+      return this.emit(eventName, detail);
+    }
+
+    /**
+     * Remove listeners for a specific event or all.
+     *
+     * @param {string} eventName if not provided, will remove all
+     */
+    removeAllListeners(eventName) {
+      if (eventName) {
+        this._listenersMap.set(eventName, []);
+      } else {
+        this._listenersMap = new Map();
+      }
+    }
+  }
+
+  window.EventEmitter = EventEmitter;
+})(window);
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
new file mode 100644
index 0000000..137ed25
--- /dev/null
+++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
@@ -0,0 +1,146 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2019 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-api-interface</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="../gr-js-api-interface/gr-js-api-interface.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-js-api-interface></gr-js-api-interface>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-event-interface tests', () => {
+    let sandbox;
+
+    setup(() => {
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    suite('test on Gerrit', () => {
+      setup(() => {
+        fixture('basic');
+        Gerrit.removeAllListeners();
+      });
+
+      test('communicate between plugin and Gerrit', done => {
+        const eventName = 'test-plugin-event';
+        let p;
+        Gerrit.on(eventName, e => {
+          assert.equal(e.value, 'test');
+          assert.equal(e.plugin, p);
+          done();
+        });
+        Gerrit.install(plugin => {
+          p = plugin;
+          Gerrit.emit(eventName, {value: 'test', plugin});
+        }, '0.1',
+            'http://test.com/plugins/testplugin/static/test.js');
+      });
+
+      test('listen on events from core', done => {
+        const eventName = 'test-plugin-event';
+        Gerrit.on(eventName, e => {
+          assert.equal(e.value, 'test');
+          done();
+        });
+
+        Gerrit.emit(eventName, {value: 'test'});
+      });
+
+      test('communicate across plugins', done => {
+        const eventName = 'test-plugin-event';
+        Gerrit.install(plugin => {
+          Gerrit.on(eventName, e => {
+            assert.equal(e.plugin.getPluginName(), 'testB');
+            done();
+          });
+        }, '0.1',
+            'http://test.com/plugins/testA/static/testA.js');
+
+        Gerrit.install(plugin => {
+          Gerrit.emit(eventName, {plugin});
+        }, '0.1',
+            'http://test.com/plugins/testB/static/testB.js');
+      });
+    });
+
+    suite('test on interfaces', () => {
+      let testObj;
+      class TestClass extends EventEmitter {
+      }
+      setup(() => {
+        testObj = new TestClass();
+      });
+
+      test('on', () => {
+        const cbStub = sinon.stub();
+        testObj.on('test', cbStub);
+        testObj.emit('test');
+        testObj.emit('test');
+        assert.isTrue(cbStub.calledTwice);
+      });
+
+      test('once', () => {
+        const cbStub = sinon.stub();
+        testObj.once('test', cbStub);
+        testObj.emit('test');
+        testObj.emit('test');
+        assert.isTrue(cbStub.calledOnce);
+      });
+
+      test('unsubscribe', () => {
+        const cbStub = sinon.stub();
+        const unsubscribe = testObj.on('test', cbStub);
+        testObj.emit('test');
+        unsubscribe();
+        testObj.emit('test');
+        assert.isTrue(cbStub.calledOnce);
+      });
+
+      test('off', () => {
+        const cbStub = sinon.stub();
+        testObj.on('test', cbStub);
+        testObj.emit('test');
+        testObj.off('test', cbStub);
+        testObj.emit('test');
+        assert.isTrue(cbStub.calledOnce);
+      });
+
+      test('removeAllListeners', () => {
+        const cbStub = sinon.stub();
+        testObj.on('test', cbStub);
+        testObj.removeAllListeners('test');
+        testObj.emit('test');
+        assert.isTrue(cbStub.notCalled);
+      });
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
index dc81545..72fc3d0 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html
@@ -39,6 +39,7 @@
     3. gr-public-js-api depends on gr-plugin-rest-api
   -->
   <script src="gr-api-utils.js"></script>
+  <script src="../gr-event-interface/gr-event-interface.js"></script>
   <script src="gr-annotation-actions-context.js"></script>
   <script src="gr-annotation-actions-js-api.js"></script>
   <script src="gr-change-actions-js-api.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
index d76c983..e451acf 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js
@@ -383,4 +383,40 @@
   };
 
   window.Plugin = Plugin;
+  // TODO(taoalpha): List all internal supported event names.
+  // Also convert this to inherited class once we move Gerrit to class.
+  Gerrit._eventEmitter = new EventEmitter();
+  ['addListener',
+    'dispatch',
+    'emit',
+    'off',
+    'on',
+    'once',
+    'removeAllListeners',
+    'removeListener',
+  ].forEach(method => {
+    /**
+     * Enabling EventEmitter interface on Gerrit.
+     *
+     * This will enable to signal across different parts of js code without relying on DOM,
+     * including core to core, plugin to plugin and also core to plugin.
+     *
+     * @example
+     *
+     * // Emit this event from pluginA
+     * Gerrit.install(pluginA => {
+     *   fetch("some-api").then(() => {
+     *     Gerrit.on("your-special-event", {plugin: pluginA});
+     *   });
+     * });
+     *
+     * // Listen on your-special-event from pluignB
+     * Gerrit.install(pluginB => {
+     *   Gerrit.on("your-special-event", ({plugin}) => {
+     *     // do something, plugin is pluginA
+     *   });
+     * });
+     */
+    Gerrit[method] = Gerrit._eventEmitter[method].bind(Gerrit._eventEmitter);
+  });
 })(window);
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index cb1fcef..cca7d04 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -154,6 +154,7 @@
     'settings/gr-settings-view/gr-settings-view_test.html',
     'settings/gr-ssh-editor/gr-ssh-editor_test.html',
     'settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html',
+    'shared/gr-event-interface/gr-event-interface_test.html',
     'shared/gr-account-entry/gr-account-entry_test.html',
     'shared/gr-account-label/gr-account-label_test.html',
     'shared/gr-account-list/gr-account-list_test.html',