Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs";
import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
const { ExtensionError } = ExtensionUtils;
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs",
});
class Tab {
constructor(window) {
this.id = GeckoViewTabBridge.windowIdToTabId(window.docShell.outerWindowID);
this.browser = window.browser;
this.active = false;
}
get linkedBrowser() {
return this.browser;
}
getActive() {
return this.active;
}
get userContextId() {
return this.browser.ownerGlobal.moduleManager.settings
.unsafeSessionContextId;
}
}
// 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;
export const GeckoViewTabBridge = {
/**
* Converts windowId to tabId as in GeckoView every browser window has exactly one tab.
*
* @param {number} windowId outerWindowId
*
* @returns {number} tabId
*/
windowIdToTabId(windowId) {
return TAB_ID_BASE + windowId;
},
/**
* Converts tabId to windowId.
*
* @param {number} tabId
*
* @returns {number}
* outerWindowId of browser window to which the tab belongs.
*/
tabIdToWindowId(tabId) {
return tabId - TAB_ID_BASE;
},
/**
* Delegates openOptionsPage handling to the app.
*
* @param {number} extensionId
* The ID of the extension requesting the options menu.
*
* @returns {Promise<Void>}
* A promise resolved after successful handling.
*/
async openOptionsPage(extensionId) {
debug`openOptionsPage for extensionId ${extensionId}`;
try {
await lazy.EventDispatcher.instance.sendRequestForResult({
type: "GeckoView:WebExtension:OpenOptionsPage",
extensionId,
});
} catch (errorMessage) {
// The error message coming from GeckoView is about :OpenOptionsPage not
// being registered so we need to have one that's extension friendly
// here.
throw new ExtensionError("runtime.openOptionsPage is not supported");
}
},
/**
* Request the GeckoView App to create a new tab (GeckoSession).
*
* @param {object} options
* @param {string} options.extensionId
* The ID of the extension that requested a new tab.
* @param {object} options.createProperties
* The properties for the new tab, see tabs.create reference for details.
*
* @returns {Promise<Tab>}
* A promise resolved to the newly created tab.
* @throws {Error}
* Throws an error if the GeckoView app doesn't support tabs.create or fails to handle the request.
*/
async createNewTab({ extensionId, createProperties } = {}) {
debug`createNewTab`;
const newSessionId = Services.uuid
.generateUUID()
.toString()
.slice(1, -1)
.replace(/-/g, "");
// The window might already be open by the time we get the response, so we
// need to start waiting before we send the message.
const windowPromise = new Promise(resolve => {
const handler = {
observe(aSubject, aTopic) {
if (
aTopic === "geckoview-window-created" &&
aSubject.name === newSessionId
) {
Services.obs.removeObserver(handler, "geckoview-window-created");
resolve(aSubject);
}
},
};
Services.obs.addObserver(handler, "geckoview-window-created");
});
let didOpenSession = false;
try {
didOpenSession = await lazy.EventDispatcher.instance.sendRequestForResult(
{
type: "GeckoView:WebExtension:NewTab",
extensionId,
createProperties,
newSessionId,
}
);
} catch (errorMessage) {
// The error message coming from GeckoView is about :NewTab not being
// registered so we need to have one that's extension friendly here.
throw new ExtensionError("tabs.create is not supported");
}
if (!didOpenSession) {
throw new ExtensionError("Cannot create new tab");
}
const window = await windowPromise;
if (!window.tab) {
window.tab = new Tab(window);
}
return window.tab;
},
/**
* 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<Void>}
* 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 } = {}) {
try {
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:CloseTab",
extensionId,
});
} catch (errorMessage) {
throw new ExtensionError(errorMessage);
}
},
async updateTab({ window, extensionId, updateProperties } = {}) {
try {
await window.WindowEventDispatcher.sendRequestForResult({
type: "GeckoView:WebExtension:UpdateTab",
extensionId,
updateProperties,
});
} catch (errorMessage) {
throw new ExtensionError(errorMessage);
}
},
};
export class GeckoViewTab extends GeckoViewModule {
onInit() {
const { window } = this;
if (!window.tab) {
window.tab = new Tab(window);
}
this.registerListener([
"GeckoView:WebExtension:SetTabActive",
"GeckoView:FlushSessionState",
]);
}
onEvent(aEvent, aData) {
debug`onEvent: event=${aEvent}, data=${aData}`;
switch (aEvent) {
case "GeckoView:WebExtension:SetTabActive": {
const { active } = aData;
lazy.mobileWindowTracker.setTabActive(this.window, active);
break;
}
case "GeckoView:FlushSessionState": {
if (Services.appinfo.sessionHistoryInParent) {
if (this.browser && this.browser.frameLoader) {
this.browser.frameLoader.requestTabStateFlush();
}
}
break;
}
}
}
}
const { debug, warn } = GeckoViewTab.initLogging("GeckoViewTab");