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/. */
6 ChromeUtils.defineModuleGetter(
8 "PrivateBrowsingUtils",
9 "resource://gre/modules/PrivateBrowsingUtils.jsm"
12 ChromeUtils.defineModuleGetter(
15 "resource://gre/modules/GeckoViewTab.jsm"
18 ChromeUtils.defineModuleGetter(
20 "mobileWindowTracker",
21 "resource://gre/modules/GeckoViewWebExtension.jsm"
24 var { EventDispatcher } = ChromeUtils.import(
25 "resource://gre/modules/Messaging.jsm"
28 var { ExtensionCommon } = ChromeUtils.import(
29 "resource://gre/modules/ExtensionCommon.jsm"
31 var { ExtensionUtils } = ChromeUtils.import(
32 "resource://gre/modules/ExtensionUtils.jsm"
35 var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
37 var { defineLazyGetter } = ExtensionCommon;
39 global.GlobalEventDispatcher = EventDispatcher.instance;
41 const BrowserStatusFilter = Components.Constructor(
42 "@mozilla.org/appshell/component/browser-status-filter;1",
47 const WINDOW_TYPE = "navigator:geckoview";
49 // We need let to break cyclic dependency
50 /* eslint-disable-next-line prefer-const */
54 * A nsIWebProgressListener for a specific XUL browser, which delegates the
55 * events that it receives to a tab progress listener, and prepends the browser
56 * to their arguments list.
58 * @param {XULElement} browser
59 * A XUL browser element.
60 * @param {object} listener
61 * A tab progress listener object.
62 * @param {integer} flags
63 * The web progress notification flags with which to filter events.
65 class BrowserProgressListener {
66 constructor(browser, listener, flags) {
67 this.listener = listener;
68 this.browser = browser;
69 this.filter = new BrowserStatusFilter(this, flags);
70 this.browser.addProgressListener(this.filter, flags);
74 * Destroy the listener, and perform any necessary cleanup.
77 this.browser.removeProgressListener(this.filter);
78 this.filter.removeProgressListener(this);
82 * Calls the appropriate listener in the wrapped tab progress listener, with
83 * the wrapped XUL browser object as its first argument, and the additional
84 * arguments in `args`.
86 * @param {string} method
87 * The name of the nsIWebProgressListener method which is being
90 * The arguments to pass to the delegated listener.
93 delegate(method, ...args) {
94 if (this.listener[method]) {
95 this.listener[method](this.browser, ...args);
99 onLocationChange(webProgress, request, locationURI, flags) {
100 const window = this.browser.ownerGlobal;
101 // GeckoView windows can become popups at any moment, so we need to check
103 if (!windowTracker.isBrowserWindow(window)) {
107 this.delegate("onLocationChange", webProgress, request, locationURI, flags);
109 onStateChange(webProgress, request, stateFlags, status) {
110 this.delegate("onStateChange", webProgress, request, stateFlags, status);
114 const PROGRESS_LISTENER_FLAGS =
115 Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION;
117 class ProgressListenerWrapper {
118 constructor(window, listener) {
119 this.listener = new BrowserProgressListener(
122 PROGRESS_LISTENER_FLAGS
127 this.listener.destroy();
131 class WindowTracker extends WindowTrackerBase {
132 constructor(...args) {
135 this.progressListeners = new DefaultWeakMap(() => new WeakMap());
138 getCurrentWindow(context) {
139 // In GeckoView the popup is on a separate window so getCurrentWindow for
140 // the popup should return whatever is the topWindow.
141 // TODO: Bug 1651506 use context?.viewType === "popup" instead
142 if (context?.currentWindow?.moduleManager.settings.isPopup) {
143 return this.topWindow;
145 return super.getCurrentWindow(context);
149 return mobileWindowTracker.topWindow;
152 get topNonPBWindow() {
153 return mobileWindowTracker.topNonPBWindow;
156 isBrowserWindow(window) {
157 const { documentElement } = window.document;
158 return documentElement.getAttribute("windowtype") === WINDOW_TYPE;
161 addProgressListener(window, listener) {
162 const listeners = this.progressListeners.get(window);
163 if (!listeners.has(listener)) {
164 const wrapper = new ProgressListenerWrapper(window, listener);
165 listeners.set(listener, wrapper);
169 removeProgressListener(window, listener) {
170 const listeners = this.progressListeners.get(window);
171 const wrapper = listeners.get(listener);
174 listeners.delete(listener);
180 * Helper to create an event manager which listens for an event in the Android
181 * global EventDispatcher, and calls the given listener function whenever the
182 * event is received. That listener function receives a `fire` object,
183 * which it can use to dispatch events to the extension, and an object
184 * detailing the EventDispatcher event that was received.
186 * @param {BaseContext} context
187 * The extension context which the event manager belongs to.
188 * @param {string} name
189 * The API name of the event manager, e.g.,"runtime.onMessage".
190 * @param {string} event
191 * The name of the EventDispatcher event to listen for.
192 * @param {function} listener
193 * The listener function to call when an EventDispatcher event is
196 * @returns {object} An injectable api for the new event.
198 global.makeGlobalEvent = function makeGlobalEvent(
204 return new EventManager({
209 onEvent(event, data, callback) {
210 listener(fire, data);
214 GlobalEventDispatcher.registerListener(listener2, [event]);
216 GlobalEventDispatcher.unregisterListener(listener2, [event]);
222 class TabTracker extends TabTrackerBase {
224 if (this.initialized) {
227 this.initialized = true;
229 windowTracker.addOpenListener(window => {
230 const nativeTab = window.tab;
231 this.emit("tab-created", { nativeTab });
234 windowTracker.addCloseListener(window => {
235 const { tab, browser } = window;
236 const { windowId, tabId } = this.getBrowserData(browser);
237 this.emit("tab-removed", {
241 // In GeckoView, it is not meaningful to speak of "window closed", because a tab is a window.
242 // Until we have a meaningful way to group tabs (and close multiple tabs at once),
243 // let's use isWindowClosing: false
244 isWindowClosing: false,
253 getTab(id, default_ = undefined) {
254 const windowId = GeckoViewTabBridge.tabIdToWindowId(id);
255 const window = windowTracker.getWindow(windowId, null, false);
258 const { tab } = window;
264 if (default_ !== undefined) {
267 throw new ExtensionError(`Invalid tab ID: ${id}`);
270 getBrowserData(browser) {
271 const window = browser.ownerGlobal;
272 const { tab } = window;
280 const windowId = windowTracker.getId(window);
282 if (!windowTracker.isBrowserWindow(window)) {
291 tabId: this.getId(tab),
296 const window = windowTracker.topWindow;
304 windowTracker = new WindowTracker();
305 const tabTracker = new TabTracker();
307 Object.assign(global, { tabTracker, windowTracker });
309 class Tab extends TabBase {
319 return this.nativeTab.playingAudio;
323 return this.nativeTab.browser;
327 return this.browser.getAttribute("pending") === "true";
330 get cookieStoreId() {
331 return getCookieStoreIdForTab(this, this.nativeTab);
335 return this.browser.clientHeight;
339 return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
347 return { muted: false };
351 return this.nativeTab.lastTouchedAt;
359 return this.nativeTab.getActive();
367 return this.nativeTab.getActive();
371 if (this.browser.webProgress.isLoadingDocument) {
377 get successorTabId() {
382 return this.browser.clientWidth;
386 return this.browser.ownerGlobal;
390 return windowTracker.getId(this.window);
393 // TODO: Just return false for these until properly implemented on Android.
394 // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
399 get isInReaderMode() {
416 // Manages tab-specific context data and dispatches tab select and close events.
417 class TabContext extends EventEmitter {
418 constructor(getDefaultPrototype) {
421 windowTracker.addListener("progress", this);
423 this.getDefaultPrototype = getDefaultPrototype;
424 this.tabData = new Map();
427 onLocationChange(browser, webProgress, request, locationURI, flags) {
428 if (!webProgress.isTopLevel) {
429 // Only pageAction and browserAction are consuming the "location-change" event
430 // to update their per-tab status, and they should only do so in response of
431 // location changes related to the top level frame (See Bug 1493470 for a rationale).
434 const { tab } = browser.ownerGlobal;
435 // fromBrowse will be false in case of e.g. a hash change or history.pushState
436 const fromBrowse = !(
437 flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
443 linkedBrowser: browser,
444 // TODO: we don't support selected so we just alway say we are
452 if (!this.tabData.has(tabId)) {
453 const data = Object.create(this.getDefaultPrototype(tabId));
454 this.tabData.set(tabId, data);
457 return this.tabData.get(tabId);
461 this.tabData.delete(tabId);
465 windowTracker.removeListener("progress", this);
469 class Window extends WindowBase {
471 return this.window.document.hasFocus();
474 isCurrentFor(context) {
475 // In GeckoView the popup is on a separate window so the current window for
476 // the popup is whatever is the topWindow.
477 // TODO: Bug 1651506 use context?.viewType === "popup" instead
478 if (context?.currentWindow?.moduleManager.settings.isPopup) {
479 return mobileWindowTracker.topWindow == this.window;
481 return super.isCurrentFor(context);
485 return this.window.screenY;
489 return this.window.screenX;
493 return this.window.outerWidth;
497 return this.window.outerHeight;
501 return PrivateBrowsingUtils.isWindowPrivate(this.window);
508 get isLastFocused() {
509 return this.window === windowTracker.topWindow;
517 yield this.activeTab;
520 *getHighlightedTabs() {
521 yield this.activeTab;
525 const { tabManager } = this.extension;
526 return tabManager.getWrapper(this.window.tab);
529 getTabAtIndex(index) {
531 return this.activeTab;
536 Object.assign(global, { Tab, TabContext, Window });
538 class TabManager extends TabManagerBase {
539 get(tabId, default_ = undefined) {
540 const nativeTab = tabTracker.getTab(tabId, default_);
543 return this.getWrapper(nativeTab);
548 addActiveTabPermission(nativeTab = tabTracker.activeTab) {
549 return super.addActiveTabPermission(nativeTab);
552 revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
553 return super.revokeActiveTabPermission(nativeTab);
556 canAccessTab(nativeTab) {
558 this.extension.privateBrowsingAllowed ||
559 !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser)
564 return new Tab(this.extension, nativeTab, nativeTab.id);
568 class WindowManager extends WindowManagerBase {
569 get(windowId, context) {
570 const window = windowTracker.getWindow(windowId, context);
572 return this.getWrapper(window);
576 for (const window of windowTracker.browserWindows()) {
577 if (!this.canAccessWindow(window, context)) {
580 const wrapped = this.getWrapper(window);
588 return new Window(this.extension, window, windowTracker.getId(window));
592 // eslint-disable-next-line mozilla/balanced-listeners
593 extensions.on("startup", (type, extension) => {
594 defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
598 () => new WindowManager(extension)
602 /* eslint-disable mozilla/balanced-listeners */
603 extensions.on("page-shutdown", (type, context) => {
604 if (context.viewType == "tab") {
605 const window = context.xulBrowser.ownerGlobal;
606 GeckoViewTabBridge.closeTab({
608 extensionId: context.extension.id,
612 /* eslint-enable mozilla/balanced-listeners */
614 global.openOptionsPage = async extension => {
615 const { options_ui } = extension.manifest;
616 const extensionId = extension.id;
618 if (options_ui.open_in_tab) {
619 // Delegate new tab creation and open the options page in the new tab.
620 const tab = await GeckoViewTabBridge.createNewTab({
623 url: options_ui.page,
628 const { browser } = tab;
629 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
631 browser.loadURI(options_ui.page, {
633 triggeringPrincipal: extension.principal,
636 const newWindow = browser.ownerGlobal;
637 mobileWindowTracker.setTabActive(newWindow, true);
641 // Delegate option page handling to the app.
642 return GeckoViewTabBridge.openOptionsPage(extensionId);