Bug 1597793 - Support tabs.onActivated in GeckoView r=snorp,agi
☠☠ backed out by b1f2823f65c2 ☠ ☠
authorChristoph Walcher <christoph-wa@gmx.de>
Sat, 25 Jan 2020 04:13:30 +0000
changeset 511774 0e9c04f18e3cd886a228c367f1b11d559fd8bc11
parent 511773 b830ae752c939716001c79a614132f2a991ec1d6
child 511775 4de3746c478ebf64a986f7c9ed8b9f91d0718953
push id106022
push userasferro@mozilla.com
push dateSat, 25 Jan 2020 04:15:17 +0000
treeherderautoland@d8132aa16234 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssnorp, agi
bugs1597793
milestone74.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 1597793 - Support tabs.onActivated in GeckoView r=snorp,agi Differential Revision: https://phabricator.services.mozilla.com/D60041
mobile/android/components/extensions/ext-tabs.js
mobile/android/components/extensions/ext-utils.js
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs-activate-remove/background.js
mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs-activate-remove/manifest.json
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ExtensionActionTest.kt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
mobile/android/modules/geckoview/GeckoViewTab.jsm
--- a/mobile/android/components/extensions/ext-tabs.js
+++ b/mobile/android/components/extensions/ext-tabs.js
@@ -109,30 +109,34 @@ this.tabs = class extends ExtensionAPI {
 
       await tabListener.awaitTabReady(tab.nativeTab);
 
       return tab;
     }
 
     let self = {
       tabs: {
-        onActivated: makeGlobalEvent(
+        onActivated: new EventManager({
           context,
-          "tabs.onActivated",
-          "Tab:Selected",
-          (fire, data) => {
-            let tab = tabManager.get(data.id);
+          name: "tabs.onActivated",
+          register: fire => {
+            let listener = (eventName, event) => {
+              fire.async({
+                windowId: event.windowId,
+                tabId: event.tabId,
+                // In GeckoView each window has only one tab, so previousTabId is omitted.
+              });
+            };
 
-            fire.async({
-              tabId: tab.id,
-              previousTabId: data.previousTabId,
-              windowId: tab.windowId,
-            });
-          }
-        ),
+            windowTracker.on("tab-activated", listener);
+            return () => {
+              windowTracker.off("tab-activated", listener);
+            };
+          },
+        }).api(),
 
         onCreated: new EventManager({
           context,
           name: "tabs.onCreated",
           register: fire => {
             let listener = (eventName, event) => {
               fire.async(tabManager.convert(event.nativeTab));
             };
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -120,19 +120,44 @@ class ProgressListenerWrapper {
   }
 }
 
 class WindowTracker extends WindowTrackerBase {
   constructor(...args) {
     super(...args);
 
     this.progressListeners = new DefaultWeakMap(() => new WeakMap());
+
+    const self = this;
+    this._topWindowRef = null;
+    const listener = {
+      onEvent(event, data, callback) {
+        const { sessionId, active } = data;
+        const window = Services.ww.getWindowByName(sessionId, null);
+        const windowId = window.windowUtils.outerWindowID;
+        const tab = window.BrowserApp.selectedTab;
+        tab.active = active;
+        if (active) {
+          self._topWindowRef = Cu.getWeakReference(window);
+          self.emit("tab-activated", {
+            windowId,
+            tabId: tab.id,
+          });
+        }
+      },
+    };
+    GlobalEventDispatcher.registerListener(listener, [
+      "GeckoView:WebExtension:SetTabActive",
+    ]);
   }
 
   get topWindow() {
+    if (this._topWindowRef) {
+      return this._topWindowRef.get();
+    }
     return Services.wm.getMostRecentWindow(WINDOW_TYPE);
   }
 
   get topNonPBWindow() {
     return Services.wm.getMostRecentNonPBWindow(WINDOW_TYPE);
   }
 
   isBrowserWindow(window) {
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -1593,16 +1593,17 @@ package org.mozilla.geckoview {
   public class WebExtensionController {
     method @AnyThread @NonNull public GeckoResult<WebExtension> disable(@NonNull WebExtension, int);
     method @AnyThread @NonNull public GeckoResult<WebExtension> enable(@NonNull WebExtension, int);
     method @UiThread @Nullable public WebExtensionController.PromptDelegate getPromptDelegate();
     method @UiThread @Nullable public WebExtensionController.TabDelegate getTabDelegate();
     method @NonNull @AnyThread public GeckoResult<WebExtension> install(@NonNull String);
     method @AnyThread @NonNull public GeckoResult<List<WebExtension>> list();
     method @UiThread public void setPromptDelegate(@Nullable WebExtensionController.PromptDelegate);
+    method @AnyThread public void setTabActive(@NonNull GeckoSession, boolean);
     method @UiThread public void setTabDelegate(@Nullable WebExtensionController.TabDelegate);
     method @NonNull @AnyThread public GeckoResult<Void> uninstall(@NonNull WebExtension);
   }
 
   public static class WebExtensionController.EnableSource {
     ctor public EnableSource();
     field public static final int APP = 2;
     field public static final int USER = 1;
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs-activate-remove/background.js
@@ -0,0 +1,6 @@
+browser.tabs.onActivated.addListener(async tabChange => {
+  const activeTabs = await browser.tabs.query({ active: true });
+  if (activeTabs.length === 1 && activeTabs[0].id == tabChange.tabId) {
+    browser.tabs.remove(tabChange.tabId);
+  }
+});
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs-activate-remove/manifest.json
@@ -0,0 +1,12 @@
+{
+  "manifest_version": 2,
+  "name": "messaging",
+  "version": "1.0",
+  "description": "Removes the activated Tab.",
+  "background": {
+    "scripts": ["background.js"]
+  },
+  "permissions": [
+    "tabs"
+  ]
+}
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ExtensionActionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/ExtensionActionTest.kt
@@ -35,16 +35,18 @@ class ExtensionActionTest : BaseSessionT
                 arrayOf("#pageAction"),
                 arrayOf("#browserAction"))
     }
 
     @field:Parameterized.Parameter(0) @JvmField var id: String = ""
 
     @Before
     fun setup() {
+        sessionRule.runtime.webExtensionController.setTabActive(mainSession, true)
+
         // This method installs the extension, opens up ports with the background script and the
         // content script and captures the default action definition from the manifest
         val browserActionDefaultResult = GeckoResult<WebExtension.Action>()
         val pageActionDefaultResult = GeckoResult<WebExtension.Action>()
 
         val windowPortResult = GeckoResult<WebExtension.Port>()
         val backgroundPortResult = GeckoResult<WebExtension.Port>()
 
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExtensionTest.kt
@@ -29,16 +29,18 @@ import java.util.UUID
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class WebExtensionTest : BaseSessionTest() {
     companion object {
         private const val TABS_CREATE_BACKGROUND: String =
                 "resource://android/assets/web_extensions/tabs-create/"
         private const val TABS_CREATE_REMOVE_BACKGROUND: String =
                 "resource://android/assets/web_extensions/tabs-create-remove/"
+        private const val TABS_ACTIVATE_REMOVE_BACKGROUND: String =
+                "resource://android/assets/web_extensions/tabs-activate-remove/"
         private const val TABS_REMOVE_BACKGROUND: String =
                 "resource://android/assets/web_extensions/tabs-remove/"
         private const val MESSAGING_BACKGROUND: String =
                 "resource://android/assets/web_extensions/messaging/"
         private const val MESSAGING_CONTENT: String =
                 "resource://android/assets/web_extensions/messaging-content/"
     }
 
@@ -49,16 +51,17 @@ class WebExtensionTest : BaseSessionTest
     fun setup() {
         sessionRule.addExternalDelegateUntilTestEnd(
                 WebExtensionController.PromptDelegate::class,
                 controller::setPromptDelegate,
                 { controller.promptDelegate = null },
                 object : WebExtensionController.PromptDelegate {}
         )
         sessionRule.setPrefsUntilTestEnd(mapOf("extensions.isembedded" to true))
+        sessionRule.runtime.webExtensionController.setTabActive(mainSession, true)
     }
 
     @Test
     fun registerWebExtension() {
         mainSession.loadUri("example.com")
         sessionRule.waitForPageStop()
 
         // First let's check that the color of the border is empty before loading
@@ -446,16 +449,61 @@ class WebExtensionTest : BaseSessionTest
 
         sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(tabsExtension))
         sessionRule.waitForResult(onCloseRequestResult)
 
         sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(tabsExtension))
     }
 
     // This test
+    // - Create and assign WebExtension TabDelegate to handle creation and closing of tabs
+    // - Create and opens a new GeckoSession
+    // - Set the main session as active tab
+    // - Registers a WebExtension
+    // - Extension listens for activated tab changes
+    // - Set the main session as inactive tab
+    // - Set the newly created GeckoSession as active tab
+    // - Extension requests removal of newly created tab if tabs.query({active: true})
+    //     contains only the newly activated tab
+    // - TabDelegate handles closing of newly created tab
+    // - Verify that close request came from right extension and targeted session
+    @Test
+    fun testBrowserTabsActivateBrowserTabsRemove() {
+        val onCloseRequestResult = GeckoResult<Void>()
+        var tabsExtension : WebExtension? = null
+        val newTabSession = GeckoSession(sessionRule.session.settings)
+
+        newTabSession.open(sessionRule.runtime)
+
+        sessionRule.addExternalDelegateUntilTestEnd(
+                WebExtensionController.TabDelegate::class,
+                controller::setTabDelegate,
+                { controller.tabDelegate = null },
+                object : WebExtensionController.TabDelegate {
+
+            override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult<AllowOrDeny> {
+                assertEquals(tabsExtension, source)
+                assertEquals(newTabSession, session)
+                onCloseRequestResult.complete(null)
+                return GeckoResult.ALLOW
+            }
+        })
+
+        tabsExtension = WebExtension(TABS_ACTIVATE_REMOVE_BACKGROUND, controller)
+
+        sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(tabsExtension))
+
+        controller.setTabActive(sessionRule.session, false)
+        controller.setTabActive(newTabSession, true)
+
+        sessionRule.waitForResult(onCloseRequestResult)
+        sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(tabsExtension))
+    }
+
+    // This test
     // - Create and assign WebExtension TabDelegate to handle closing of tabs
     // - Create new GeckoSession for WebExtension to close
     // - Load url that will allow extension to identify the tab
     // - Registers a WebExtension
     // - Extension finds the tab by url and removes it
     // - TabDelegate handles closing of the tab
     // - Verify that request targets previously created GeckoSession
     @Test
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebExtensionController.java
@@ -711,16 +711,29 @@ public class WebExtensionController {
             if (value == AllowOrDeny.ALLOW) {
                 callback.sendSuccess(null);
             } else {
                 callback.sendError(null);
             }
         });
     }
 
+    /**
+     * Notifies extensions about a active tab change over the `tabs.onActivated` event.
+     *
+     * @param session The {@link GeckoSession} of the newly selected session/tab.
+     * @param active true if the tab became active, false if the tab became inactive.
+     */
+    @AnyThread
+    public void setTabActive(@NonNull final GeckoSession session, final boolean active) {
+        final GeckoBundle bundle = new GeckoBundle(1);
+        bundle.putString("sessionId", session.getId());
+        bundle.putBoolean("active", active);
+        EventDispatcher.getInstance().dispatch("GeckoView:WebExtension:SetTabActive", bundle);
+    }
 
     /* package */ void unregisterWebExtension(final WebExtension webExtension) {
         mExtensions.remove(webExtension.id);
         webExtension.setDelegateController(null);
 
         // Some ports may still be open so we need to go through the list and close all of the
         // ports tied to this web extension
         Iterator<Map.Entry<Long, WebExtension.Port>> it = mPorts.entrySet().iterator();
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -32,29 +32,33 @@ exclude: true
 - ⚠️ Updated [`ContentBlockingController`][74.7] to use new representation for content blocking
   exceptions and to add better support for removing exceptions. This deprecates [`ExceptionList`][74.8]
   and [`restoreExceptionList`][74.9] with the intent to remove them in 76.
   ([bug 1587552]({{bugzilla}}1587552))
 - Added [`GeckoSession.ContentDelegate.onMetaViewportFitChange`][74.10]. This exposes `viewport-fit` value that is CSS Round Display Level 1. ([bug 1574307]({{bugzilla}}1574307))
 - Extended [`LoginStorage.Delegate`][74.11] with [`onLoginUsed`][74.12] to
   report when existing login entries are used for autofill.
   ([bug 1610353]({{bugzilla}}1610353))
+- Added ['WebExtensionController#setTabActive'][74.13], which is used to notify extensions about
+  tab changes
+  ([bug 1597793]({{bugzilla}}1597793))
 
 [74.1]: {{javadoc_uri}}/WebExtensionController.html#enable-org.mozilla.geckoview.WebExtension-int-
 [74.2]: {{javadoc_uri}}/WebExtensionController.html#disable-org.mozilla.geckoview.WebExtension-int-
 [74.3]: {{javadoc_uri}}/GeckoSession.ProgressDelegate.SecurityInformation.html#certificate
 [74.4]: {{javadoc_uri}}/WebResponse.html#isSecure
 [74.5]: {{javadoc_uri}}/WebResponse.html#certificate
 [74.6]: {{javadoc_uri}}/WebRequestError.html#certificate
 [74.7]: {{javadoc_uri}}/ContentBlockingController.html
 [74.8]: {{javadoc_uri}}/ContentBlockingController.ExceptionList.html
 [74.9]: {{javadoc_uri}}/ContentBlockingController.html#restoreExceptionList-org.mozilla.geckoview.ContentBlockingController.ExceptionList-
 [74.10]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onMetaViewportFitChange-org.mozilla.geckoview.GeckoSession-java.lang.String-
 [74.11]: {{javadoc_uri}}/LoginStorage.Delegate.html
 [74.12]: {{javadoc_uri}}/LoginStorage.Delegate.html#onLoginUsed-org.mozilla.geckoview.LoginStorage.LoginEntry-int-
+[74.13]: {{javadoc_uri}}/WebExtensionController.html#setTabActive
 
 ## v73
 - Added [`WebExtensionController.install`][73.1] and [`uninstall`][73.2] to
   manage installed extensions
 - ⚠️ Renamed `ScreenLength.VIEWPORT_WIDTH`, `ScreenLength.VIEWPORT_HEIGHT`,
   `ScreenLength.fromViewportWidth` and `ScreenLength.fromViewportHeight` to
   [`ScreenLength.VISUAL_VIEWPORT_WIDTH`][73.3],
   [`ScreenLength.VISUAL_VIEWPORT_HEIGHT`][73.4],
@@ -555,9 +559,9 @@ exclude: true
 [65.19]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest.html#isRedirect
 [65.20]: {{javadoc_uri}}/GeckoSession.html#LOAD_FLAGS_BYPASS_CLASSIFIER
 [65.21]: {{javadoc_uri}}/GeckoSession.ContentDelegate.ContextElement.html
 [65.22]: {{javadoc_uri}}/GeckoSession.ContentDelegate.html#onContextMenu-org.mozilla.geckoview.GeckoSession-int-int-org.mozilla.geckoview.GeckoSession.ContentDelegate.ContextElement-
 [65.23]: {{javadoc_uri}}/GeckoSession.FinderResult.html
 [65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: {{javadoc_uri}}/GeckoResult.html
 
-[api-version]: aad1f21bb13244e937db3391643efe7a5fd46366
+[api-version]: 3557370c2de982c70c9de1336a1606053fa427d5
--- a/mobile/android/modules/geckoview/GeckoViewTab.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewTab.jsm
@@ -18,20 +18,21 @@ XPCOMUtils.defineLazyModuleGetters(this,
   Services: "resource://gre/modules/Services.jsm",
 });
 
 // Based on the "Tab" prototype from mobile/android/chrome/content/browser.js
 class Tab {
   constructor(id, browser) {
     this.id = id;
     this.browser = browser;
+    this.active = false;
   }
 
   getActive() {
-    return this.browser.docShellIsActive;
+    return this.active;
   }
 }
 
 // Stub BrowserApp implementation for WebExtensions support.
 class BrowserAppShim {
   constructor(window) {
     const tabId = GeckoViewTabBridge.windowIdToTabId(
       window.windowUtils.outerWindowID