зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
87980a0867
Коммит
271cd169eb
|
@ -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() {
|
||||
|
|
Загрузка…
Ссылка в новой задаче