Bug 1708243 - Part 2: stop using sender data from the child process r=robwu,agi
[gecko.git] / mobile / android / components / extensions / ext-android.js
blob3fff6710193d8920e6c63e9e8dc3e37b93352d42
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";
6 ChromeUtils.defineModuleGetter(
7   this,
8   "PrivateBrowsingUtils",
9   "resource://gre/modules/PrivateBrowsingUtils.jsm"
12 ChromeUtils.defineModuleGetter(
13   this,
14   "GeckoViewTabBridge",
15   "resource://gre/modules/GeckoViewTab.jsm"
18 ChromeUtils.defineModuleGetter(
19   this,
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",
43   "nsIWebProgress",
44   "addProgressListener"
47 const WINDOW_TYPE = "navigator:geckoview";
49 // We need let to break cyclic dependency
50 /* eslint-disable-next-line prefer-const */
51 let windowTracker;
53 /**
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.
57  *
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.
64  */
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);
71   }
73   /**
74    * Destroy the listener, and perform any necessary cleanup.
75    */
76   destroy() {
77     this.browser.removeProgressListener(this.filter);
78     this.filter.removeProgressListener(this);
79   }
81   /**
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`.
85    *
86    * @param {string} method
87    *        The name of the nsIWebProgressListener method which is being
88    *        delegated.
89    * @param {*} args
90    *        The arguments to pass to the delegated listener.
91    * @private
92    */
93   delegate(method, ...args) {
94     if (this.listener[method]) {
95       this.listener[method](this.browser, ...args);
96     }
97   }
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
102     // here
103     if (!windowTracker.isBrowserWindow(window)) {
104       return;
105     }
107     this.delegate("onLocationChange", webProgress, request, locationURI, flags);
108   }
109   onStateChange(webProgress, request, stateFlags, status) {
110     this.delegate("onStateChange", webProgress, request, stateFlags, status);
111   }
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(
120       window.browser,
121       listener,
122       PROGRESS_LISTENER_FLAGS
123     );
124   }
126   destroy() {
127     this.listener.destroy();
128   }
131 class WindowTracker extends WindowTrackerBase {
132   constructor(...args) {
133     super(...args);
135     this.progressListeners = new DefaultWeakMap(() => new WeakMap());
136   }
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;
144     }
145     return super.getCurrentWindow(context);
146   }
148   get topWindow() {
149     return mobileWindowTracker.topWindow;
150   }
152   get topNonPBWindow() {
153     return mobileWindowTracker.topNonPBWindow;
154   }
156   isBrowserWindow(window) {
157     const { documentElement } = window.document;
158     return documentElement.getAttribute("windowtype") === WINDOW_TYPE;
159   }
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);
166     }
167   }
169   removeProgressListener(window, listener) {
170     const listeners = this.progressListeners.get(window);
171     const wrapper = listeners.get(listener);
172     if (wrapper) {
173       wrapper.destroy();
174       listeners.delete(listener);
175     }
176   }
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
194  *        recieved.
196  * @returns {object} An injectable api for the new event.
197  */
198 global.makeGlobalEvent = function makeGlobalEvent(
199   context,
200   name,
201   event,
202   listener
203 ) {
204   return new EventManager({
205     context,
206     name,
207     register: fire => {
208       const listener2 = {
209         onEvent(event, data, callback) {
210           listener(fire, data);
211         },
212       };
214       GlobalEventDispatcher.registerListener(listener2, [event]);
215       return () => {
216         GlobalEventDispatcher.unregisterListener(listener2, [event]);
217       };
218     },
219   }).api();
222 class TabTracker extends TabTrackerBase {
223   init() {
224     if (this.initialized) {
225       return;
226     }
227     this.initialized = true;
229     windowTracker.addOpenListener(window => {
230       const nativeTab = window.tab;
231       this.emit("tab-created", { nativeTab });
232     });
234     windowTracker.addCloseListener(window => {
235       const { tab, browser } = window;
236       const { windowId, tabId } = this.getBrowserData(browser);
237       this.emit("tab-removed", {
238         tab,
239         tabId,
240         windowId,
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,
245       });
246     });
247   }
249   getId(nativeTab) {
250     return nativeTab.id;
251   }
253   getTab(id, default_ = undefined) {
254     const windowId = GeckoViewTabBridge.tabIdToWindowId(id);
255     const window = windowTracker.getWindow(windowId, null, false);
257     if (window) {
258       const { tab } = window;
259       if (tab) {
260         return tab;
261       }
262     }
264     if (default_ !== undefined) {
265       return default_;
266     }
267     throw new ExtensionError(`Invalid tab ID: ${id}`);
268   }
270   getBrowserData(browser) {
271     const window = browser.ownerGlobal;
272     const { tab } = window;
273     if (!tab) {
274       return {
275         tabId: -1,
276         windowId: -1,
277       };
278     }
280     const windowId = windowTracker.getId(window);
282     if (!windowTracker.isBrowserWindow(window)) {
283       return {
284         windowId,
285         tabId: -1,
286       };
287     }
289     return {
290       windowId,
291       tabId: this.getId(tab),
292     };
293   }
295   get activeTab() {
296     const window = windowTracker.topWindow;
297     if (window) {
298       return window.tab;
299     }
300     return null;
301   }
304 windowTracker = new WindowTracker();
305 const tabTracker = new TabTracker();
307 Object.assign(global, { tabTracker, windowTracker });
309 class Tab extends TabBase {
310   get _favIconUrl() {
311     return undefined;
312   }
314   get attention() {
315     return false;
316   }
318   get audible() {
319     return this.nativeTab.playingAudio;
320   }
322   get browser() {
323     return this.nativeTab.browser;
324   }
326   get discarded() {
327     return this.browser.getAttribute("pending") === "true";
328   }
330   get cookieStoreId() {
331     return getCookieStoreIdForTab(this, this.nativeTab);
332   }
334   get height() {
335     return this.browser.clientHeight;
336   }
338   get incognito() {
339     return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
340   }
342   get index() {
343     return 0;
344   }
346   get mutedInfo() {
347     return { muted: false };
348   }
350   get lastAccessed() {
351     return this.nativeTab.lastTouchedAt;
352   }
354   get pinned() {
355     return false;
356   }
358   get active() {
359     return this.nativeTab.getActive();
360   }
362   get highlighted() {
363     return this.active;
364   }
366   get selected() {
367     return this.nativeTab.getActive();
368   }
370   get status() {
371     if (this.browser.webProgress.isLoadingDocument) {
372       return "loading";
373     }
374     return "complete";
375   }
377   get successorTabId() {
378     return -1;
379   }
381   get width() {
382     return this.browser.clientWidth;
383   }
385   get window() {
386     return this.browser.ownerGlobal;
387   }
389   get windowId() {
390     return windowTracker.getId(this.window);
391   }
393   // TODO: Just return false for these until properly implemented on Android.
394   // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
395   get isArticle() {
396     return false;
397   }
399   get isInReaderMode() {
400     return false;
401   }
403   get hidden() {
404     return false;
405   }
407   get sharingState() {
408     return {
409       screen: undefined,
410       microphone: false,
411       camera: false,
412     };
413   }
416 // Manages tab-specific context data and dispatches tab select and close events.
417 class TabContext extends EventEmitter {
418   constructor(getDefaultPrototype) {
419     super();
421     windowTracker.addListener("progress", this);
423     this.getDefaultPrototype = getDefaultPrototype;
424     this.tabData = new Map();
425   }
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).
432       return;
433     }
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
438     );
439     this.emit(
440       "location-change",
441       {
442         id: tab.id,
443         linkedBrowser: browser,
444         // TODO: we don't support selected so we just alway say we are
445         selected: true,
446       },
447       fromBrowse
448     );
449   }
451   get(tabId) {
452     if (!this.tabData.has(tabId)) {
453       const data = Object.create(this.getDefaultPrototype(tabId));
454       this.tabData.set(tabId, data);
455     }
457     return this.tabData.get(tabId);
458   }
460   clear(tabId) {
461     this.tabData.delete(tabId);
462   }
464   shutdown() {
465     windowTracker.removeListener("progress", this);
466   }
469 class Window extends WindowBase {
470   get focused() {
471     return this.window.document.hasFocus();
472   }
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;
480     }
481     return super.isCurrentFor(context);
482   }
484   get top() {
485     return this.window.screenY;
486   }
488   get left() {
489     return this.window.screenX;
490   }
492   get width() {
493     return this.window.outerWidth;
494   }
496   get height() {
497     return this.window.outerHeight;
498   }
500   get incognito() {
501     return PrivateBrowsingUtils.isWindowPrivate(this.window);
502   }
504   get alwaysOnTop() {
505     return false;
506   }
508   get isLastFocused() {
509     return this.window === windowTracker.topWindow;
510   }
512   get state() {
513     return "fullscreen";
514   }
516   *getTabs() {
517     yield this.activeTab;
518   }
520   *getHighlightedTabs() {
521     yield this.activeTab;
522   }
524   get activeTab() {
525     const { tabManager } = this.extension;
526     return tabManager.getWrapper(this.window.tab);
527   }
529   getTabAtIndex(index) {
530     if (index == 0) {
531       return this.activeTab;
532     }
533   }
536 Object.assign(global, { Tab, TabContext, Window });
538 class TabManager extends TabManagerBase {
539   get(tabId, default_ = undefined) {
540     const nativeTab = tabTracker.getTab(tabId, default_);
542     if (nativeTab) {
543       return this.getWrapper(nativeTab);
544     }
545     return default_;
546   }
548   addActiveTabPermission(nativeTab = tabTracker.activeTab) {
549     return super.addActiveTabPermission(nativeTab);
550   }
552   revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
553     return super.revokeActiveTabPermission(nativeTab);
554   }
556   canAccessTab(nativeTab) {
557     return (
558       this.extension.privateBrowsingAllowed ||
559       !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser)
560     );
561   }
563   wrapTab(nativeTab) {
564     return new Tab(this.extension, nativeTab, nativeTab.id);
565   }
568 class WindowManager extends WindowManagerBase {
569   get(windowId, context) {
570     const window = windowTracker.getWindow(windowId, context);
572     return this.getWrapper(window);
573   }
575   *getAll(context) {
576     for (const window of windowTracker.browserWindows()) {
577       if (!this.canAccessWindow(window, context)) {
578         continue;
579       }
580       const wrapped = this.getWrapper(window);
581       if (wrapped) {
582         yield wrapped;
583       }
584     }
585   }
587   wrapWindow(window) {
588     return new Window(this.extension, window, windowTracker.getId(window));
589   }
592 // eslint-disable-next-line mozilla/balanced-listeners
593 extensions.on("startup", (type, extension) => {
594   defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
595   defineLazyGetter(
596     extension,
597     "windowManager",
598     () => new WindowManager(extension)
599   );
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({
607       window,
608       extensionId: context.extension.id,
609     });
610   }
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({
621       extensionId,
622       createProperties: {
623         url: options_ui.page,
624         active: true,
625       },
626     });
628     const { browser } = tab;
629     const flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
631     browser.loadURI(options_ui.page, {
632       flags,
633       triggeringPrincipal: extension.principal,
634     });
636     const newWindow = browser.ownerGlobal;
637     mobileWindowTracker.setTabActive(newWindow, true);
638     return;
639   }
641   // Delegate option page handling to the app.
642   return GeckoViewTabBridge.openOptionsPage(extensionId);