GeckoViewTab.sys.mjs (6624B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs"; 6 7 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; 8 9 const { ExtensionError } = ExtensionUtils; 10 11 const lazy = {}; 12 13 ChromeUtils.defineESModuleGetters(lazy, { 14 EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", 15 mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", 16 }); 17 18 class Tab { 19 constructor(window) { 20 this.id = GeckoViewTabBridge.windowIdToTabId(window.docShell.outerWindowID); 21 this.browser = window.browser; 22 this.active = false; 23 } 24 25 get linkedBrowser() { 26 return this.browser; 27 } 28 29 getActive() { 30 return this.active; 31 } 32 33 get userContextId() { 34 return this.browser.ownerGlobal.moduleManager.settings 35 .unsafeSessionContextId; 36 } 37 } 38 39 // Because of bug 1410749, we can't use 0, though, and just to be safe 40 // we choose a value that is unlikely to overlap with Fennec's tab IDs. 41 const TAB_ID_BASE = 10000; 42 43 export const GeckoViewTabBridge = { 44 /** 45 * Converts windowId to tabId as in GeckoView every browser window has exactly one tab. 46 * 47 * @param {number} windowId outerWindowId 48 * 49 * @returns {number} tabId 50 */ 51 windowIdToTabId(windowId) { 52 return TAB_ID_BASE + windowId; 53 }, 54 55 /** 56 * Converts tabId to windowId. 57 * 58 * @param {number} tabId 59 * 60 * @returns {number} 61 * outerWindowId of browser window to which the tab belongs. 62 */ 63 tabIdToWindowId(tabId) { 64 return tabId - TAB_ID_BASE; 65 }, 66 67 /** 68 * Delegates openOptionsPage handling to the app. 69 * 70 * @param {number} extensionId 71 * The ID of the extension requesting the options menu. 72 * 73 * @returns {Promise<Void>} 74 * A promise resolved after successful handling. 75 */ 76 async openOptionsPage(extensionId) { 77 debug`openOptionsPage for extensionId ${extensionId}`; 78 79 try { 80 await lazy.EventDispatcher.instance.sendRequestForResult({ 81 type: "GeckoView:WebExtension:OpenOptionsPage", 82 extensionId, 83 }); 84 } catch (errorMessage) { 85 // The error message coming from GeckoView is about :OpenOptionsPage not 86 // being registered so we need to have one that's extension friendly 87 // here. 88 throw new ExtensionError("runtime.openOptionsPage is not supported"); 89 } 90 }, 91 92 /** 93 * Request the GeckoView App to create a new tab (GeckoSession). 94 * 95 * @param {object} options 96 * @param {string} options.extensionId 97 * The ID of the extension that requested a new tab. 98 * @param {object} options.createProperties 99 * The properties for the new tab, see tabs.create reference for details. 100 * 101 * @returns {Promise<Tab>} 102 * A promise resolved to the newly created tab. 103 * @throws {Error} 104 * Throws an error if the GeckoView app doesn't support tabs.create or fails to handle the request. 105 */ 106 async createNewTab({ extensionId, createProperties } = {}) { 107 debug`createNewTab`; 108 109 const newSessionId = Services.uuid 110 .generateUUID() 111 .toString() 112 .slice(1, -1) 113 .replace(/-/g, ""); 114 115 // The window might already be open by the time we get the response, so we 116 // need to start waiting before we send the message. 117 const windowPromise = new Promise(resolve => { 118 const handler = { 119 observe(aSubject, aTopic) { 120 if ( 121 aTopic === "geckoview-window-created" && 122 aSubject.name === newSessionId 123 ) { 124 Services.obs.removeObserver(handler, "geckoview-window-created"); 125 resolve(aSubject); 126 } 127 }, 128 }; 129 Services.obs.addObserver(handler, "geckoview-window-created"); 130 }); 131 132 let didOpenSession = false; 133 try { 134 didOpenSession = await lazy.EventDispatcher.instance.sendRequestForResult( 135 { 136 type: "GeckoView:WebExtension:NewTab", 137 extensionId, 138 createProperties, 139 newSessionId, 140 } 141 ); 142 } catch (errorMessage) { 143 // The error message coming from GeckoView is about :NewTab not being 144 // registered so we need to have one that's extension friendly here. 145 throw new ExtensionError("tabs.create is not supported"); 146 } 147 148 if (!didOpenSession) { 149 throw new ExtensionError("Cannot create new tab"); 150 } 151 152 const window = await windowPromise; 153 if (!window.tab) { 154 window.tab = new Tab(window); 155 } 156 return window.tab; 157 }, 158 159 /** 160 * Request the GeckoView App to close a tab (GeckoSession). 161 * 162 * @param {object} options 163 * @param {Window} options.window The window owning the tab to close 164 * @param {string} options.extensionId 165 * 166 * @returns {Promise<Void>} 167 * A promise resolved after GeckoSession is closed. 168 * @throws {Error} 169 * Throws an error if the GeckoView app doesn't allow extension to close tab. 170 */ 171 async closeTab({ window, extensionId } = {}) { 172 try { 173 await window.WindowEventDispatcher.sendRequestForResult({ 174 type: "GeckoView:WebExtension:CloseTab", 175 extensionId, 176 }); 177 } catch (errorMessage) { 178 throw new ExtensionError(errorMessage); 179 } 180 }, 181 182 async updateTab({ window, extensionId, updateProperties } = {}) { 183 try { 184 await window.WindowEventDispatcher.sendRequestForResult({ 185 type: "GeckoView:WebExtension:UpdateTab", 186 extensionId, 187 updateProperties, 188 }); 189 } catch (errorMessage) { 190 throw new ExtensionError(errorMessage); 191 } 192 }, 193 }; 194 195 export class GeckoViewTab extends GeckoViewModule { 196 onInit() { 197 const { window } = this; 198 if (!window.tab) { 199 window.tab = new Tab(window); 200 } 201 202 this.registerListener([ 203 "GeckoView:WebExtension:SetTabActive", 204 "GeckoView:FlushSessionState", 205 ]); 206 } 207 208 onEvent(aEvent, aData) { 209 debug`onEvent: event=${aEvent}, data=${aData}`; 210 211 switch (aEvent) { 212 case "GeckoView:WebExtension:SetTabActive": { 213 const { active } = aData; 214 lazy.mobileWindowTracker.setTabActive(this.window, active); 215 break; 216 } 217 case "GeckoView:FlushSessionState": { 218 if (Services.appinfo.sessionHistoryInParent) { 219 if (this.browser && this.browser.frameLoader) { 220 this.browser.frameLoader.requestTabStateFlush(); 221 } 222 } 223 break; 224 } 225 } 226 } 227 } 228 229 const { debug, warn } = GeckoViewTab.initLogging("GeckoViewTab");