Bug 1268077: expose AddonListener through mozAddonManager r?smaug,rhelmer draft
authorAndrew Swan <aswan@mozilla.com>
Fri, 27 May 2016 15:43:05 -0700
changeset 374695 68c8c26caf6d8a10961990128134fc9b770f9fe3
parent 374516 0ce9bb45bb3d30cc8781260e5265c03cc9c945f2
child 522676 012e36f63e59aafabc12744898d42bc75c006e9a
push id20068
push useraswan@mozilla.com
push dateThu, 02 Jun 2016 19:57:19 +0000
reviewerssmaug, rhelmer
bugs1268077
milestone49.0a1
Bug 1268077: expose AddonListener through mozAddonManager r?smaug,rhelmer 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
@@ -754,16 +754,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>