Bug 1268077: expose AddonListener through mozAddonManager r=rhelmer,smaug
authorAndrew Swan <aswan@mozilla.com>
Fri, 27 May 2016 15:43:05 -0700
changeset 300291 291d7bedba4f501b2cf3880b7e43b9a9ec0d9f1f
parent 300290 ad4fb284d4a7b61ef7846b737f48f304304623a0
child 300292 1e7b3807891800367bc2f624197f34f2c6dcb2e0
push id30309
push usercbook@mozilla.com
push dateFri, 03 Jun 2016 10:00:40 +0000
treeherdermozilla-central@e27fe24a746f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrhelmer, smaug
bugs1268077
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1268077: expose AddonListener through mozAddonManager r=rhelmer,smaug MozReview-Commit-ID: Klw4o0qIvCE
dom/webidl/AddonEvent.webidl
dom/webidl/AddonManager.webidl
dom/webidl/moz.build
toolkit/mozapps/extensions/AddonManagerWebAPI.h
toolkit/mozapps/extensions/addonManager.js
toolkit/mozapps/extensions/amWebAPI.js
toolkit/mozapps/extensions/test/browser/browser.ini
toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
toolkit/mozapps/extensions/test/browser/head.js
toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
new file mode 100644
--- /dev/null
+++ b/dom/webidl/AddonEvent.webidl
@@ -0,0 +1,12 @@
+[ Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
+  Constructor(DOMString type, AddonEventInit eventInitDict)]
+interface AddonEvent : Event {
+  readonly attribute DOMString id;
+  readonly attribute boolean needsRestart;
+};
+
+dictionary AddonEventInit : EventInit {
+  required DOMString id;
+  required boolean needsRestart;
+};
+
--- a/dom/webidl/AddonManager.webidl
+++ b/dom/webidl/AddonManager.webidl
@@ -46,17 +46,17 @@ interface AddonInstall : EventTarget {
 dictionary addonInstallOptions {
   required DOMString url;
 };
 
 [HeaderFile="mozilla/AddonManagerWebAPI.h",
  Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
  NavigatorProperty="mozAddonManager",
  JSImplementation="@mozilla.org/addon-web-api/manager;1"]
-interface AddonManager {
+interface AddonManager : EventTarget {
   /**
    * Gets information about an add-on
    *
    * @param  id
    *         The ID of the add-on to test for.
    * @return A promise. It will resolve to an Addon if the add-on is installed.
    */
   Promise<Addon> getAddonByID(DOMString id);
@@ -64,9 +64,15 @@ interface AddonManager {
   /**
    * Creates an AddonInstall object for a given URL.
    *
    * @param options
    *        Only one supported option: 'url', the URL of the addon to install.
    * @return A promise that resolves to an instance of AddonInstall.
    */
   Promise<AddonInstall> createInstall(optional addonInstallOptions options);
+
+  /* Hooks for managing event listeners */
+  [ChromeOnly]
+  void eventListenerWasAdded(DOMString type);
+  [ChromeOnly]
+  void eventListenerWasRemoved(DOMString type);
 };
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -758,16 +758,17 @@ else:
     ]
 
 if CONFIG['MOZ_B2G_FM']:
     WEBIDL_FILES += [
         'FMRadio.webidl',
     ]
 
 GENERATED_EVENTS_WEBIDL_FILES = [
+    'AddonEvent.webidl',
     'AnimationPlaybackEvent.webidl',
     'AutocompleteErrorEvent.webidl',
     'BlobEvent.webidl',
     'CallEvent.webidl',
     'CallGroupErrorEvent.webidl',
     'CameraClosedEvent.webidl',
     'CameraConfigurationEvent.webidl',
     'CameraFacesDetectedEvent.webidl',
--- a/toolkit/mozapps/extensions/AddonManagerWebAPI.h
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -1,19 +1,24 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+#ifndef addonmanagerwebapi_h_
+#define addonmanagerwebapi_h_
+
 #include "nsPIDOMWindow.h"
 
 namespace mozilla {
 
 class AddonManagerWebAPI {
 public:
   static bool IsAPIEnabled(JSContext* cx, JSObject* obj);
 
 private:
   static bool IsValidSite(nsIURI* uri);
 };
 
 } // namespace mozilla
+
+#endif // addonmanagerwebapi_h_
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -23,16 +23,18 @@ const SUCCESS           = 0;
 const MSG_INSTALL_ENABLED  = "WebInstallerIsInstallEnabled";
 const MSG_INSTALL_ADDONS   = "WebInstallerInstallAddonsFromWebpage";
 const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
 
 const MSG_PROMISE_REQUEST  = "WebAPIPromiseRequest";
 const MSG_PROMISE_RESULT   = "WebAPIPromiseResult";
 const MSG_INSTALL_EVENT    = "WebAPIInstallEvent";
 const MSG_INSTALL_CLEANUP  = "WebAPICleanup";
+const MSG_ADDON_EVENT_REQ  = "WebAPIAddonEventRequest";
+const MSG_ADDON_EVENT      = "WebAPIAddonEvent";
 
 const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 var gSingleton = null;
 
@@ -46,16 +48,17 @@ function amManager() {
   let globalMM = Services.mm;
   globalMM.loadFrameScript(CHILD_SCRIPT, true);
   globalMM.addMessageListener(MSG_INSTALL_ADDONS, this);
 
   gParentMM = Services.ppmm;
   gParentMM.addMessageListener(MSG_INSTALL_ENABLED, this);
   gParentMM.addMessageListener(MSG_PROMISE_REQUEST, this);
   gParentMM.addMessageListener(MSG_INSTALL_CLEANUP, this);
+  gParentMM.addMessageListener(MSG_ADDON_EVENT_REQ, this);
 
   Services.obs.addObserver(this, "message-manager-close", false);
   Services.obs.addObserver(this, "message-manager-disconnect", false);
 
   AddonManager.webAPI.setEventHandler(this.sendEvent);
 
   // Needed so receiveMessage can be called directly by JS callers
   this.wrappedJSObject = this;
@@ -156,16 +159,18 @@ amManager.prototype = {
 
     return retval;
   },
 
   notify: function(aTimer) {
     AddonManagerPrivate.backgroundUpdateTimerHandler();
   },
 
+  addonListener: null,
+
   /**
    * messageManager callback function.
    *
    * Listens to requests from child processes for InstallTrigger
    * activity, and sends back callbacks.
    */
   receiveMessage: function(aMessage) {
     let payload = aMessage.data;
@@ -216,16 +221,40 @@ amManager.prototype = {
         }
         break;
       }
 
       case MSG_INSTALL_CLEANUP: {
         AddonManager.webAPI.clearInstalls(payload.ids);
         break;
       }
+
+      case MSG_ADDON_EVENT_REQ: {
+        if (payload.enabled) {
+          if (!this.addonListener) {
+            let target = aMessage.target;
+            let handler = (event, id, needsRestart) => {
+              target.sendAsyncMessage(MSG_ADDON_EVENT, {event, id, needsRestart});
+            };
+            this.addonListener = {
+              onEnabling: (addon, needsRestart) => handler("onEnabling", addon.id, needsRestart),
+              onEnabled: (addon) => handler("onEnabled", addon.id, false),
+              onDisabling: (addon, needsRestart) => handler("onDisabling", addon.id, needsRestart),
+              onDisabled: (addon) => handler("onDisabled", addon.id, false),
+              onInstalling: (addon, needsRestart) => handler("onInstalling", addon.id, needsRestart),
+              onInstalled: (addon) => handler("onInstalled", addon.id, false),
+              onUninstalling: (addon, needsRestart) => handler("onUninstalling", addon.id, needsRestart),
+              onUninstalled: (addon) => handler("onUninstalled", addon.id, false),
+            };
+          }
+          AddonManager.addAddonListener(this.addonListener);
+        } else {
+          AddonManager.removeAddonListener(this.addonListener);
+        }
+      }
     }
     return undefined;
   },
 
   sendEvent(target, data) {
     target.sendAsyncMessage(MSG_INSTALL_EVENT, data);
   },
 
--- a/toolkit/mozapps/extensions/amWebAPI.js
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -9,28 +9,32 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
 const MSG_PROMISE_REQUEST  = "WebAPIPromiseRequest";
 const MSG_PROMISE_RESULT   = "WebAPIPromiseResult";
 const MSG_INSTALL_EVENT    = "WebAPIInstallEvent";
 const MSG_INSTALL_CLEANUP  = "WebAPICleanup";
+const MSG_ADDON_EVENT_REQ  = "WebAPIAddonEventRequest";
+const MSG_ADDON_EVENT      = "WebAPIAddonEvent";
 
 const APIBroker = {
   _nextID: 0,
 
   init() {
     this._promises = new Map();
 
     // _installMap maps integer ids to DOM AddonInstall instances
     this._installMap = new Map();
 
     Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this);
     Services.cpmm.addMessageListener(MSG_INSTALL_EVENT, this);
+
+    this._eventListener = null;
   },
 
   receiveMessage(message) {
     let payload = message.data;
 
     switch (message.name) {
       case MSG_PROMISE_RESULT: {
         if (!this._promises.has(payload.callbackID)) {
@@ -52,29 +56,47 @@ const APIBroker = {
         if (!install) {
           let err = new Error(`Got install event for unknown install ${payload.id}`);
           Cu.reportError(err);
           return;
         }
         install._dispatch(payload);
         break;
       }
+
+      case MSG_ADDON_EVENT: {
+        if (this._eventListener) {
+          this._eventListener(payload);
+        }
+      }
     }
   },
 
   sendRequest: function(type, ...args) {
     return new Promise((resolve, reject) => {
       let callbackID = this._nextID++;
 
       this._promises.set(callbackID, { resolve, reject });
       Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
     });
   },
 
+  setAddonListener(callback) {
+    this._eventListener = callback;
+    if (callback) {
+      Services.cpmm.addMessageListener(MSG_ADDON_EVENT, this);
+      Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: true});
+    } else {
+      Services.cpmm.removeMessageListener(MSG_ADDON_EVENT, this);
+      Services.cpmm.sendAsyncMessage(MSG_ADDON_EVENT_REQ, {enabled: false});
+    }
+  },
+
   sendCleanup: function(ids) {
+    this.setAddonListener(null);
     Services.cpmm.sendAsyncMessage(MSG_INSTALL_CLEANUP, { ids });
   },
 };
 
 APIBroker.init();
 
 function Addon(window, properties) {
   this.window = window;
@@ -166,16 +188,17 @@ AddonInstall.prototype = {
 
 function WebAPI() {
 }
 
 WebAPI.prototype = {
   init(window) {
     this.window = window;
     this.allInstalls = [];
+    this.listenerCount = 0;
 
     window.addEventListener("unload", event => {
       APIBroker.sendCleanup(this.allInstalls);
     });
   },
 
   getAddonByID: WebAPITask(function*(id) {
     let addonInfo = yield APIBroker.sendRequest("getAddonByID", id);
@@ -187,14 +210,31 @@ WebAPI.prototype = {
     if (!installInfo) {
       return null;
     }
     let install = new AddonInstall(this.window, installInfo);
     this.allInstalls.push(installInfo.id);
     return install;
   }),
 
+  eventListenerWasAdded(type) {
+    if (this.listenerCount == 0) {
+      APIBroker.setAddonListener(data => {
+        let event = new this.window.AddonEvent(data.event, data);
+        this.__DOM_IMPL__.dispatchEvent(event);
+      });
+    }
+    this.listenerCount++;
+  },
+
+  eventListenerWasRemoved(type) {
+    this.listenerCount--;
+    if (this.listenerCount == 0) {
+      APIBroker.setAddonListener(null);
+    }
+  },
+
   classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"),
   contractID: "@mozilla.org/addon-web-api/manager;1",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer])
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]);
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -32,16 +32,17 @@ support-files =
   browser_updatessl.rdf
   browser_updatessl.rdf^headers^
   browser_install.rdf
   browser_install.rdf^headers^
   browser_install.xml
   browser_install1_3.xpi
   browser_eula.xml
   browser_purchase.xml
+  webapi_addon_listener.html
   webapi_checkavailable.html
   webapi_checkchromeframe.xul
   webapi_checkframed.html
   webapi_checknavigatedwindow.html
   !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
   !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
@@ -61,12 +62,13 @@ skip-if = require_signing
 [browser_installssl.js]
 [browser_newaddon.js]
 [browser_updatessl.js]
 [browser_task_next_test.js]
 [browser_discovery_install.js]
 [browser_update.js]
 [browser_webapi.js]
 [browser_webapi_access.js]
+[browser_webapi_addon_listener.js]
 [browser_webapi_install.js]
 [browser_webapi_uninstall.js]
 
 [include:browser-common.ini]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
@@ -0,0 +1,147 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+
+function* getListenerEvents(browser) {
+  let result = yield ContentTask.spawn(browser, null, function*() {
+    return content.document.getElementById("result").textContent;
+  });
+
+  return result.split('\n').map(JSON.parse);
+}
+
+const RESTART_ID = "restart@tests.mozilla.org";
+const RESTART_DISABLED_ID = "restart_disabled@tests.mozilla.org";
+const RESTARTLESS_ID = "restartless@tests.mozilla.org";
+const INSTALL_ID = "install@tests.mozilla.org";
+
+let provider = new MockProvider(false);
+provider.createAddons([
+  {
+    id: RESTART_ID,
+    name: "Add-on that requires restart",
+  },
+  {
+    id: RESTART_DISABLED_ID,
+    name: "Disabled add-on that requires restart",
+    userDisabled: true,
+  },
+  {
+    id: RESTARTLESS_ID,
+    name: "Restartless add-on",
+    operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+  },
+]);
+
+// Test disable of add-on requiring restart
+add_task(function* test_disable() {
+  yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+    let addon = yield promiseAddonByID(RESTART_ID);
+    is(addon.userDisabled, false, "addon is enabled");
+
+    // disable it
+    addon.userDisabled = true;
+    is(addon.userDisabled, true, "addon was disabled successfully");
+
+    let events = yield getListenerEvents(browser);
+
+    // Just a single onDisabling since restart is needed to complete
+    let expected = [
+      {id: RESTART_ID, needsRestart: true, event: "onDisabling"},
+    ];
+    Assert.deepEqual(events, expected, "Got expected disable event");
+  });
+});
+
+// Test enable of add-on requiring restart
+add_task(function* test_enable() {
+  yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+    let addon = yield promiseAddonByID(RESTART_DISABLED_ID);
+    is(addon.userDisabled, true, "addon is disabled");
+
+    // enable it
+    addon.userDisabled = false;
+    is(addon.userDisabled, false, "addon was enabled successfully");
+
+    let events = yield getListenerEvents(browser);
+
+    // Just a single onEnabling since restart is needed to complete
+    let expected = [
+      {id: RESTART_DISABLED_ID, needsRestart: true, event: "onEnabling"},
+    ];
+    Assert.deepEqual(events, expected, "Got expected enable event");
+  });
+});
+
+// Test enable/disable events for restartless
+add_task(function* test_restartless() {
+  yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+    let addon = yield promiseAddonByID(RESTARTLESS_ID);
+    is(addon.userDisabled, false, "addon is enabled");
+
+    // disable it
+    addon.userDisabled = true;
+    is(addon.userDisabled, true, "addon was disabled successfully");
+
+    // re-enable it
+    addon.userDisabled = false;
+    is(addon.userDisabled, false, "addon was re-enabled successfuly");
+
+    let events = yield getListenerEvents(browser);
+    let expected = [
+      {id: RESTARTLESS_ID, needsRestart: false, event: "onDisabling"},
+      {id: RESTARTLESS_ID, needsRestart: false, event: "onDisabled"},
+      {id: RESTARTLESS_ID, needsRestart: false, event: "onEnabling"},
+      {id: RESTARTLESS_ID, needsRestart: false, event: "onEnabled"},
+    ];
+    Assert.deepEqual(events, expected, "Got expected disable/enable events");
+  });
+});
+
+// Test install events
+add_task(function* test_restartless() {
+  yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+    let addon = new MockAddon(INSTALL_ID, "installme", null,
+                              AddonManager.OP_NEED_RESTART_NONE);
+    let install = new MockInstall(null, null, addon);
+
+    let installPromise = new Promise(resolve => {
+      install.addTestListener({
+        onInstallEnded: resolve,
+      });
+    });
+
+    provider.addInstall(install);
+    install.install();
+
+    yield installPromise;
+
+    let events = yield getListenerEvents(browser);
+    let expected = [
+      {id: INSTALL_ID, needsRestart: false, event: "onInstalling"},
+      {id: INSTALL_ID, needsRestart: false, event: "onInstalled"},
+    ];
+    Assert.deepEqual(events, expected, "Got expected install events");
+  });
+});
+
+// Test uninstall
+add_task(function* test_uninstall() {
+  yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+    let addon = yield promiseAddonByID(RESTARTLESS_ID);
+    isnot(addon, null, "Found add-on for uninstall");
+
+    addon.uninstall();
+
+    let events = yield getListenerEvents(browser);
+    let expected = [
+      {id: RESTARTLESS_ID, needsRestart: false, event: "onUninstalling"},
+      {id: RESTARTLESS_ID, needsRestart: false, event: "onUninstalled"},
+    ];
+    Assert.deepEqual(events, expected, "Got expected uninstall events");
+  });
+});
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -526,16 +526,22 @@ function is_element_visible(aElement, aM
   ok(!is_hidden(aElement), aMsg || (aElement + " should be visible"));
 }
 
 function is_element_hidden(aElement, aMsg) {
   isnot(aElement, null, "Element should not be null, when checking visibility");
   ok(is_hidden(aElement), aMsg || (aElement + " should be hidden"));
 }
 
+function promiseAddonByID(aId) {
+  return new Promise(resolve => {
+    AddonManager.getAddonByID(aId, resolve);
+  });
+}
+
 function promiseAddonsByIDs(aIDs) {
   return new Promise(resolve => {
     AddonManager.getAddonsByIDs(aIDs, resolve);
   });
 }
 /**
  * Install an add-on and call a callback when complete.
  *
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+let events = [];
+let resultEl = document.getElementById("result");
+[ "onEnabling",
+  "onEnabled",
+  "onDisabling",
+  "onDisabled",
+  "onInstalling",
+  "onInstalled",
+  "onUninstalling",
+  "onUninstalled",
+].forEach(event => {
+  navigator.mozAddonManager.addEventListener(event, data => {
+    let obj = {event, id: data.id, needsRestart: data.needsRestart};
+    events.push(JSON.stringify(obj));
+    resultEl.textContent = events.join('\n');
+  });
+});
+</script>
+</body>
+</html>