Bug 1308310 Post-install notification for mozAddonManager installs draft
authorAndrew Swan <aswan@mozilla.com>
Thu, 19 Jan 2017 21:26:23 -0800
changeset 465851 526b54952b78f50c05d13c834df6d63bf0e4db60
parent 464990 5a4412474c63e1d9e66036d603ac42e9cb2b9150
child 543278 706921cbc15c8d4b4fa7b0b5c4b406dfca683c1d
push id42743
push useraswan@mozilla.com
push dateWed, 25 Jan 2017 00:20:24 +0000
bugs1308310
milestone54.0a1
Bug 1308310 Post-install notification for mozAddonManager installs With this patch, the Promise returned by install() does not resolve until after the post-install notification prompt has been displayed and dismissed (either by an explicit click on "Ok" or by clicking somewhere else in the browser UI). That promise is also now rejected if the install fails or is cancelled. MozReview-Commit-ID: ej0HmOaU7q
browser/base/content/test/general/browser_extension_permissions.js
browser/base/content/test/general/file_install_extensions.html
browser/modules/ExtensionsUI.jsm
toolkit/mozapps/extensions/AddonManager.jsm
toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
--- a/browser/base/content/test/general/browser_extension_permissions.js
+++ b/browser/base/content/test/general/browser_extension_permissions.js
@@ -58,25 +58,26 @@ function checkNotification(panel, url) {
 
     is(header.getAttribute("hidden"), "true", "Permission list header is hidden");
     is(ul.childElementCount, 0, "Permissions list has 0 entries");
   }
 }
 
 const INSTALL_FUNCTIONS = [
   function installMozAM(url) {
-    ContentTask.spawn(gBrowser.selectedBrowser, url, function*(cUrl) {
-      content.wrappedJSObject.installMozAM(cUrl);
+    return ContentTask.spawn(gBrowser.selectedBrowser, url, function*(cUrl) {
+      yield content.wrappedJSObject.installMozAM(cUrl);
     });
   },
 
   function installTrigger(url) {
     ContentTask.spawn(gBrowser.selectedBrowser, url, function*(cUrl) {
       content.wrappedJSObject.installTrigger(cUrl);
     });
+    return Promise.resolve();
   },
 ];
 
 add_task(function* () {
   yield SpecialPowers.pushPrefEnv({set: [
     ["extensions.webapi.testing", true],
     ["extensions.install.requireBuiltInCerts", false],
 
@@ -112,27 +113,39 @@ add_task(function* () {
         onInstallFailed() {
           AddonManager.removeInstallListener(listener);
           resolve(false);
         },
       };
       AddonManager.addInstallListener(listener);
     });
 
-    installFn(url);
+    let installMethodPromise = installFn(url);
 
     let panel = yield promisePopupNotificationShown("addon-webext-permissions");
     checkNotification(panel, url);
 
     if (cancel) {
       panel.secondaryButton.click();
+      try {
+        yield installMethodPromise;
+      } catch (err) {}
     } else {
+      // Look for post-install notification
+      let postInstallPromise = promisePopupNotificationShown("addon-installed");
       panel.button.click();
+
+      // Press OK on the post-install notification
+      panel = yield postInstallPromise;
+      panel.button.click();
+
+      yield installMethodPromise;
     }
 
+
     let result = yield installPromise;
     let addon = yield promiseGetAddonByID(ID);
     if (cancel) {
       ok(!result, "Installation was cancelled");
       is(addon, null, "Extension is not installed");
     } else {
       ok(result, "Installation completed");
       isnot(addon, null, "Extension is installed");
--- a/browser/base/content/test/general/file_install_extensions.html
+++ b/browser/base/content/test/general/file_install_extensions.html
@@ -2,19 +2,18 @@
 
 <html>
 <head>
   <meta charset="utf-8">
 </head>
 <body>
 <script type="text/javascript">
 function installMozAM(url) {
-  return navigator.mozAddonManager.createInstall({url}).then(install => {
-    install.install();
-  });
+  return navigator.mozAddonManager.createInstall({url})
+                  .then(install => install.install());
 }
 
 function installTrigger(url) {
   InstallTrigger.install({extension: url});
 }
 </script>
 </body>
 </html>
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -138,18 +138,22 @@ this.ExtensionsUI = {
       } else {
         info.permissions = perms;
         this.showPermissionsPrompt(target, info).then(reply);
       }
     } else if (topic == "webextension-update-permissions") {
       this.updates.add(subject.wrappedJSObject);
       this.emit("change");
     } else if (topic == "webextension-install-notify") {
-      let {target, addon} = subject.wrappedJSObject;
-      this.showInstallNotification(target, addon);
+      let {target, addon, callback} = subject.wrappedJSObject;
+      this.showInstallNotification(target, addon).then(() => {
+        if (callback) {
+          callback();
+        }
+      });
     }
   },
 
   showPermissionsPrompt(target, info) {
     let perms = info.permissions;
     if (!perms) {
       return Promise.resolve();
     }
@@ -318,34 +322,38 @@ this.ExtensionsUI = {
     let appName = brandBundle.getString("brandShortName");
 
     let bundle = win.gNavigatorBundle;
     let msg1 = bundle.getFormattedString("addonPostInstall.message1",
                                          [addonLabel, appName]);
     let msg2 = bundle.getFormattedString("addonPostInstall.message2",
                                          [addonLabel, addonIcon, toolbarIcon]);
 
-    let action = {
-      label: bundle.getString("addonPostInstall.okay.label"),
-      accessKey: bundle.getString("addonPostInstall.okay.key"),
-      callback: () => {},
-    };
+    return new Promise(resolve => {
+      let action = {
+        label: bundle.getString("addonPostInstall.okay.label"),
+        accessKey: bundle.getString("addonPostInstall.okay.key"),
+        callback: resolve,
+      };
 
-    let options = {
-      hideClose: true,
-      popupIconURL: addon.iconURL || DEFAULT_EXTENSION_ICON,
-      eventCallback(topic) {
-        if (topic == "showing") {
-          let doc = this.browser.ownerDocument;
-          doc.getElementById("addon-installed-notification-header")
-             .innerHTML = msg1;
-          doc.getElementById("addon-installed-notification-message")
-             .innerHTML = msg2;
+      let options = {
+        hideClose: true,
+        popupIconURL: addon.iconURL || DEFAULT_EXTENSION_ICON,
+        eventCallback(topic) {
+          if (topic == "showing") {
+            let doc = this.browser.ownerDocument;
+            doc.getElementById("addon-installed-notification-header")
+               .innerHTML = msg1;
+            doc.getElementById("addon-installed-notification-message")
+               .innerHTML = msg2;
+          } else if (topic == "dismissed") {
+            resolve();
+          }
         }
-      }
-    };
+      };
 
-    popups.show(target, "addon-installed", "", "addons-notification-icon",
-                action, null, options);
+      popups.show(target, "addon-installed", "", "addons-notification-icon",
+                  action, null, options);
+    });
   },
 };
 
 EventEmitter.decorate(ExtensionsUI);
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2987,24 +2987,39 @@ var AddonManagerInternal = {
         "onDownloadFailed",
         "onInstallStarted",
         "onInstallEnded",
         "onInstallCancelled",
         "onInstallFailed",
       ];
 
       let listener = {};
-      events.forEach(event => {
-        listener[event] = (install) => {
-          let data = {event, id};
-          AddonManager.webAPI.copyProps(install, data);
-          this.sendEvent(mm, data);
-        }
+      let installPromise = new Promise((resolve, reject) => {
+        events.forEach(event => {
+          listener[event] = (install, addon) => {
+            let data = {event, id};
+            AddonManager.webAPI.copyProps(install, data);
+            this.sendEvent(mm, data);
+            if (event == "onInstallEnded") {
+              resolve(addon);
+            } else if (event == "onDownloadFailed" || event == "onInstallFailed") {
+              reject({message: "install failed"});
+            } else if (event == "onDownloadCancelled" || event == "onInstallCancelled") {
+              reject({message: "install cancelled"});
+            }
+          }
+        });
       });
-      return listener;
+
+      // We create the promise here since this is where we're setting
+      // up the InstallListener, but if the install is never started,
+      // no handlers will be attached so make sure we terminate errors.
+      installPromise.catch(() => {});
+
+      return {listener, installPromise};
     },
 
     forgetInstall(id) {
       let info = this.installs.get(id);
       if (!info) {
         throw new Error(`forgetInstall cannot find ${id}`);
       }
       info.install.removeListener(info.listener);
@@ -3028,25 +3043,25 @@ var AddonManagerInternal = {
       }
 
       try {
         checkInstallUrl(options.url);
       } catch (err) {
         return Promise.reject({message: err.message});
       }
 
-      return AddonManagerInternal.getInstallForURL(options.url, "application/x-xpinstall",
-                                                   options.hash).then(install => {
+      return AddonManagerInternal.getInstallForURL(options.url, "application/x-xpinstall", options.hash)
+                                 .then(install => {
         AddonManagerInternal.setupPromptHandler(target, null, install, false);
 
         let id = this.nextInstall++;
-        let listener = this.makeListener(id, target.messageManager);
+        let {listener, installPromise} = this.makeListener(id, target.messageManager);
         install.addListener(listener);
 
-        this.installs.set(id, {install, target, listener});
+        this.installs.set(id, {install, target, listener, installPromise});
 
         let result = {id};
         this.copyProps(install, result);
         return result;
       });
     },
 
     addonUninstall(target, id) {
@@ -3079,17 +3094,27 @@ var AddonManagerInternal = {
       });
     },
 
     addonInstallDoInstall(target, id) {
       let state = this.installs.get(id);
       if (!state) {
         return Promise.reject(`invalid id ${id}`);
       }
-      return Promise.resolve(state.install.install());
+      let result = state.install.install();
+
+      return state.installPromise.then(addon => new Promise(resolve => {
+        let callback = () => resolve(result);
+        if (Preferences.get(PREF_WEBEXT_PERM_PROMPTS, false)) {
+          let subject = {wrappedJSObject: {target, addon, callback}};
+          Services.obs.notifyObservers(subject, "webextension-install-notify", null)
+        } else {
+          callback();
+        }
+      }));
     },
 
     addonInstallCancel(target, id) {
       let state = this.installs.get(id);
       if (!state) {
         return Promise.reject(`invalid id ${id}`);
       }
       return Promise.resolve(state.install.cancel());
--- a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
@@ -120,17 +120,26 @@ function* testInstall(browser, args, ste
         check();
       });
     }
 
     while (steps.length > 0) {
       let nextStep = steps.shift();
       if (nextStep.action) {
         if (nextStep.action == "install") {
-          yield install.install();
+          try {
+            yield install.install();
+            if (nextStep.expectError) {
+              throw new Error("Expected install to fail but it did not");
+            }
+          } catch (err) {
+            if (!nextStep.expectError) {
+              throw new Error("Install failed unexpectedly");
+            }
+          }
         } else if (nextStep.action == "cancel") {
           yield install.cancel();
         } else {
           throw new Error(`unknown action ${nextStep.action}`);
         }
       } else {
         yield expectEvent(nextStep.event, nextStep.props);
       }
@@ -229,17 +238,17 @@ add_task(makeInstallTest(function* (brow
   let addons = yield promiseAddonsByIDs([ID]);
   is(addons[0], null, "The addon was not installed");
 
   ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
 }));
 
 add_task(makeInstallTest(function* (browser) {
   let steps = [
-    {action: "install"},
+    {action: "install", expectError: true},
     {
       event: "onDownloadStarted",
       props: {state: "STATE_DOWNLOADING"},
     },
     {event: "onDownloadProgress"},
     {
       event: "onDownloadFailed",
       props: {
@@ -254,17 +263,17 @@ add_task(makeInstallTest(function* (brow
   let addons = yield promiseAddonsByIDs([ID]);
   is(addons[0], null, "The addon was not installed");
 
   ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
 }));
 
 add_task(makeInstallTest(function* (browser) {
   let steps = [
-    {action: "install"},
+    {action: "install", expectError: true},
     {
       event: "onDownloadStarted",
       props: {state: "STATE_DOWNLOADING"},
     },
     {event: "onDownloadProgress"},
     {
       event: "onDownloadFailed",
       props: {