Bug 1565782 - Implement browser.tabs.remove for GeckoView webextensions APIs r=agi,robwu,rpl,geckoview-reviewers,snorp

Differential Revision: https://phabricator.services.mozilla.com/D38216

--HG--
rename : mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs/background.js => mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs-create/background.js
rename : mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs/manifest.json => mobile/android/geckoview/src/androidTest/assets/web_extensions/tabs-create/manifest.json
extra : moz-landing-system : lando
This commit is contained in:
chrmod 2019-07-26 20:26:47 +00:00
Родитель 87980a0867
Коммит 271cd169eb
16 изменённых файлов: 279 добавлений и 40 удалений

Просмотреть файл

@ -8,6 +8,12 @@ ChromeUtils.defineModuleGetter(
"resource://gre/modules/PromiseUtils.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"GeckoViewTabBridge",
"resource://gre/modules/GeckoViewTab.jsm"
);
const getBrowserWindow = window => {
return window.docShell.rootTreeItem.domWindow;
};
@ -339,7 +345,8 @@ this.tabs = class extends ExtensionAPI {
nativeTab = BrowserApp.addTab(url, options);
} else {
options.extensionId = context.extension.id;
nativeTab = await BrowserApp.createNewTab(url, options);
options.url = url;
nativeTab = await GeckoViewTabBridge.createNewTab(options);
}
if (createProperties.url) {
@ -354,6 +361,27 @@ this.tabs = class extends ExtensionAPI {
tabs = [tabs];
}
if (!Services.androidBridge.isFennec) {
await Promise.all(
tabs.map(async tabId => {
const windowId = GeckoViewTabBridge.tabIdToWindowId(tabId);
const window = windowTracker.getWindow(
windowId,
context,
false
);
if (!window) {
throw new ExtensionError(`Invalid tab ID ${tabId}`);
}
await GeckoViewTabBridge.closeTab({
window,
extensionId: context.extension.id,
});
})
);
return;
}
for (let tabId of tabs) {
let nativeTab = tabTracker.getTab(tabId);
nativeTab.browser.ownerGlobal.BrowserApp.closeTab(nativeTab);

Просмотреть файл

@ -1115,6 +1115,7 @@ package org.mozilla.geckoview {
}
public static interface WebExtensionController.TabDelegate {
method @UiThread @NonNull default public GeckoResult<AllowOrDeny> onCloseTab(@Nullable WebExtension, @NonNull GeckoSession);
method @UiThread @Nullable default public GeckoResult<GeckoSession> onNewTab(@Nullable WebExtension, @Nullable String);
}

Просмотреть файл

@ -0,0 +1,3 @@
browser.tabs.create({}).then(tab => {
browser.tabs.remove(tab.id);
});

Просмотреть файл

@ -0,0 +1,11 @@
{
"manifest_version": 2,
"name": "messaging",
"version": "1.0",
"description": "Creates and removes a tab.",
"background": {
"scripts": ["background.js"]
},
"permissions": [
]
}

Просмотреть файл

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "messaging",
"version": "1.0",
"description": "Test browser tabs api",
"description": "Creates a tab.",
"background": {
"scripts": ["background.js"]
},

Просмотреть файл

@ -0,0 +1,3 @@
browser.tabs.query({ url: "*://*/*?tabToClose" }).then(([tab]) => {
browser.tabs.remove(tab.id);
});

Просмотреть файл

@ -0,0 +1,12 @@
{
"manifest_version": 2,
"name": "messaging",
"version": "1.0",
"description": "Removes an existing tab.",
"background": {
"scripts": ["background.js"]
},
"permissions": [
"tabs"
]
}

Просмотреть файл

@ -195,6 +195,11 @@ public class TestRunnerActivity extends Activity {
public GeckoResult<GeckoSession> onNewTab(WebExtension source, String uri) {
return GeckoResult.fromValue(createSession());
}
@Override
public GeckoResult<AllowOrDeny> onCloseTab(WebExtension source, GeckoSession session) {
closeSession(session);
return GeckoResult.ALLOW;
}
});
sRuntime.setDelegate(() -> {
mKillProcessOnDestroy = true;

Просмотреть файл

@ -29,7 +29,9 @@ import java.util.UUID
class WebExtensionTest : BaseSessionTest() {
companion object {
val TEST_ENDPOINT: String = "http://localhost:4243"
val TABS_BACKGROUND: String = "resource://android/assets/web_extensions/tabs/"
val TABS_CREATE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-create/"
val TABS_CREATE_REMOVE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-create-remove/"
val TABS_REMOVE_BACKGROUND: String = "resource://android/assets/web_extensions/tabs-remove/"
val MESSAGING_BACKGROUND: String = "resource://android/assets/web_extensions/messaging/"
val MESSAGING_CONTENT: String = "resource://android/assets/web_extensions/messaging-content/"
}
@ -120,6 +122,8 @@ class WebExtensionTest : BaseSessionTest() {
// This test
// - Listen for a new tab request from a web extension
// - Registers a web extension
// - Waits for onNewTab request
// - Verify that request came from right extension
@Test
fun testBrowserTabsCreate() {
val tabsCreateResult = GeckoResult<Void>()
@ -134,7 +138,7 @@ class WebExtensionTest : BaseSessionTest() {
}
}
sessionRule.runtime.webExtensionController.tabDelegate = tabDelegate
tabsExtension = WebExtension(TABS_BACKGROUND)
tabsExtension = WebExtension(TABS_CREATE_BACKGROUND)
sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(tabsExtension))
sessionRule.waitForResult(tabsCreateResult)
@ -142,6 +146,77 @@ class WebExtensionTest : BaseSessionTest() {
sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(tabsExtension))
}
// This test
// - Create and assign WebExtension TabDelegate to handle creation and closing of tabs
// - Registers a WebExtension
// - Extension requests creation of new tab
// - TabDelegate handles creation of new tab
// - Extension requests removal of newly created tab
// - TabDelegate handles closing of newly created tab
// - Verify that close request came from right extension and targeted session
@Test
fun testBrowserTabsCreateBrowserTabsRemove() {
val onCloseRequestResult = GeckoResult<Void>()
var tabsExtension : WebExtension? = null
var extensionCreatedSession : GeckoSession? = null
val tabDelegate = object : WebExtensionController.TabDelegate {
override fun onNewTab(source: WebExtension?, uri: String?): GeckoResult<GeckoSession> {
extensionCreatedSession = GeckoSession(sessionRule.session.settings)
return GeckoResult.fromValue(extensionCreatedSession)
}
override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult<AllowOrDeny> {
Assert.assertEquals(tabsExtension, source)
Assert.assertNotEquals(null, extensionCreatedSession)
Assert.assertEquals(extensionCreatedSession, session)
onCloseRequestResult.complete(null)
return GeckoResult.ALLOW;
}
}
sessionRule.runtime.webExtensionController.tabDelegate = tabDelegate
tabsExtension = WebExtension(TABS_CREATE_REMOVE_BACKGROUND)
sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(tabsExtension))
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
fun testBrowserTabsRemove() {
val onCloseRequestResult = GeckoResult<Void>()
val existingSession = sessionRule.createOpenSession()
val tabDelegate = object : WebExtensionController.TabDelegate {
override fun onCloseTab(source: WebExtension?, session: GeckoSession): GeckoResult<AllowOrDeny> {
Assert.assertEquals(existingSession, session)
onCloseRequestResult.complete(null)
return GeckoResult.ALLOW;
}
}
existingSession.loadTestPath("$HELLO_HTML_PATH?tabToClose")
existingSession.waitForPageStop()
sessionRule.runtime.webExtensionController.tabDelegate = tabDelegate
val tabsExtension = WebExtension(TABS_REMOVE_BACKGROUND)
sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(tabsExtension))
sessionRule.waitForResult(onCloseRequestResult)
sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(tabsExtension))
}
private fun createWebExtension(background: Boolean,
messageDelegate: WebExtension.MessageDelegate): WebExtension {
val webExtension: WebExtension

Просмотреть файл

@ -159,9 +159,9 @@ public final class GeckoRuntime implements Parcelable {
private GeckoRuntimeSettings mSettings;
private Delegate mDelegate;
private RuntimeTelemetry mTelemetry;
private WebExtensionEventDispatcher mWebExtensionDispatcher;
private final WebExtensionEventDispatcher mWebExtensionDispatcher;
private StorageController mStorageController;
private WebExtensionController mWebExtensionController;
private final WebExtensionController mWebExtensionController;
public GeckoRuntime() {
mWebExtensionDispatcher = new WebExtensionEventDispatcher();
@ -427,7 +427,7 @@ public final class GeckoRuntime implements Parcelable {
return result;
}
/* protected */ WebExtensionEventDispatcher getWebExtensionDispatcher() {
/* protected */ @NonNull WebExtensionEventDispatcher getWebExtensionDispatcher() {
return mWebExtensionDispatcher;
}

Просмотреть файл

@ -358,6 +358,7 @@ public class GeckoSession implements Parcelable {
"GeckoView:WebExtension:Message",
"GeckoView:WebExtension:PortMessage",
"GeckoView:WebExtension:Connect",
"GeckoView:WebExtension:CloseTab",
null);
}
@ -375,7 +376,7 @@ public class GeckoSession implements Parcelable {
@Override
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
if (mWindow == null || mWindow.runtime.getWebExtensionDispatcher() == null) {
if (mWindow == null) {
return;
}
@ -384,6 +385,8 @@ public class GeckoSession implements Parcelable {
|| "GeckoView:WebExtension:Connect".equals(event)) {
mWindow.runtime.getWebExtensionDispatcher()
.handleMessage(event, message, callback, GeckoSession.this);
} else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
mWindow.runtime.getWebExtensionController().closeTab(message, callback, GeckoSession.this);
}
}
}

Просмотреть файл

@ -1,5 +1,6 @@
package org.mozilla.geckoview;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
@ -27,6 +28,20 @@ public class WebExtensionController {
default GeckoResult<GeckoSession> onNewTab(@Nullable WebExtension source, @Nullable String uri) {
return null;
}
/**
* Called when tabs.remove is invoked, this method decides if WebExtension can close the
* tab. In case WebExtension can close the tab, it should close passed GeckoSession and
* return GeckoResult.ALLOW or GeckoResult.DENY in case tab cannot be closed.
*
* @param source An instance of {@link WebExtension} or null if extension was not registered
* with GeckoRuntime.registerWebextension
* @param session An instance of {@link GeckoSession} to be closed.
*/
@UiThread
@NonNull
default GeckoResult<AllowOrDeny> onCloseTab(@Nullable WebExtension source, @NonNull GeckoSession session) {
return GeckoResult.DENY;
}
}
private GeckoRuntime mRuntime;
@ -106,4 +121,23 @@ public class WebExtensionController {
callback.sendSuccess(session.getId());
});
}
/* package */ void closeTab(final GeckoBundle message, final EventCallback callback, final GeckoSession session) {
if (mTabDelegate == null) {
callback.sendError(null);
return;
}
WebExtension extension = mRuntime.getWebExtensionDispatcher().getWebExtension(message.getString("extensionId"));
GeckoResult<AllowOrDeny> result = mTabDelegate.onCloseTab(extension, session);
result.accept(value -> {
if (value == AllowOrDeny.ALLOW) {
callback.sendSuccess(null);
} else {
callback.sendError(null);
}
});
}
}

Просмотреть файл

@ -52,6 +52,11 @@ exclude: true
- Created `onKill` to `ContentDelegate` to differentiate from crashes.
- Added `onCloseTab` to `WebExtensionController.TabDelegate` to handle
[`browser.tabs.remove`][69.8] calls by WebExtensions.
[69.8]: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/remove
## v68
- Added [`GeckoRuntime#configurationChanged`][68.1] to notify the device
configuration has changed.
@ -363,4 +368,4 @@ exclude: true
[65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: ../GeckoResult.html
[api-version]: d770e67f7e5b87640574810468c76208ce4c1a43
[api-version]: b51a187d4c36d7d0f4091d9d1227a553a4e08edb

Просмотреть файл

@ -163,6 +163,12 @@ public class GeckoViewActivity extends AppCompatActivity {
mToolbarView.updateTabCount();
return GeckoResult.fromValue(newSession);
}
@Override
public GeckoResult<AllowOrDeny> onCloseTab(WebExtension source, GeckoSession session) {
TabSession tabSession = mTabSessionManager.getSession(session);
closeTab(tabSession);
return GeckoResult.ALLOW;
}
});
}

Просмотреть файл

@ -4,7 +4,7 @@
"use strict";
var EXPORTED_SYMBOLS = ["GeckoViewTab"];
var EXPORTED_SYMBOLS = ["GeckoViewTab", "GeckoViewTabBridge"];
const { GeckoViewModule } = ChromeUtils.import(
"resource://gre/modules/GeckoViewModule.jsm"
@ -33,18 +33,14 @@ class Tab {
// Stub BrowserApp implementation for WebExtensions support.
class BrowserAppShim {
constructor(window) {
// Because of bug 1410749, we can't use 0, though, and just to be safe
// we choose a value that is unlikely to overlap with Fennec's tab IDs.
const tabId = 10000 + window.windowUtils.outerWindowID;
const tabId = GeckoViewTabBridge.windowIdToTabId(
window.windowUtils.outerWindowID
);
this.selectedBrowser = window.browser;
this.selectedTab = new Tab(tabId, this.selectedBrowser);
this.tabs = [this.selectedTab];
}
closeTab(aTab) {
// not implemented
}
getTabForId(aId) {
return this.selectedTab;
}
@ -69,11 +65,61 @@ class BrowserAppShim {
return this.selectedBrowser;
}
// ext-tabs calls tabListener.initTabReady(); which rely on deck when initializing ProgressListeners.
// Deck will be removed by https://phabricator.services.mozilla.com/D36575.
get deck() {
return {
addEventListener() {},
removeEventListener() {},
};
}
static getBrowserApp(window) {
let { BrowserApp } = window;
if (!BrowserApp) {
BrowserApp = window.gBrowser = window.BrowserApp = new BrowserAppShim(
window
);
}
return BrowserApp;
}
}
// Because of bug 1410749, we can't use 0, though, and just to be safe
// we choose a value that is unlikely to overlap with Fennec's tab IDs.
const TAB_ID_BASE = 10000;
const GeckoViewTabBridge = {
/**
* Converts windowId to tabId as in GeckoView every browser window has exactly one tab.
*
* @param {windowId} number outerWindowId
*
* @returns {number} tabId
*/
windowIdToTabId(windowId) {
return TAB_ID_BASE + windowId;
},
/**
* Converts tabId to windowId.
*
* @param {windowId} number
*
* @returns {number}
* outerWindowId of browser window to which the tab belongs.
*/
tabIdToWindowId(tabId) {
return tabId - TAB_ID_BASE;
},
/**
* Request the GeckoView App to create a new tab (GeckoSession).
*
* @param {string} url The url to load in the newly created tab
* @param {object} options
* @param {string} options.url The url to load in the newly created tab
* @param {nsIPrincipal} options.triggeringPrincipal
* @param {boolean} [options.disallowInheritPrincipal]
* @param {string} options.extensionId
@ -83,8 +129,8 @@ class BrowserAppShim {
* @throws {Error}
* Throws an error if the GeckoView app doesn't support tabs.create or fails to handle the request.
*/
async createNewTab(url, options) {
url = url || "about:blank";
async createNewTab(options = {}) {
const url = options.url || "about:blank";
if (!options.extensionId) {
throw new Error("options.extensionId missing");
@ -133,29 +179,36 @@ class BrowserAppShim {
});
return BrowserAppShim.getBrowserApp(window).selectedTab;
}
},
// ext-tabs calls tabListener.initTabReady(); which rely on deck when initializing ProgressListeners.
// Deck will be removed by https://phabricator.services.mozilla.com/D36575.
get deck() {
return {
addEventListener() {},
removeEventListener() {},
};
}
static getBrowserApp(window) {
let { BrowserApp } = window;
if (!BrowserApp) {
BrowserApp = window.gBrowser = window.BrowserApp = new BrowserAppShim(
window
);
/**
* Request the GeckoView App to close a tab (GeckoSession).
*
*
* @param {object} options
* @param {Window} options.window The window owning the tab to close
* @param {string} options.extensionId
*
* @returns {Promise<Tab>}
* A promise resolved after GeckoSession is closed.
* @throws {Error}
* Throws an error if the GeckoView app doesn't allow extension to close tab.
*/
async closeTab({ window, extensionId } = {}) {
if (!extensionId) {
throw new Error("extensionId is required");
}
return BrowserApp;
}
}
if (!window) {
throw new Error("window is required");
}
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:CloseTab",
extensionId: extensionId,
});
},
};
class GeckoViewTab extends GeckoViewModule {
onInit() {