no bug - Import translations from android-l10n r=release a=l10n CLOSED TREE
[gecko.git] / mobile / android / components / extensions / ext-android.js
blob2576a42ba57079362e994106c082d92287a685ac
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 /**
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  */
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 });
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",
34   "nsIWebProgress",
35   "addProgressListener"
38 const WINDOW_TYPE = "navigator:geckoview";
40 // We need let to break cyclic dependency
41 /* eslint-disable-next-line prefer-const */
42 let windowTracker;
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   }
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   }
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   }
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     }
98     this.delegate("onLocationChange", webProgress, request, locationURI, flags);
99   }
100   onStateChange(webProgress, request, stateFlags, status) {
101     this.delegate("onStateChange", webProgress, request, stateFlags, status);
102   }
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(
111       window.browser,
112       listener,
113       PROGRESS_LISTENER_FLAGS
114     );
115   }
117   destroy() {
118     this.listener.destroy();
119   }
122 class WindowTracker extends WindowTrackerBase {
123   constructor(...args) {
124     super(...args);
126     this.progressListeners = new DefaultWeakMap(() => new WeakMap());
127   }
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;
135     }
136     return super.getCurrentWindow(context);
137   }
139   get topWindow() {
140     return mobileWindowTracker.topWindow;
141   }
143   get topNonPBWindow() {
144     return mobileWindowTracker.topNonPBWindow;
145   }
147   isBrowserWindow(window) {
148     const { documentElement } = window.document;
149     return documentElement.getAttribute("windowtype") === WINDOW_TYPE;
150   }
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);
157     }
158   }
160   removeProgressListener(window, listener) {
161     const listeners = this.progressListeners.get(window);
162     const wrapper = listeners.get(listener);
163     if (wrapper) {
164       wrapper.destroy();
165       listeners.delete(listener);
166     }
167   }
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
185  *        recieved.
187  * @returns {object} An injectable api for the new event.
188  */
189 global.makeGlobalEvent = function makeGlobalEvent(
190   context,
191   name,
192   event,
193   listener
194 ) {
195   return new EventManager({
196     context,
197     name,
198     register: fire => {
199       const listener2 = {
200         onEvent(event, data) {
201           listener(fire, data);
202         },
203       };
205       EventDispatcher.instance.registerListener(listener2, [event]);
206       return () => {
207         EventDispatcher.instance.unregisterListener(listener2, [event]);
208       };
209     },
210   }).api();
213 class TabTracker extends TabTrackerBase {
214   init() {
215     if (this.initialized) {
216       return;
217     }
218     this.initialized = true;
220     windowTracker.addOpenListener(window => {
221       const nativeTab = window.tab;
222       this.emit("tab-created", { nativeTab });
223     });
225     windowTracker.addCloseListener(window => {
226       const { tab: nativeTab, browser } = window;
227       const { windowId, tabId } = this.getBrowserData(browser);
228       this.emit("tab-removed", {
229         nativeTab,
230         tabId,
231         windowId,
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,
236       });
237     });
238   }
240   getId(nativeTab) {
241     return nativeTab.id;
242   }
244   getTab(id, default_ = undefined) {
245     const windowId = GeckoViewTabBridge.tabIdToWindowId(id);
246     const window = windowTracker.getWindow(windowId, null, false);
248     if (window) {
249       const { tab } = window;
250       if (tab) {
251         return tab;
252       }
253     }
255     if (default_ !== undefined) {
256       return default_;
257     }
258     throw new ExtensionError(`Invalid tab ID: ${id}`);
259   }
261   getBrowserData(browser) {
262     const window = browser.ownerGlobal;
263     const tab = window?.tab;
264     if (!tab) {
265       return {
266         tabId: -1,
267         windowId: -1,
268       };
269     }
271     const windowId = windowTracker.getId(window);
273     if (!windowTracker.isBrowserWindow(window)) {
274       return {
275         windowId,
276         tabId: -1,
277       };
278     }
280     return {
281       windowId,
282       tabId: this.getId(tab),
283     };
284   }
286   get activeTab() {
287     const window = windowTracker.topWindow;
288     if (window) {
289       return window.tab;
290     }
291     return null;
292   }
295 windowTracker = new WindowTracker();
296 const tabTracker = new TabTracker();
298 Object.assign(global, { tabTracker, windowTracker });
300 class Tab extends TabBase {
301   get _favIconUrl() {
302     return undefined;
303   }
305   get attention() {
306     return false;
307   }
309   get audible() {
310     return this.nativeTab.playingAudio;
311   }
313   get browser() {
314     return this.nativeTab.browser;
315   }
317   get discarded() {
318     return this.browser.getAttribute("pending") === "true";
319   }
321   get cookieStoreId() {
322     return getCookieStoreIdForTab(this, this.nativeTab);
323   }
325   get height() {
326     return this.browser.clientHeight;
327   }
329   get incognito() {
330     return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
331   }
333   get index() {
334     return 0;
335   }
337   get mutedInfo() {
338     return { muted: false };
339   }
341   get lastAccessed() {
342     return this.nativeTab.lastTouchedAt;
343   }
345   get pinned() {
346     return false;
347   }
349   get active() {
350     return this.nativeTab.getActive();
351   }
353   get highlighted() {
354     return this.active;
355   }
357   get status() {
358     if (this.browser.webProgress.isLoadingDocument) {
359       return "loading";
360     }
361     return "complete";
362   }
364   get successorTabId() {
365     return -1;
366   }
368   get width() {
369     return this.browser.clientWidth;
370   }
372   get window() {
373     return this.browser.ownerGlobal;
374   }
376   get windowId() {
377     return windowTracker.getId(this.window);
378   }
380   // TODO: Just return false for these until properly implemented on Android.
381   // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
382   get isArticle() {
383     return false;
384   }
386   get isInReaderMode() {
387     return false;
388   }
390   get hidden() {
391     return false;
392   }
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.
397     return true;
398   }
400   get sharingState() {
401     return {
402       screen: undefined,
403       microphone: false,
404       camera: false,
405     };
406   }
409 // Manages tab-specific context data and dispatches tab select and close events.
410 class TabContext extends EventEmitter {
411   constructor(getDefaultPrototype) {
412     super();
414     windowTracker.addListener("progress", this);
416     this.getDefaultPrototype = getDefaultPrototype;
417     this.tabData = new Map();
418   }
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).
425       return;
426     }
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
431     );
432     this.emit(
433       "location-change",
434       {
435         id: tab.id,
436         linkedBrowser: browser,
437         // TODO: we don't support selected so we just alway say we are
438         selected: true,
439       },
440       fromBrowse
441     );
442   }
444   get(tabId) {
445     if (!this.tabData.has(tabId)) {
446       const data = Object.create(this.getDefaultPrototype(tabId));
447       this.tabData.set(tabId, data);
448     }
450     return this.tabData.get(tabId);
451   }
453   clear(tabId) {
454     this.tabData.delete(tabId);
455   }
457   shutdown() {
458     windowTracker.removeListener("progress", this);
459   }
462 class Window extends WindowBase {
463   get focused() {
464     return this.window.document.hasFocus();
465   }
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;
473     }
474     return super.isCurrentFor(context);
475   }
477   get top() {
478     return this.window.screenY;
479   }
481   get left() {
482     return this.window.screenX;
483   }
485   get width() {
486     return this.window.outerWidth;
487   }
489   get height() {
490     return this.window.outerHeight;
491   }
493   get incognito() {
494     return PrivateBrowsingUtils.isWindowPrivate(this.window);
495   }
497   get alwaysOnTop() {
498     return false;
499   }
501   get isLastFocused() {
502     return this.window === windowTracker.topWindow;
503   }
505   get state() {
506     return "fullscreen";
507   }
509   *getTabs() {
510     yield this.activeTab;
511   }
513   *getHighlightedTabs() {
514     yield this.activeTab;
515   }
517   get activeTab() {
518     const { tabManager } = this.extension;
519     return tabManager.getWrapper(this.window.tab);
520   }
522   getTabAtIndex(index) {
523     if (index == 0) {
524       return this.activeTab;
525     }
526   }
529 Object.assign(global, { Tab, TabContext, Window });
531 class TabManager extends TabManagerBase {
532   get(tabId, default_ = undefined) {
533     const nativeTab = tabTracker.getTab(tabId, default_);
535     if (nativeTab) {
536       return this.getWrapper(nativeTab);
537     }
538     return default_;
539   }
541   addActiveTabPermission(nativeTab = tabTracker.activeTab) {
542     return super.addActiveTabPermission(nativeTab);
543   }
545   revokeActiveTabPermission(nativeTab = tabTracker.activeTab) {
546     return super.revokeActiveTabPermission(nativeTab);
547   }
549   canAccessTab(nativeTab) {
550     return (
551       this.extension.privateBrowsingAllowed ||
552       !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser)
553     );
554   }
556   wrapTab(nativeTab) {
557     return new Tab(this.extension, nativeTab, nativeTab.id);
558   }
561 class WindowManager extends WindowManagerBase {
562   get(windowId, context) {
563     const window = windowTracker.getWindow(windowId, context);
565     return this.getWrapper(window);
566   }
568   *getAll(context) {
569     for (const window of windowTracker.browserWindows()) {
570       if (!this.canAccessWindow(window, context)) {
571         continue;
572       }
573       const wrapped = this.getWrapper(window);
574       if (wrapped) {
575         yield wrapped;
576       }
577     }
578   }
580   wrapWindow(window) {
581     return new Window(this.extension, window, windowTracker.getId(window));
582   }
585 // eslint-disable-next-line mozilla/balanced-listeners
586 extensions.on("startup", (type, extension) => {
587   defineLazyGetter(extension, "tabManager", () => new TabManager(extension));
588   defineLazyGetter(
589     extension,
590     "windowManager",
591     () => new WindowManager(extension)
592   );
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.
602       return;
603     }
604     GeckoViewTabBridge.closeTab({
605       window,
606       extensionId: context.extension.id,
607     });
608   }
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({
619       extensionId,
620       createProperties: {
621         url: options_ui.page,
622         active: true,
623       },
624     });
626     const { browser } = tab;
627     const flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
629     browser.fixupAndLoadURIString(options_ui.page, {
630       flags,
631       triggeringPrincipal: extension.principal,
632     });
634     const newWindow = browser.ownerGlobal;
635     mobileWindowTracker.setTabActive(newWindow, true);
636     return;
637   }
639   // Delegate option page handling to the app.
640   return GeckoViewTabBridge.openOptionsPage(extensionId);