ext-android.js (16165B)
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 "use strict"; 5 6 /** 7 * NOTE: If you change the globals in this file, you must check if the globals 8 * list in mobile/android/.eslintrc.js also needs updating. 9 */ 10 11 ChromeUtils.defineESModuleGetters(this, { 12 GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs", 13 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 14 mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", 15 }); 16 17 var { EventDispatcher } = ChromeUtils.importESModule( 18 "resource://gre/modules/Messaging.sys.mjs" 19 ); 20 21 var { ExtensionCommon } = ChromeUtils.importESModule( 22 "resource://gre/modules/ExtensionCommon.sys.mjs" 23 ); 24 var { ExtensionUtils } = ChromeUtils.importESModule( 25 "resource://gre/modules/ExtensionUtils.sys.mjs" 26 ); 27 28 var { DefaultWeakMap, ExtensionError } = ExtensionUtils; 29 30 var { defineLazyGetter } = ExtensionCommon; 31 32 const BrowserStatusFilter = Components.Constructor( 33 "@mozilla.org/appshell/component/browser-status-filter;1", 34 "nsIWebProgress", 35 "addProgressListener" 36 ); 37 38 const WINDOW_TYPE = "navigator:geckoview"; 39 40 // We need let to break cyclic dependency 41 /* eslint-disable-next-line prefer-const */ 42 let windowTracker; 43 44 /** 45 * A nsIWebProgressListener for a specific XUL browser, which delegates the 46 * events that it receives to a tab progress listener, and prepends the browser 47 * to their arguments list. 48 * 49 * @param {XULElement} browser 50 * A XUL browser element. 51 * @param {object} listener 52 * A tab progress listener object. 53 * @param {integer} flags 54 * The web progress notification flags with which to filter events. 55 */ 56 class BrowserProgressListener { 57 constructor(browser, listener, flags) { 58 this.listener = listener; 59 this.browser = browser; 60 this.filter = new BrowserStatusFilter(this, flags); 61 this.browser.addProgressListener(this.filter, flags); 62 } 63 64 /** 65 * Destroy the listener, and perform any necessary cleanup. 66 */ 67 destroy() { 68 this.browser.removeProgressListener(this.filter); 69 this.filter.removeProgressListener(this); 70 } 71 72 /** 73 * Calls the appropriate listener in the wrapped tab progress listener, with 74 * the wrapped XUL browser object as its first argument, and the additional 75 * arguments in `args`. 76 * 77 * @param {string} method 78 * The name of the nsIWebProgressListener method which is being 79 * delegated. 80 * @param {*} args 81 * The arguments to pass to the delegated listener. 82 * @private 83 */ 84 delegate(method, ...args) { 85 if (this.listener[method]) { 86 this.listener[method](this.browser, ...args); 87 } 88 } 89 90 onLocationChange(webProgress, request, locationURI, flags) { 91 const window = this.browser.ownerGlobal; 92 // GeckoView windows can become popups at any moment, so we need to check 93 // here 94 if (!windowTracker.isBrowserWindow(window)) { 95 return; 96 } 97 98 this.delegate("onLocationChange", webProgress, request, locationURI, flags); 99 } 100 onStateChange(webProgress, request, stateFlags, status) { 101 this.delegate("onStateChange", webProgress, request, stateFlags, status); 102 } 103 } 104 105 const PROGRESS_LISTENER_FLAGS = 106 Ci.nsIWebProgress.NOTIFY_STATE_NETWORK | Ci.nsIWebProgress.NOTIFY_LOCATION; 107 108 class ProgressListenerWrapper { 109 constructor(window, listener) { 110 this.listener = new BrowserProgressListener( 111 window.browser, 112 listener, 113 PROGRESS_LISTENER_FLAGS 114 ); 115 } 116 117 destroy() { 118 this.listener.destroy(); 119 } 120 } 121 122 class WindowTracker extends WindowTrackerBase { 123 constructor(...args) { 124 super(...args); 125 126 this.progressListeners = new DefaultWeakMap(() => new WeakMap()); 127 } 128 129 getCurrentWindow(context) { 130 // In GeckoView the popup is on a separate window so getCurrentWindow for 131 // the popup should return whatever is the topWindow. 132 if (context?.viewType === "popup") { 133 return this.topWindow; 134 } 135 return super.getCurrentWindow(context); 136 } 137 138 get topWindow() { 139 return mobileWindowTracker.topWindow; 140 } 141 142 get topNonPBWindow() { 143 return mobileWindowTracker.topNonPBWindow; 144 } 145 146 isBrowserWindow(window) { 147 const { documentElement } = window.document; 148 return documentElement.getAttribute("windowtype") === WINDOW_TYPE; 149 } 150 151 addProgressListener(window, listener) { 152 const listeners = this.progressListeners.get(window); 153 if (!listeners.has(listener)) { 154 const wrapper = new ProgressListenerWrapper(window, listener); 155 listeners.set(listener, wrapper); 156 } 157 } 158 159 removeProgressListener(window, listener) { 160 const listeners = this.progressListeners.get(window); 161 const wrapper = listeners.get(listener); 162 if (wrapper) { 163 wrapper.destroy(); 164 listeners.delete(listener); 165 } 166 } 167 } 168 169 /** 170 * Helper to create an event manager which listens for an event in the Android 171 * global EventDispatcher, and calls the given listener function whenever the 172 * event is received. That listener function receives a `fire` object, 173 * which it can use to dispatch events to the extension, and an object 174 * detailing the EventDispatcher event that was received. 175 * 176 * @param {BaseContext} context 177 * The extension context which the event manager belongs to. 178 * @param {string} name 179 * The API name of the event manager, e.g.,"runtime.onMessage". 180 * @param {string} event 181 * The name of the EventDispatcher event to listen for. 182 * @param {Function} listener 183 * The listener function to call when an EventDispatcher event is 184 * recieved. 185 * 186 * @returns {object} An injectable api for the new event. 187 */ 188 global.makeGlobalEvent = function makeGlobalEvent( 189 context, 190 name, 191 event, 192 listener 193 ) { 194 return new EventManager({ 195 context, 196 name, 197 register: fire => { 198 const listener2 = { 199 onEvent(event, data) { 200 listener(fire, data); 201 }, 202 }; 203 204 EventDispatcher.instance.registerListener(listener2, [event]); 205 return () => { 206 EventDispatcher.instance.unregisterListener(listener2, [event]); 207 }; 208 }, 209 }).api(); 210 }; 211 212 class TabTracker extends TabTrackerBase { 213 init() { 214 if (this.initialized) { 215 return; 216 } 217 this.initialized = true; 218 219 windowTracker.addOpenListener(window => { 220 const nativeTab = window.tab; 221 this.emit("tab-created", { nativeTab }); 222 }); 223 224 windowTracker.addCloseListener(window => { 225 const { tab: nativeTab, browser } = window; 226 const { windowId, tabId } = this.getBrowserData(browser); 227 this.emit("tab-removed", { 228 nativeTab, 229 tabId, 230 windowId, 231 // In GeckoView, it is not meaningful to speak of "window closed", because a tab is a window. 232 // Until we have a meaningful way to group tabs (and close multiple tabs at once), 233 // let's use isWindowClosing: false 234 isWindowClosing: false, 235 }); 236 }); 237 } 238 239 getId(nativeTab) { 240 return nativeTab.id; 241 } 242 243 getTab(id, default_ = undefined) { 244 const windowId = GeckoViewTabBridge.tabIdToWindowId(id); 245 const window = windowTracker.getWindow(windowId, null, false); 246 247 if (window) { 248 const { tab } = window; 249 if (tab) { 250 return tab; 251 } 252 } 253 254 if (default_ !== undefined) { 255 return default_; 256 } 257 throw new ExtensionError(`Invalid tab ID: ${id}`); 258 } 259 260 getBrowserData(browser) { 261 const window = browser.ownerGlobal; 262 const tab = window?.tab; 263 if (!tab) { 264 return { 265 tabId: -1, 266 windowId: -1, 267 }; 268 } 269 270 const windowId = windowTracker.getId(window); 271 272 if (!windowTracker.isBrowserWindow(window)) { 273 return { 274 windowId, 275 tabId: -1, 276 }; 277 } 278 279 return { 280 windowId, 281 tabId: this.getId(tab), 282 }; 283 } 284 285 getBrowserDataForContext(context) { 286 if (["tab", "background"].includes(context.viewType)) { 287 return this.getBrowserData(context.xulBrowser); 288 } else if (context.viewType === "popup") { 289 const chromeWindow = windowTracker.getCurrentWindow(context); 290 const windowId = chromeWindow ? windowTracker.getId(chromeWindow) : -1; 291 return { tabId: -1, windowId }; 292 } 293 294 return { tabId: -1, windowId: -1 }; 295 } 296 297 get activeTab() { 298 const window = windowTracker.topWindow; 299 if (window) { 300 return window.tab; 301 } 302 return null; 303 } 304 } 305 306 windowTracker = new WindowTracker(); 307 const tabTracker = new TabTracker(); 308 309 Object.assign(global, { tabTracker, windowTracker }); 310 311 class Tab extends TabBase { 312 get _favIconUrl() { 313 return undefined; 314 } 315 316 get attention() { 317 return false; 318 } 319 320 get audible() { 321 return this.nativeTab.playingAudio; 322 } 323 324 get browser() { 325 return this.nativeTab.browser; 326 } 327 328 get discarded() { 329 return this.browser.getAttribute("pending") === "true"; 330 } 331 332 get cookieStoreId() { 333 return getCookieStoreIdForTab(this, this.nativeTab); 334 } 335 336 get height() { 337 return this.browser.clientHeight; 338 } 339 340 get incognito() { 341 return PrivateBrowsingUtils.isBrowserPrivate(this.browser); 342 } 343 344 get index() { 345 return 0; 346 } 347 348 get mutedInfo() { 349 return { muted: false }; 350 } 351 352 get lastAccessed() { 353 return this.nativeTab.lastTouchedAt; 354 } 355 356 get pinned() { 357 return false; 358 } 359 360 get active() { 361 return this.nativeTab.getActive(); 362 } 363 364 get highlighted() { 365 return this.active; 366 } 367 368 get status() { 369 if (this.browser.webProgress.isLoadingDocument) { 370 return "loading"; 371 } 372 return "complete"; 373 } 374 375 get successorTabId() { 376 return -1; 377 } 378 379 get groupId() { 380 return -1; 381 } 382 383 get width() { 384 return this.browser.clientWidth; 385 } 386 387 get window() { 388 return this.browser.ownerGlobal; 389 } 390 391 get windowId() { 392 return windowTracker.getId(this.window); 393 } 394 395 // TODO: Just return false for these until properly implemented on Android. 396 // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924 397 get isArticle() { 398 return false; 399 } 400 401 get isInReaderMode() { 402 return false; 403 } 404 405 get hidden() { 406 return false; 407 } 408 409 get autoDiscardable() { 410 // This property reflects whether the browser is allowed to auto-discard. 411 // Since extensions cannot do so on Android, we return true here. 412 return true; 413 } 414 415 get sharingState() { 416 return { 417 screen: undefined, 418 microphone: false, 419 camera: false, 420 }; 421 } 422 } 423 424 // Manages tab-specific context data and dispatches tab select and close events. 425 class TabContext extends EventEmitter { 426 constructor(getDefaultPrototype) { 427 super(); 428 429 windowTracker.addListener("progress", this); 430 431 this.getDefaultPrototype = getDefaultPrototype; 432 this.tabData = new Map(); 433 } 434 435 onLocationChange(browser, webProgress, request, locationURI, flags) { 436 if (!webProgress.isTopLevel) { 437 // Only pageAction and browserAction are consuming the "location-change" event 438 // to update their per-tab status, and they should only do so in response of 439 // location changes related to the top level frame (See Bug 1493470 for a rationale). 440 return; 441 } 442 const { tab } = browser.ownerGlobal; 443 // fromBrowse will be false in case of e.g. a hash change or history.pushState 444 const fromBrowse = !( 445 flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT 446 ); 447 this.emit( 448 "location-change", 449 { 450 id: tab.id, 451 linkedBrowser: browser, 452 // TODO: we don't support selected so we just alway say we are 453 selected: true, 454 }, 455 fromBrowse 456 ); 457 } 458 459 get(tabId) { 460 if (!this.tabData.has(tabId)) { 461 const data = Object.create(this.getDefaultPrototype(tabId)); 462 this.tabData.set(tabId, data); 463 } 464 465 return this.tabData.get(tabId); 466 } 467 468 clear(tabId) { 469 this.tabData.delete(tabId); 470 } 471 472 shutdown() { 473 windowTracker.removeListener("progress", this); 474 } 475 } 476 477 class Window extends WindowBase { 478 get focused() { 479 return this.window.document.hasFocus(); 480 } 481 482 isCurrentFor(context) { 483 // In GeckoView the popup is on a separate window so the current window for 484 // the popup is whatever is the topWindow. 485 if (context?.viewType === "popup") { 486 return mobileWindowTracker.topWindow == this.window; 487 } 488 return super.isCurrentFor(context); 489 } 490 491 get top() { 492 return this.window.screenY; 493 } 494 495 get left() { 496 return this.window.screenX; 497 } 498 499 get width() { 500 return this.window.outerWidth; 501 } 502 503 get height() { 504 return this.window.outerHeight; 505 } 506 507 get incognito() { 508 return PrivateBrowsingUtils.isWindowPrivate(this.window); 509 } 510 511 get alwaysOnTop() { 512 return false; 513 } 514 515 get isLastFocused() { 516 return this.window === windowTracker.topWindow; 517 } 518 519 get state() { 520 return "fullscreen"; 521 } 522 523 *getTabs() { 524 yield this.activeTab; 525 } 526 527 *getHighlightedTabs() { 528 yield this.activeTab; 529 } 530 531 get activeTab() { 532 const { tabManager } = this.extension; 533 return tabManager.getWrapper(this.window.tab); 534 } 535 536 getTabAtIndex(index) { 537 if (index == 0) { 538 return this.activeTab; 539 } 540 } 541 } 542 543 Object.assign(global, { Tab, TabContext, Window }); 544 545 class TabManager extends TabManagerBase { 546 get(tabId, default_ = undefined) { 547 const nativeTab = tabTracker.getTab(tabId, default_); 548 549 if (nativeTab) { 550 return this.getWrapper(nativeTab); 551 } 552 return default_; 553 } 554 555 addActiveTabPermission(nativeTab = tabTracker.activeTab) { 556 return super.addActiveTabPermission(nativeTab); 557 } 558 559 revokeActiveTabPermission(nativeTab = tabTracker.activeTab) { 560 return super.revokeActiveTabPermission(nativeTab); 561 } 562 563 canAccessTab(nativeTab) { 564 return ( 565 this.extension.privateBrowsingAllowed || 566 !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser) 567 ); 568 } 569 570 wrapTab(nativeTab) { 571 return new Tab(this.extension, nativeTab, nativeTab.id); 572 } 573 } 574 575 class WindowManager extends WindowManagerBase { 576 get(windowId, context) { 577 const window = windowTracker.getWindow(windowId, context); 578 579 return this.getWrapper(window); 580 } 581 582 *getAll(context) { 583 for (const window of windowTracker.browserWindows()) { 584 if (!this.canAccessWindow(window, context)) { 585 continue; 586 } 587 const wrapped = this.getWrapper(window); 588 if (wrapped) { 589 yield wrapped; 590 } 591 } 592 } 593 594 wrapWindow(window) { 595 return new Window(this.extension, window, windowTracker.getId(window)); 596 } 597 } 598 599 // eslint-disable-next-line mozilla/balanced-listeners 600 extensions.on("startup", (type, extension) => { 601 defineLazyGetter(extension, "tabManager", () => new TabManager(extension)); 602 defineLazyGetter( 603 extension, 604 "windowManager", 605 () => new WindowManager(extension) 606 ); 607 }); 608 609 /* eslint-disable mozilla/balanced-listeners */ 610 extensions.on("page-shutdown", (type, context) => { 611 if (context.viewType == "tab") { 612 const window = context.xulBrowser.ownerGlobal; 613 if (!windowTracker.isBrowserWindow(window)) { 614 // Content in non-browser window, e.g. ContentPage in xpcshell uses 615 // chrome://extensions/content/dummy.xhtml as the window. 616 return; 617 } 618 GeckoViewTabBridge.closeTab({ 619 window, 620 extensionId: context.extension.id, 621 }); 622 } 623 }); 624 /* eslint-enable mozilla/balanced-listeners */ 625 626 global.openOptionsPage = async extension => { 627 const { optionsPageProperties } = extension; 628 const extensionId = extension.id; 629 630 if (optionsPageProperties.open_in_tab) { 631 // Delegate new tab creation and open the options page in the new tab. 632 const tab = await GeckoViewTabBridge.createNewTab({ 633 extensionId, 634 createProperties: { 635 url: optionsPageProperties.page, 636 active: true, 637 }, 638 }); 639 640 const { browser } = tab; 641 const loadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; 642 643 browser.fixupAndLoadURIString(optionsPageProperties.page, { 644 loadFlags, 645 triggeringPrincipal: extension.principal, 646 }); 647 648 const newWindow = browser.ownerGlobal; 649 mobileWindowTracker.setTabActive(newWindow, true); 650 return; 651 } 652 653 // Delegate option page handling to the app. 654 return GeckoViewTabBridge.openOptionsPage(extensionId); 655 };