Bug 1364945 - Fix runtime.openOptionsPage on Firefox for Android. draft
authorLuca Greco <lgreco@mozilla.com>
Mon, 07 Aug 2017 17:56:36 +0200
changeset 650523 808314897955796c527851a50ec20bf010580d9c
parent 648260 04bee69b3274bd8d5cf52d54a0a5cc14dbe8693a
child 650524 46343e52f208091316835c080244b8942499b722
push id75434
push userluca.greco@alcacoop.it
push dateTue, 22 Aug 2017 15:46:25 +0000
bugs1364945
milestone57.0a1
Bug 1364945 - Fix runtime.openOptionsPage on Firefox for Android. MozReview-Commit-ID: Envx19jlCjY
mobile/android/chrome/content/aboutAddons.js
mobile/android/chrome/content/browser.js
mobile/android/components/extensions/ext-android.js
mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -90,39 +90,60 @@ var ContextMenus = {
     Addons.setEnabled(false, this.target.addon);
     this.target = null;
   },
 
   uninstall: function(event) {
     Addons.uninstall(this.target.addon);
     this.target = null;
   }
+};
+
+function sendEMPong() {
+  Services.obs.notifyObservers(window, "EM-pong");
 }
 
-function init() {
+async function init() {
   window.addEventListener("popstate", onPopState);
 
   AddonManager.addInstallListener(Addons);
   AddonManager.addAddonListener(Addons);
-  Addons.init();
+
+  await Addons.init();
   showAddons();
   ContextMenus.init();
+
+  Services.obs.addObserver(sendEMPong, "EM-ping");
+
+  // The addons list has been loaded and rendered, send a notification
+  // if the openOptionsPage is waiting to be able to select an addon details page.
+  Services.obs.notifyObservers(window, "EM-loaded");
 }
 
-
 function uninit() {
   AddonManager.removeInstallListener(Addons);
   AddonManager.removeAddonListener(Addons);
+
+  Services.obs.removeObserver(sendEMPong, "EM-ping");
 }
 
 function openLink(url) {
   let BrowserApp = gChromeWin.BrowserApp;
   BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
 }
 
+function openOptionsInTab(url) {
+  let BrowserApp = gChromeWin.BrowserApp;
+  BrowserApp.selectOrAddTab(url, {
+    startsWith: true,
+    selected: true,
+    parentId: BrowserApp.selectedTab.id
+  });
+}
+
 function onPopState(aEvent) {
   // Called when back/forward is used to change the state of the page
   if (aEvent.state) {
     // Show the detail page for an addon
     const listItem = Addons._getElementForAddon(aEvent.state.id);
     if (listItem) {
       Addons.showDetails(listItem);
     } else {
@@ -133,16 +154,26 @@ function onPopState(aEvent) {
     // Clear any previous detail addon
     let detailItem = document.querySelector("#addons-details > .addon-item");
     detailItem.addon = null;
 
     showAddons();
   }
 }
 
+function showAddonDetails(addonId) {
+  const listItem = Addons._getElementForAddon(addonId);
+  if (listItem) {
+    Addons.showDetails(listItem);
+    history.pushState({ id: addonId }, document.title);
+  } else {
+    throw new Error(`Addon not found: ${addonId}`);
+  }
+}
+
 function showAddons() {
   // Hide the addon options and show the addons list
   let details = document.querySelector("#addons-details");
   details.classList.add("hidden");
   let list = document.querySelector("#addons-list");
   list.classList.remove("hidden");
   document.documentElement.removeAttribute("details");
 
@@ -273,39 +304,39 @@ var Addons = {
   },
 
   _getElementForAddon: function(aKey) {
     let list = document.getElementById("addons-list");
     let element = list.querySelector("div[addonID=\"" + CSS.escape(aKey) + "\"]");
     return element;
   },
 
-  init: function init() {
-    let self = this;
-    AddonManager.getAllAddons(function(aAddons) {
-      // Clear all content before filling the addons
-      let list = document.getElementById("addons-list");
-      list.innerHTML = "";
+  init: async function init() {
+    const aAddons = await AddonManager.getAllAddons();
+
+    // Clear all content before filling the addons
+    let list = document.getElementById("addons-list");
+    list.innerHTML = "";
+
+    aAddons.sort(function(a, b) {
+      return a.name.localeCompare(b.name);
+    });
 
-      aAddons.sort(function(a, b) {
-        return a.name.localeCompare(b.name);
-      });
-      for (let i = 0; i < aAddons.length; i++) {
-        // Don't create item for system add-ons.
-        if (aAddons[i].isSystem)
-          continue;
+    for (let i = 0; i < aAddons.length; i++) {
+      // Don't create item for system add-ons.
+      if (aAddons[i].isSystem)
+        continue;
 
-        let item = self._createItemForAddon(aAddons[i]);
-        list.appendChild(item);
-      }
+      let item = this._createItemForAddon(aAddons[i]);
+      list.appendChild(item);
+    }
 
-      // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
-      let browseItem = self._createBrowseItem();
-      list.appendChild(browseItem);
-    });
+    // Add a "Browse all Firefox Add-ons" item to the bottom of the list.
+    let browseItem = this._createBrowseItem();
+    list.appendChild(browseItem);
 
     document.getElementById("uninstall-btn").addEventListener("click", Addons.uninstallCurrent.bind(this));
     document.getElementById("cancel-btn").addEventListener("click", Addons.cancelUninstall.bind(this));
     document.getElementById("disable-btn").addEventListener("click", Addons.disable.bind(this));
     document.getElementById("enable-btn").addEventListener("click", Addons.enable.bind(this));
 
     document.getElementById("unsigned-learn-more").addEventListener("click", function() {
       openLink(Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons");
@@ -367,27 +398,59 @@ var Addons = {
         optionsBox.classList.remove("inner");
 
         // WebExtensions are loaded asynchronously and the optionsURL
         // may not be available via listitem when the add-on has just been
         // installed, but it is available on the addon if one is set.
         detailItem.setAttribute("optionsURL", addon.optionsURL);
         this.createWebExtensionOptions(optionsBox, addon.optionsURL, addon.optionsBrowserStyle);
         break;
+      case AddonManager.OPTIONS_TYPE_TAB:
+        // Keep the usual layout for any options related the legacy (or system) add-ons
+        // when the options are opened in a new tab from a single button in the addon
+        // details page.
+        optionsBox.classList.add("inner");
+
+        this.createOptionsInTabButton(optionsBox, addon);
+        break;
       case AddonManager.OPTIONS_TYPE_INLINE:
         // Keep the usual layout for any options related the legacy (or system) add-ons.
         optionsBox.classList.add("inner");
 
         this.createInlineOptions(optionsBox, optionsURL, aListItem);
         break;
     }
 
     showAddonOptions();
   },
 
+  createOptionsInTabButton: function(destination, addon) {
+    let frame = destination.querySelector("iframe#addon-options");
+    let button = destination.querySelector("button#open-addon-options");
+
+    if (frame) {
+      // Remove any existent options frame (e.g. when the addon updates
+      // contains the open_in_tab options for the first time).
+
+      frame.remove();
+    }
+
+    if (!button) {
+      button = document.createElement("button");
+      button.setAttribute("id", "open-addon-options");
+      button.textContent = gStringBundle.GetStringFromName("addon.options");
+      destination.appendChild(button);
+    }
+
+    button.onclick = async () => {
+      const {optionsURL} = addon;
+      openOptionsInTab(optionsURL);
+    };
+  },
+
   createWebExtensionOptions: async function(destination, optionsURL, browserStyle) {
     let originalHeight;
     let frame = document.createElement("iframe");
     frame.setAttribute("id", "addon-options");
     frame.setAttribute("mozbrowser", "true");
     frame.setAttribute("style", "width: 100%; overflow: hidden;");
 
     // Adjust iframe height to the iframe content (also between navigation of multiple options
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1410,16 +1410,55 @@ var BrowserApp = {
       tab = this.addTab(aURL, aParams);
     } else {
       this.selectTab(tab);
     }
 
     return tab;
   },
 
+  /**
+   * Open or select a tab with the "about:addons" page and optionally
+   * switch to the details page related to a defined addonId.
+   *
+   * @param {string} addonId
+   */
+  openAddonManager: function openAddonManager({addonId}) {
+    if (addonId) {
+      let emWindow;
+
+      function receivePong(subject, topic, data) {
+        emWindow = subject;
+      };
+
+      Services.obs.addObserver(receivePong, "EM-pong");
+      Services.obs.notifyObservers(null, "EM-ping");
+      Services.obs.removeObserver(receivePong, "EM-pong");
+
+      if (emWindow) {
+        // "about:addons" has been already loaded in a tab.
+        emWindow.showAddonDetails(addonId);
+      } else {
+        // Wait for "about:addons" to be fully loaded.
+        function waitAboutAddons(subject, topic, data) {
+          Services.obs.removeObserver(waitAboutAddons, "EM-loaded");
+          emWindow = subject;
+
+          emWindow.showAddonDetails(addonId);
+        }
+        Services.obs.addObserver(waitAboutAddons, "EM-loaded");
+      }
+    }
+
+    BrowserApp.selectOrAddTab("about:addons", {
+      selected: true,
+      parentId: BrowserApp.selectedTab.id,
+    });
+  },
+
   // This method updates the state in BrowserApp after a tab has been selected
   // in the Java UI.
   _handleTabSelected: function _handleTabSelected(aTab) {
     if (this.fullscreenTransitionTab) {
       // Defer updating to "fullscreenchange" if tab selection happened during
       // a fullscreen transition.
       return;
     }
--- a/mobile/android/components/extensions/ext-android.js
+++ b/mobile/android/components/extensions/ext-android.js
@@ -43,16 +43,35 @@ extensions.on("page-shutdown", (type, co
       if (nativeTab) {
         BrowserApp.closeTab(nativeTab);
       }
     }
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
+global.openOptionsPage = (extension) => {
+  let window = windowTracker.topWindow;
+  if (!window) {
+    return Promise.reject({message: "No browser window available"});
+  }
+
+  let {BrowserApp} = window;
+
+  if (extension.manifest.options_ui.open_in_tab) {
+    BrowserApp.selectOrAddTab(extension.manifest.options_ui.page, {
+      selected: true,
+      parentId: BrowserApp.selectedTab.id,
+    });
+  } else {
+    BrowserApp.openAddonManager({addonId: extension.id});
+  }
+
+  return Promise.resolve();
+};
 
 extensions.registerModules({
   browserAction: {
     url: "chrome://browser/content/ext-browserAction.js",
     schema: "chrome://browser/content/schemas/browser_action.json",
     scopes: ["addon_parent"],
     manifest: ["browser_action"],
     paths: [
--- a/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html
@@ -56,16 +56,35 @@ async function clickOnLinkInOptionsPage(
   const optionsIframe = content.document.querySelector(`#addon-options`);
   optionsIframe.contentDocument.querySelector(selector).click();
 }
 
 async function navigateBack() {
   content.window.history.back();
 }
 
+function waitDOMContentLoaded(checkUrlCb) {
+  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
+
+  return new Promise(resolve => {
+    let listener = (event) => {
+      if (checkUrlCb(event.target.defaultView.location.href)) {
+        BrowserApp.deck.removeEventListener("DOMContentLoaded", listener);
+        resolve();
+      }
+    };
+
+    BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
+  });
+}
+
+function waitAboutAddonsLoaded() {
+  return waitDOMContentLoaded(url => url === "about:addons");
+}
+
 add_task(async function test_options_ui_iframe_height() {
   let addonID = "test-options-ui@mozilla.org";
 
   let extension = ExtensionTestUtils.loadExtension({
     useAddonManager: "temporary",
     manifest: {
       applications: {
         gecko: {id: addonID},
@@ -108,44 +127,30 @@ add_task(async function test_options_ui_
           </body>
         </html>
       `,
     },
   });
 
   await extension.startup();
 
-  let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
-  let BrowserApp = chromeWin.BrowserApp;
+  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
 
-  let waitAboutAddonsLoaded = new Promise(resolve => {
-    let listener = (event) => {
-      if (event.target.defaultView.location.href === "about:addons") {
-        BrowserApp.deck.removeEventListener("DOMContentLoaded", listener);
-        resolve();
-      }
-    };
-
-    BrowserApp.deck.addEventListener("DOMContentLoaded", listener);
-  });
+  let onceAboutAddonsLoaded = waitAboutAddonsLoaded();
 
   BrowserApp.selectOrAddTab("about:addons", {
     selected: true,
     parentId: BrowserApp.selectedTab.id,
   });
 
-  await waitAboutAddonsLoaded;
+  await onceAboutAddonsLoaded;
 
   is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
      "about:addons is the currently selected tab");
 
-  let aboutAddonsWindow = BrowserApp.selectedTab.browser.contentWindow;
-
-  is(aboutAddonsWindow.location.href, "about:addons");
-
   await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, waitAboutAddonsRendered);
 
   await ContentTask.spawn(BrowserApp.selectedTab.browser, addonID, navigateToAddonDetails);
 
   const optionsSizes = await ContentTask.spawn(
     BrowserApp.selectedTab.browser, [addonID, "Options page 1"], waitAddonOptionsPage
   );
 
@@ -185,12 +190,87 @@ add_task(async function test_options_ui_
   is(backToOptionsSizes.iframeHeight, optionsSizes.iframeHeight,
      `When navigating back, the old iframe size is restored (${backToOptionsSizes.iframeHeight})`);
 
   BrowserApp.closeTab(BrowserApp.selectedTab);
 
   await extension.unload();
 });
 
+add_task(async function test_options_ui_open_aboutaddons_details() {
+  let addonID = "test-options-ui-open-addon-details@mozilla.org";
+
+  function background() {
+    browser.test.onMessage.addListener(msg => {
+      if (msg !== "runtime.openOptionsPage") {
+        browser.test.fail(`Received unexpected test message: ${msg}`);
+        return;
+      }
+
+      browser.runtime.openOptionsPage();
+    });
+  }
+
+  function optionsScript() {
+    browser.test.sendMessage("options-page-loaded", window.location.href);
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    useAddonManager: "temporary",
+    background,
+    manifest: {
+      applications: {
+        gecko: {id: addonID},
+      },
+      name: "Options UI open addon details Extension",
+      description: "Longer addon description",
+      options_ui: {
+        page: "options.html",
+      },
+    },
+    files: {
+      "options.js": optionsScript,
+      "options.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+          </head>
+          <body>
+            <h1>Options page</h1>
+            <script src="options.js"><\/script>
+          </body>
+        </html>
+      `,
+    },
+  });
+
+  await extension.startup();
+
+  const {BrowserApp} = Services.wm.getMostRecentWindow("navigator:browser");
+
+  let onceAboutAddonsLoaded = waitAboutAddonsLoaded();
+
+  BrowserApp.selectOrAddTab("about:addons", {
+    selected: true,
+    parentId: BrowserApp.selectedTab.id,
+  });
+
+  await onceAboutAddonsLoaded;
+
+  is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
+     "about:addons is the currently selected tab");
+
+  info("Wait runtime.openOptionsPage to open the about:addond details in the existent tab");
+  extension.sendMessage("runtime.openOptionsPage");
+  await extension.awaitMessage("options-page-loaded");
+
+  is(BrowserApp.selectedTab.currentURI.spec, "about:addons",
+     "about:addons is still the currently selected tab once the options has been loaded");
+
+  BrowserApp.closeTab(BrowserApp.selectedTab);
+
+  await extension.unload();
+});
+
 </script>
 
 </body>
 </html>