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/. */
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.
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",
17 var { EventDispatcher } = ChromeUtils.importESModule(
18 "resource://gre/modules/Messaging.sys.mjs"
21 var { ExtensionCommon } = ChromeUtils.importESModule(
22 "resource://gre/modules/ExtensionCommon.sys.mjs"
24 var { ExtensionUtils } = ChromeUtils.importESModule(
25 "resource://gre/modules/ExtensionUtils.sys.mjs"
28 var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
30 var { defineLazyGetter } = ExtensionCommon;
32 const BrowserStatusFilter = Components.Constructor(
33 "@mozilla.org/appshell/component/browser-status-filter;1",
38 const WINDOW_TYPE = "navigator:geckoview";
40 // We need let to break cyclic dependency
41 /* eslint-disable-next-line prefer-const */
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.
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.
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);
65 * Destroy the listener, and perform any necessary cleanup.
68 this.browser.removeProgressListener(this.filter);
69 this.filter.removeProgressListener(this);
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`.
77 * @param {string} method
78 * The name of the nsIWebProgressListener method which is being
81 * The arguments to pass to the delegated listener.
84 delegate(method, ...args) {
85 if (this.listener[method]) {
86 this.listener[method](this.browser, ...args);
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
94 if (!windowTracker.isBrowserWindow(window)) {
98 this.delegate("onLocationChange", webProgress, request, locationURI, flags);
100 onStateChange(webProgress, request, stateFlags, status) {
101 this.delegate("onStateChange", webProgress, request, stateFlags, status);
105 const PROGRESS_LISTENER_FLAGS =
106 Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION;
108 class ProgressListenerWrapper {
109 constructor(window, listener) {
110 this.listener = new BrowserProgressListener(
113 PROGRESS_LISTENER_FLAGS
118 this.listener.destroy();
122 class WindowTracker extends WindowTrackerBase {
123 constructor(...args) {
126 this.progressListeners = new DefaultWeakMap(() => new WeakMap());
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 // TODO: Bug 1651506 use context?.viewType === "popup" instead
133 if (context?.currentWindow?.moduleManager.settings.isPopup) {
134 return this.topWindow;
136 return super.getCurrentWindow(context);
140 return mobileWindowTracker.topWindow;
143 get topNonPBWindow() {
144 return mobileWindowTracker.topNonPBWindow;
147 isBrowserWindow(window) {
148 const { documentElement } = window.document;
149 return documentElement.getAttribute("windowtype") === WINDOW_TYPE;
152 addProgressListener(window, listener) {
153 const listeners = this.progressListeners.get(window);
154 if (!listeners.has(listener)) {
155 const wrapper = new ProgressListenerWrapper(window, listener);
156 listeners.set(listener, wrapper);
160 removeProgressListener(window, listener) {
161 const listeners = this.progressListeners.get(window);
162 const wrapper = listeners.get(listener);
165 listeners.delete(listener);
171 * Helper to create an event manager which listens for an event in the Android
172 * global EventDispatcher, and calls the given listener function whenever the
173 * event is received. That listener function receives a `fire` object,
174 * which it can use to dispatch events to the extension, and an object
175 * detailing the EventDispatcher event that was received.
177 * @param {BaseContext} context
178 * The extension context which the event manager belongs to.
179 * @param {string} name
180 * The API name of the event manager, e.g.,"runtime.onMessage".
181 * @param {string} event
182 * The name of the EventDispatcher event to listen for.
183 * @param {Function} listener
184 * The listener function to call when an EventDispatcher event is
187 * @returns {object} An injectable api for the new event.
189 global.makeGlobalEvent = function makeGlobalEvent(
195 return new EventManager({
200 onEvent(event, data) {
201 listener(fire, data);
205 EventDispatcher.instance.registerListener(listener2, [event]);
207 EventDispatcher.instance.unregisterListener(listener2, [event]);
213 class TabTracker extends TabTrackerBase {
215 if (this.initialized) {
218 this.initialized = true;
220 windowTracker.addOpenListener(window => {
221 const nativeTab = window.tab;
222 this.emit("tab-created", { nativeTab });
225 windowTracker.addCloseListener(window => {
226 const { tab: nativeTab, browser } = window;
227 const { windowId, tabId } = this.getBrowserData(browser);
228 this.emit("tab-removed", {
232 // In GeckoView, it is not meaningful to speak of "window closed", because a tab is a window.
233 // Until we have a meaningful way to group tabs (and close multiple tabs at once),
234 // let's use isWindowClosing: false
235 isWindowClosing: false,
244 getTab(id, default_ = undefined) {
245 const windowId = GeckoViewTabBridge.tabIdToWindowId(id);
246 const window = windowTracker.getWindow(windowId, null, false);
249 const { tab } = window;
255 if (default_ !== undefined) {
258 throw new ExtensionError(`Invalid tab ID: ${id}`);
261 getBrowserData(browser) {
262 const window = browser.ownerGlobal;
263 const tab = window?.tab;
271 const windowId = windowTracker.getId(window);
273 if (!windowTracker.isBrowserWindow(window)) {
282 tabId: this.getId(tab),
287 const window = windowTracker.topWindow;
295 windowTracker = new WindowTracker();
296 const tabTracker = new TabTracker();
298 Object.assign(global, { tabTracker, windowTracker });
300 class Tab extends TabBase {
310 return this.nativeTab.playingAudio;
314 return this.nativeTab.browser;
318 return this.browser.getAttribute("pending") === "true";
321 get cookieStoreId() {
322 return getCookieStoreIdForTab(this, this.nativeTab);
326 return this.browser.clientHeight;
330 return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
338 return { muted: false };
342 return this.nativeTab.lastTouchedAt;
350 return this.nativeTab.getActive();
358 if (this.browser.webProgress.isLoadingDocument) {
364 get successorTabId() {
369 return this.browser.clientWidth;
373 return this.browser.ownerGlobal;
377 return windowTracker.getId(this.window);
380 // TODO: Just return false for these until properly implemented on Android.
381 // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
386 get isInReaderMode() {
394 get autoDiscardable() {
395 // This property reflects whether the browser is allowed to auto-discard.
396 // Since extensions cannot do so on Android, we return true here.
409 // Manages tab-specific context data and dispatches tab select and close events.
410 class TabContext extends EventEmitter {
411 constructor(getDefaultPrototype) {
414 windowTracker.addListener("progress", this);
416 this.getDefaultPrototype = getDefaultPrototype;
417 this.tabData = new Map();
420 onLocationChange(browser, webProgress, request, locationURI, flags) {
421 if (!webProgress.isTopLevel) {
422 // Only pageAction and browserAction are consuming the "location-change" event
423 // to update their per-tab status, and they should only do so in response of
424 // location changes related to the top level frame (See Bug 1493470 for a rationale).
427 const { tab } = browser.ownerGlobal;
428 // fromBrowse will be false in case of e.g. a hash change or history.pushState
429 const fromBrowse = !(
430 flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
436 linkedBrowser: browser,
437 // TODO: we don't support selected so we just alway say we are
445 if (!this.tabData.has(tabId)) {
446 const data = Object.create(this.getDefaultPrototype(tabId));
447 this.tabData.set(tabId, data);
450 return this.tabData.get(tabId);
454 this.tabData.delete(tabId);
458 windowTracker.removeListener("progress", this);
462 class Window extends WindowBase {
464 return this.window.document.hasFocus();
467 isCurrentFor(context) {
468 // In GeckoView the popup is on a separate window so the current window for
469 // the popup is whatever is the topWindow.
470 // TODO: Bug 1651506 use context?.viewType === "popup" instead
471 if (context?.currentWindow?.moduleManager.settings.isPopup) {
472 return mobileWindowTracker.topWindow == this.window;
474 return super.isCurrentFor(context);
478 return this.window.screenY;
482 return this.window.screenX;
486 return this.window.outerWidth;
490 return this.window.outerHeight;
494 return PrivateBrowsingUtils.isWindowPrivate(this.window);
501 get isLastFocused() {
502 return this.window === windowTracker.topWindow;
510 yield this.activeTab;
513 *getHighlightedTabs() {
514 yield this.activeTab;
518 const { tabManager } = this.extension;
519 return tabManager.getWrapper(this.window.tab);
522 getTabAtIndex(index) {
524 return this.activeTab;
529 Object.assign(global, { Tab, TabContext, Window });
531 class TabManager extends TabManagerBase {
532 get(tabId, default_ = undefined) {
533 const nativeTab = tabTracker.getTab(tabId, default_);
536 return this.getWrapper(nativeTab);
541 addActiveTabPermission(nativeTab = tabTracker.activeTab) {
542 return super.addActiveTabPermission(nativeTab);
545 revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
546 return super.revokeActiveTabPermission(nativeTab);
549 canAccessTab(nativeTab) {
551 this.extension.privateBrowsingAllowed ||
552 !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser)
557 return new Tab(this.extension, nativeTab, nativeTab.id);
561 class WindowManager extends WindowManagerBase {
562 get(windowId, context) {
563 const window = windowTracker.getWindow(windowId, context);
565 return this.getWrapper(window);
569 for (const window of windowTracker.browserWindows()) {
570 if (!this.canAccessWindow(window, context)) {
573 const wrapped = this.getWrapper(window);
581 return new Window(this.extension, window, windowTracker.getId(window));
585 // eslint-disable-next-line mozilla/balanced-listeners
586 extensions.on("startup", (type, extension) => {
587 defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
591 () => new WindowManager(extension)
595 /* eslint-disable mozilla/balanced-listeners */
596 extensions.on("page-shutdown", (type, context) => {
597 if (context.viewType == "tab") {
598 const window = context.xulBrowser.ownerGlobal;
599 if (!windowTracker.isBrowserWindow(window)) {
600 // Content in non-browser window, e.g. ContentPage in xpcshell uses
601 // chrome://extensions/content/dummy.xhtml as the window.
604 GeckoViewTabBridge.closeTab({
606 extensionId: context.extension.id,
610 /* eslint-enable mozilla/balanced-listeners */
612 global.openOptionsPage = async extension => {
613 const { options_ui } = extension.manifest;
614 const extensionId = extension.id;
616 if (options_ui.open_in_tab) {
617 // Delegate new tab creation and open the options page in the new tab.
618 const tab = await GeckoViewTabBridge.createNewTab({
621 url: options_ui.page,
626 const { browser } = tab;
627 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
629 browser.fixupAndLoadURIString(options_ui.page, {
631 triggeringPrincipal: extension.principal,
634 const newWindow = browser.ownerGlobal;
635 mobileWindowTracker.setTabActive(newWindow, true);
639 // Delegate option page handling to the app.
640 return GeckoViewTabBridge.openOptionsPage(extensionId);