Bug 1383160 - Fix Android pageAction popup behavior. draft
authorLuca Greco <lgreco@mozilla.com>
Tue, 15 Aug 2017 22:55:13 +0200
changeset 657417 68ab8e9fa3d8d8fd8ac54de54a05b8f01d6018e6
parent 655949 1b20c80ff14689acbeb2f3485d29c31b88c8ee35
child 657525 346430b1bb7c64942df4732ca36c71b9770fb9b3
push id77516
push userluca.greco@alcacoop.it
push dateFri, 01 Sep 2017 13:08:23 +0000
bugs1383160
milestone57.0a1
Bug 1383160 - Fix Android pageAction popup behavior. MozReview-Commit-ID: 66PnjFv4IIx
mobile/android/components/extensions/ext-pageAction.js
mobile/android/components/extensions/ext-utils.js
mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -45,21 +45,17 @@ class PageAction extends EventEmitter {
       id: `{${extension.uuid}}`,
       clickCallback: () => {
         let tab = tabTracker.activeTab;
 
         this.tabManager.addActiveTabPermission(tab);
 
         let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
         if (popup) {
-          let win = Services.wm.getMostRecentWindow("navigator:browser");
-          win.BrowserApp.addTab(popup, {
-            selected: true,
-            parentId: win.BrowserApp.selectedTab.id,
-          });
+          tabTracker.openExtensionPopupTab(popup);
         } else {
           this.emit("click", tab);
         }
       },
     };
 
     this.shouldShow = false;
 
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -245,24 +245,84 @@ global.WindowEventManager = class extend
       return () => {
         windowTracker.removeListener(event, listener2);
       };
     });
   }
 };
 
 class TabTracker extends TabTrackerBase {
+  constructor() {
+    super();
+
+    // Keep track of the extension popup tab.
+    this._extensionPopupTabWeak = null;
+  }
+
   init() {
     if (this.initialized) {
       return;
     }
     this.initialized = true;
 
     windowTracker.addListener("TabClose", this);
     windowTracker.addListener("TabOpen", this);
+
+    // Register a listener for the Tab:Selected global event,
+    // so that we can close the popup when a popup tab has been
+    // unselected.
+    GlobalEventDispatcher.registerListener(this, [
+      "Tab:Selected",
+    ]);
+  }
+
+  /**
+   * Returns the currently opened popup tab if any
+   */
+  get extensionPopupTab() {
+    if (this._extensionPopupTabWeak) {
+      const tab = this._extensionPopupTabWeak.get();
+
+      // Return the native tab only if the tab has not been removed in the meantime.
+      if (tab.browser) {
+        return tab;
+      }
+
+      // Clear the tracked popup tab if it has been closed in the meantime.
+      this._extensionPopupTabWeak = null;
+    }
+
+    return undefined;
+  }
+
+  /**
+   * Open a pageAction/browserAction popup url in a tab and keep track of
+   * its weak reference (to be able to customize the activedTab using the tab parentId,
+   * to skip it in the tabs.query and to set the parent tab as active when the popup
+   * tab is currently selected).
+   *
+   * @param {string} popup
+   *   The popup url to open in a tab.
+   */
+  openExtensionPopupTab(popup) {
+    let win = windowTracker.topWindow;
+    if (!win) {
+      throw new ExtensionError(`Unable to open a popup without an active window`);
+    }
+
+    if (this.extensionPopupTab) {
+      win.BrowserApp.closeTab(this.extensionPopupTab);
+    }
+
+    this.init();
+
+    this._extensionPopupTabWeak = Cu.getWeakReference(win.BrowserApp.addTab(popup, {
+      selected: true,
+      parentId: win.BrowserApp.selectedTab.id,
+    }));
   }
 
   getId(nativeTab) {
     return nativeTab.id;
   }
 
   getTab(id, default_ = undefined) {
     let win = windowTracker.topWindow;
@@ -283,30 +343,55 @@ class TabTracker extends TabTrackerBase 
    * events for them.
    *
    * @param {Event} event
    *        A DOM event to handle.
    * @private
    */
   handleEvent(event) {
     const {BrowserApp} = event.target.ownerGlobal;
-    let nativeTab = BrowserApp.getTabForBrowser(event.target);
+    const nativeTab = BrowserApp.getTabForBrowser(event.target);
 
     switch (event.type) {
       case "TabOpen":
         this.emitCreated(nativeTab);
         break;
 
       case "TabClose":
         this.emitRemoved(nativeTab, false);
         break;
     }
   }
 
   /**
+   * Required by the GlobalEventDispatcher module. This event will get
+   * called whenever one of the registered listeners fires.
+   * @param {string} event The event which fired.
+   * @param {object} data Information about the event which fired.
+   */
+  onEvent(event, data) {
+    const {BrowserApp} = windowTracker.topWindow;
+
+    switch (event) {
+      case "Tab:Selected": {
+        // If a new tab has been selected while an extension popup tab is still open,
+        // close it immediately.
+        const nativeTab = BrowserApp.getTabForId(data.id);
+
+        const popupTab = tabTracker.extensionPopupTab;
+        if (popupTab && popupTab !== nativeTab) {
+          BrowserApp.closeTab(popupTab);
+        }
+
+        break;
+      }
+    }
+  }
+
+  /**
    * Emits a "tab-created" event for the given tab element.
    *
    * @param {NativeTab} nativeTab
    *        The tab element which is being created.
    * @private
    */
   emitCreated(nativeTab) {
     this.emit("tab-created", {nativeTab});
@@ -321,16 +406,27 @@ class TabTracker extends TabTrackerBase 
    *        True if the tab is being removed because the browser window is
    *        closing.
    * @private
    */
   emitRemoved(nativeTab, isWindowClosing) {
     let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal);
     let tabId = this.getId(nativeTab);
 
+    if (this.extensionPopupTab && this.extensionPopupTab === nativeTab) {
+      this._extensionPopupTabWeak = null;
+
+      // Select the parent tab when the closed tab was an extension popup tab.
+      const {BrowserApp} = windowTracker.topWindow;
+      const popupParentTab = BrowserApp.getTabForId(nativeTab.parentId);
+      if (popupParentTab) {
+        BrowserApp.selectTab(popupParentTab);
+      }
+    }
+
     Services.tm.dispatchToMainThread(() => {
       this.emit("tab-removed", {nativeTab, tabId, windowId, isWindowClosing});
     });
   }
 
   getBrowserData(browser) {
     let result = {
       tabId: -1,
@@ -346,20 +442,29 @@ class TabTracker extends TabTrackerBase 
         result.tabId = this.getId(nativeTab);
       }
     }
 
     return result;
   }
 
   get activeTab() {
-    let window = windowTracker.topWindow;
-    if (window && window.BrowserApp) {
-      return window.BrowserApp.selectedTab;
+    let win = windowTracker.topWindow;
+    if (win && win.BrowserApp) {
+      const selectedTab = win.BrowserApp.selectedTab;
+
+      // If the current tab is an extension popup tab, we use the parentId to retrieve
+      // and return the tab that was selected when the popup tab has been opened.
+      if (selectedTab === this.extensionPopupTab) {
+        return win.BrowserApp.getTabForId(selectedTab.parentId);
+      }
+
+      return selectedTab;
     }
+
     return null;
   }
 }
 
 windowTracker = new WindowTracker();
 tabTracker = new TabTracker();
 
 Object.assign(global, {tabTracker, windowTracker});
@@ -401,16 +506,33 @@ class Tab extends TabBase {
     return this.nativeTab.lastTouchedAt;
   }
 
   get pinned() {
     return false;
   }
 
   get active() {
+    // If there is an extension popup tab and it is active,
+    // then the parent tab of the extension popup tab is active
+    // (while the extension popup tab will not be included in the
+    // tabs.query results).
+    if (tabTracker.extensionPopupTab) {
+      if (tabTracker.extensionPopupTab.getActive() &&
+          this.nativeTab.id === tabTracker.extensionPopupTab.parentId) {
+        return true;
+      }
+
+      // Never return true for an active extension popup, e.g. so that
+      // the popup tab will not be part of the results of querying
+      // all the active tabs.
+      if (tabTracker.extensionPopupTab === this.nativeTab) {
+        return false;
+      }
+    }
     return this.nativeTab.getActive();
   }
 
   get selected() {
     return this.nativeTab.getActive();
   }
 
   get status() {
--- a/mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_activeTab_permission.html
@@ -10,16 +10,17 @@
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 var {BrowserActions} = SpecialPowers.Cu.import("resource://gre/modules/BrowserActions.jsm", {});
 var {PageActions} = SpecialPowers.Cu.import("resource://gre/modules/PageActions.jsm", {});
+var {Services} = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {});
 
 function pageLoadedContentScript() {
   browser.test.sendMessage("page-loaded", window.location.href);
 }
 
 add_task(async function test_activeTab_pageAction() {
   async function background() {
     function contentScriptCode() {
@@ -197,12 +198,144 @@ add_task(async function test_activeTab_b
   info("Click the browserAction");
   BrowserActions.synthesizeClick(uuid);
 
   await extension.awaitFinish("browser_action.activeTab.done");
 
   await extension.unload();
 });
 
+add_task(async function test_activeTab_pageAction_popup() {
+  async function background() {
+    await browser.tabs.create({url: "http://example.com#test_activeTab_pageAction_popup"});
+    const tabs = await browser.tabs.query({active: true});
+    await browser.pageAction.show(tabs[0].id);
+
+    browser.test.log(`pageAction shown on tab ${tabs[0].id}`);
+
+    browser.test.sendMessage("background_page.ready", {activeTabId: tabs[0].id});
+  }
+
+  async function popupScript() {
+    function contentScriptCode() {
+      browser.test.log("content script executed");
+
+      return "tabs.executeScript result";
+    }
+
+    const tabs = await browser.tabs.query({active: true});
+    const tab = tabs[0];
+
+    browser.test.log(`extension popup tab opened loaded for activeTab ${tab.id}`);
+
+    browser.test.sendMessage("extension_popup.activeTab", tab.id);
+
+    const [result] = await browser.tabs.executeScript(tab.id, {
+      code: `(${contentScriptCode})()`,
+    }).catch(error => {
+      // Make the test to fail fast if something goes wrong.
+      browser.test.fail(`Unexpected exception on tabs.executeScript: ${error}`);
+      browser.test.notifyFail("page_action_popup.activeTab.done");
+      throw error;
+    });
+
+    browser.test.assertEq("tabs.executeScript result", result,
+                          "Got the expected result from tabs.executeScript");
+
+    browser.test.notifyPass("page_action_popup.activeTab.done");
+  }
+
+  let popupHtml = `<!DOCTYPE html>
+    <html>
+      <head>
+        <meta charset="utf-8">
+      </head>
+      <body>
+        <h1>Extension Popup</h1>
+        <script src="popup.js"><\/script>
+      </body>
+    </html>
+  `;
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background,
+    manifest: {
+      "name": "PageAction Extension",
+      "page_action": {
+        "default_title": "Page Action",
+        "default_icon": {
+          "18": "extension.png",
+        },
+        "default_popup": "popup.html",
+      },
+      "content_scripts": [
+        {
+          "js": ["page_loaded.js"],
+          "matches": ["http://example.com/*"],
+          "run_at": "document_end",
+        },
+      ],
+      "permissions": ["activeTab"],
+    },
+    files: {
+      "extension.png": TEST_ICON_ARRAYBUFFER,
+      "page_loaded.js": pageLoadedContentScript,
+      "popup.html": popupHtml,
+      "popup.js": popupScript,
+    },
+  });
+
+  await extension.startup();
+
+  const {activeTabId} = await extension.awaitMessage("background_page.ready");
+
+  const uuid = `{${extension.uuid}}`;
+
+  ok(PageActions.isShown(uuid), "page action is shown");
+
+  info("Wait the new tab to be loaded");
+  const loadedURL = await extension.awaitMessage("page-loaded");
+
+  is(loadedURL, "http://example.com/#test_activeTab_pageAction_popup",
+     "The expected URL has been loaded in a new tab");
+
+  PageActions.synthesizeClick(uuid);
+
+  const popupActiveTabId = await extension.awaitMessage("extension_popup.activeTab");
+
+  // Check that while the extension popup tab is selected the active tab is still the tab
+  // from which the user has opened the extension popup.
+  is(popupActiveTabId, activeTabId,
+     "Got the expected tabId while the extension popup tab was selected");
+
+  await extension.awaitFinish("page_action_popup.activeTab.done");
+
+  const chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+  const BrowserApp = chromeWin.BrowserApp;
+
+  const popupTab = BrowserApp.selectedTab;
+  const popupTabId = popupTab.id;
+
+  let onceTabClosed = new Promise(resolve => {
+    BrowserApp.deck.addEventListener("TabClose", resolve, {once: true});
+  });
+
+  // Switch to the parent tab of the popup tab.
+  // (which should make the extension popup tab to be closed automatically)
+  BrowserApp.selectTab(BrowserApp.getTabForId(popupTab.parentId));
+
+  info("Wait for the extension popup tab to be closed once the parent tab has been selected");
+
+  await onceTabClosed;
+
+  is(BrowserApp.getTabForId(popupTabId), null,
+     "The extension popup tab should have been closed");
+
+  // Close the tab that opened the extension popup before exiting the test.
+  BrowserApp.closeTab(BrowserApp.selectedTab);
+
+  await extension.unload();
+});
+
 </script>
 
 </body>
 </html>