Bug 1890689 accumulate input in LargerReceiverBlockSizeThanDesiredBuffering GTest...
[gecko.git] / remote / shared / TabManager.sys.mjs
blobc7170f8810078a4516d441057bb095d665f3e2ef
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
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
9   BrowsingContextListener:
10     "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
11   EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
12   generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
13   MobileTabBrowser: "chrome://remote/content/shared/MobileTabBrowser.sys.mjs",
14   UserContextManager:
15     "chrome://remote/content/shared/UserContextManager.sys.mjs",
16 });
18 class TabManagerClass {
19   #browserUniqueIds;
20   #contextListener;
21   #navigableIds;
23   constructor() {
24     // Maps browser's permanentKey to uuid: WeakMap.<Object, string>
25     this.#browserUniqueIds = new WeakMap();
27     // Maps browsing contexts to uuid: WeakMap.<BrowsingContext, string>.
28     // It's required as a fallback, since in the case when a context was discarded
29     // embedderElement is gone, and we cannot retrieve
30     // the context id from this.#browserUniqueIds.
31     this.#navigableIds = new WeakMap();
33     this.#contextListener = new lazy.BrowsingContextListener();
34     this.#contextListener.on("attached", this.#onContextAttached);
35     this.#contextListener.startListening();
37     this.browsers.forEach(browser => {
38       if (this.isValidCanonicalBrowsingContext(browser.browsingContext)) {
39         this.#navigableIds.set(
40           browser.browsingContext,
41           this.getIdForBrowsingContext(browser.browsingContext)
42         );
43       }
44     });
45   }
47   /**
48    * Retrieve all the browser elements from tabs as contained in open windows.
49    *
50    * @returns {Array<XULBrowser>}
51    *     All the found <xul:browser>s. Will return an empty array if
52    *     no windows and tabs can be found.
53    */
54   get browsers() {
55     const browsers = [];
57     for (const win of this.windows) {
58       for (const tab of this.getTabsForWindow(win)) {
59         const contentBrowser = this.getBrowserForTab(tab);
60         if (contentBrowser !== null) {
61           browsers.push(contentBrowser);
62         }
63       }
64     }
66     return browsers;
67   }
69   get windows() {
70     return Services.wm.getEnumerator(null);
71   }
73   /**
74    * Array of unique browser ids (UUIDs) for all content browsers of all
75    * windows.
76    *
77    * TODO: Similarly to getBrowserById, we should improve the performance of
78    * this getter in Bug 1750065.
79    *
80    * @returns {Array<string>}
81    *     Array of UUIDs for all content browsers.
82    */
83   get allBrowserUniqueIds() {
84     const browserIds = [];
86     for (const win of this.windows) {
87       // Only return handles for browser windows
88       for (const tab of this.getTabsForWindow(win)) {
89         const contentBrowser = this.getBrowserForTab(tab);
90         const winId = this.getIdForBrowser(contentBrowser);
91         if (winId !== null) {
92           browserIds.push(winId);
93         }
94       }
95     }
97     return browserIds;
98   }
100   /**
101    * Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
102    *
103    * @param {Tab} tab
104    *     The tab whose browser needs to be returned.
105    *
106    * @returns {XULBrowser}
107    *     The linked browser for the tab or null if no browser can be found.
108    */
109   getBrowserForTab(tab) {
110     if (tab && "linkedBrowser" in tab) {
111       return tab.linkedBrowser;
112     }
114     return null;
115   }
117   /**
118    * Return the tab browser for the specified chrome window.
119    *
120    * @param {ChromeWindow} win
121    *     Window whose <code>tabbrowser</code> needs to be accessed.
122    *
123    * @returns {Tab}
124    *     Tab browser or null if it's not a browser window.
125    */
126   getTabBrowser(win) {
127     if (lazy.AppInfo.isAndroid) {
128       return new lazy.MobileTabBrowser(win);
129     } else if (lazy.AppInfo.isFirefox) {
130       return win.gBrowser;
131     }
133     return null;
134   }
136   /**
137    * Create a new tab.
138    *
139    * @param {object} options
140    * @param {boolean=} options.focus
141    *     Set to true if the new tab should be focused (selected). Defaults to
142    *     false. `false` value is not properly supported on Android, additional
143    *     focus of previously selected tab is required after initial navigation.
144    * @param {Tab=} options.referenceTab
145    *     The reference tab after which the new tab will be added. If no
146    *     reference tab is provided, the new tab will be added after all the
147    *     other tabs.
148    * @param {string=} options.userContextId
149    *     A user context id from UserContextManager.
150    * @param {window=} options.window
151    *     The window where the new tab will open. Defaults to Services.wm.getMostRecentWindow
152    *     if no window is provided. Will be ignored if referenceTab is provided.
153    */
154   async addTab(options = {}) {
155     let {
156       focus = false,
157       referenceTab = null,
158       userContextId = null,
159       window = Services.wm.getMostRecentWindow(null),
160     } = options;
162     let index;
163     if (referenceTab != null) {
164       // If a reference tab was specified, the window should be the window
165       // owning the reference tab.
166       window = this.getWindowForTab(referenceTab);
167     }
169     if (referenceTab != null) {
170       index = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
171     }
173     const tabBrowser = this.getTabBrowser(window);
175     const tab = await tabBrowser.addTab("about:blank", {
176       index,
177       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
178       userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
179     });
181     if (focus) {
182       await this.selectTab(tab);
183     }
185     return tab;
186   }
188   /**
189    * Retrieve the browser element corresponding to the provided unique id,
190    * previously generated via getIdForBrowser.
191    *
192    * TODO: To avoid creating strong references on browser elements and
193    * potentially leaking those elements, this method loops over all windows and
194    * all tabs. It should be replaced by a faster implementation in Bug 1750065.
195    *
196    * @param {string} id
197    *     A browser unique id created by getIdForBrowser.
198    * @returns {XULBrowser}
199    *     The <xul:browser> corresponding to the provided id. Will return null if
200    *     no matching browser element is found.
201    */
202   getBrowserById(id) {
203     for (const win of this.windows) {
204       for (const tab of this.getTabsForWindow(win)) {
205         const contentBrowser = this.getBrowserForTab(tab);
206         if (this.getIdForBrowser(contentBrowser) == id) {
207           return contentBrowser;
208         }
209       }
210     }
211     return null;
212   }
214   /**
215    * Retrieve the browsing context corresponding to the provided unique id.
216    *
217    * @param {string} id
218    *     A browsing context unique id (created by getIdForBrowsingContext).
219    * @returns {BrowsingContext=}
220    *     The browsing context found for this id, null if none was found.
221    */
222   getBrowsingContextById(id) {
223     const browser = this.getBrowserById(id);
224     if (browser) {
225       return browser.browsingContext;
226     }
228     return BrowsingContext.get(id);
229   }
231   /**
232    * Retrieve the unique id for the given xul browser element. The id is a
233    * dynamically generated uuid associated with the permanentKey property of the
234    * given browser element. This method is preferable over getIdForBrowsingContext
235    * in case of working with browser element of a tab, since we can not guarantee
236    * that browsing context is attached to it.
237    *
238    * @param {XULBrowser} browserElement
239    *     The <xul:browser> for which we want to retrieve the id.
240    * @returns {string} The unique id for this browser.
241    */
242   getIdForBrowser(browserElement) {
243     if (browserElement === null) {
244       return null;
245     }
247     const key = browserElement.permanentKey;
248     if (key === undefined) {
249       return null;
250     }
252     if (!this.#browserUniqueIds.has(key)) {
253       this.#browserUniqueIds.set(key, lazy.generateUUID());
254     }
255     return this.#browserUniqueIds.get(key);
256   }
258   /**
259    * Retrieve the id of a Browsing Context.
260    *
261    * For a top-level browsing context a custom unique id will be returned.
262    *
263    * @param {BrowsingContext=} browsingContext
264    *     The browsing context to get the id from.
265    *
266    * @returns {string}
267    *     The id of the browsing context.
268    */
269   getIdForBrowsingContext(browsingContext) {
270     if (!browsingContext) {
271       return null;
272     }
274     if (!browsingContext.parent) {
275       // Top-level browsing contexts have their own custom unique id.
276       // If a context was discarded, embedderElement is already gone,
277       // so use navigable id instead.
278       return browsingContext.embedderElement
279         ? this.getIdForBrowser(browsingContext.embedderElement)
280         : this.#navigableIds.get(browsingContext);
281     }
283     return browsingContext.id.toString();
284   }
286   /**
287    * Get the navigable for the given browsing context.
288    *
289    * Because Gecko doesn't support the Navigable concept in content
290    * scope the content browser could be used to uniquely identify
291    * top-level browsing contexts.
292    *
293    * @param {BrowsingContext} browsingContext
294    *
295    * @returns {BrowsingContext|XULBrowser} The navigable
296    *
297    * @throws {TypeError}
298    *     If `browsingContext` is not a CanonicalBrowsingContext instance.
299    */
300   getNavigableForBrowsingContext(browsingContext) {
301     if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
302       throw new TypeError(
303         `Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
304       );
305     }
307     if (browsingContext.isContent && browsingContext.parent === null) {
308       return browsingContext.embedderElement;
309     }
311     return browsingContext;
312   }
314   getTabCount() {
315     let count = 0;
316     for (const win of this.windows) {
317       // For browser windows count the tabs. Otherwise take the window itself.
318       const tabsLength = this.getTabsForWindow(win).length;
319       count += tabsLength ? tabsLength : 1;
320     }
321     return count;
322   }
324   /**
325    * Retrieve the tab owning a Browsing Context.
326    *
327    * @param {BrowsingContext=} browsingContext
328    *     The browsing context to get the tab from.
329    *
330    * @returns {Tab|null}
331    *     The tab owning the Browsing Context.
332    */
333   getTabForBrowsingContext(browsingContext) {
334     const browser = browsingContext?.top.embedderElement;
335     if (!browser) {
336       return null;
337     }
339     const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
340     return tabBrowser.getTabForBrowser(browser);
341   }
343   /**
344    * Retrieve the list of tabs for a given window.
345    *
346    * @param {ChromeWindow} win
347    *     Window whose <code>tabs</code> need to be returned.
348    *
349    * @returns {Array<Tab>}
350    *     The list of tabs. Will return an empty list if tab browser is not available
351    *     or tabs are undefined.
352    */
353   getTabsForWindow(win) {
354     const tabBrowser = this.getTabBrowser(win);
355     // For web-platform reftests a faked tabbrowser is used,
356     // which does not actually have tabs.
357     if (tabBrowser && tabBrowser.tabs) {
358       return tabBrowser.tabs;
359     }
360     return [];
361   }
363   getWindowForTab(tab) {
364     // `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
365     // Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
366     // of the platforms.
367     return tab.linkedBrowser.ownerGlobal;
368   }
370   /**
371    * Check if the given argument is a valid canonical browsing context and was not
372    * discarded.
373    *
374    * @param {BrowsingContext} browsingContext
375    *     The browsing context to check.
376    *
377    * @returns {boolean}
378    *     True if the browsing context is valid, false otherwise.
379    */
380   isValidCanonicalBrowsingContext(browsingContext) {
381     return (
382       CanonicalBrowsingContext.isInstance(browsingContext) &&
383       !browsingContext.isDiscarded
384     );
385   }
387   /**
388    * Remove the given tab.
389    *
390    * @param {Tab} tab
391    *     Tab to remove.
392    */
393   async removeTab(tab) {
394     if (!tab) {
395       return;
396     }
398     const ownerWindow = this.getWindowForTab(tab);
399     const tabBrowser = this.getTabBrowser(ownerWindow);
400     await tabBrowser.removeTab(tab);
401   }
403   /**
404    * Select the given tab.
405    *
406    * @param {Tab} tab
407    *     Tab to select.
408    *
409    * @returns {Promise}
410    *     Promise that resolves when the given tab has been selected.
411    */
412   async selectTab(tab) {
413     if (!tab) {
414       return Promise.resolve();
415     }
417     const ownerWindow = this.getWindowForTab(tab);
418     const tabBrowser = this.getTabBrowser(ownerWindow);
420     if (tab === tabBrowser.selectedTab) {
421       return Promise.resolve();
422     }
424     const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
425     tabBrowser.selectedTab = tab;
427     await selected;
429     // Sometimes at that point window is not focused.
430     if (Services.focus.activeWindow != ownerWindow) {
431       const activated = new lazy.EventPromise(ownerWindow, "activate");
432       ownerWindow.focus();
433       return activated;
434     }
436     return Promise.resolve();
437   }
439   supportsTabs() {
440     return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
441   }
443   #onContextAttached = (eventName, data = {}) => {
444     const { browsingContext } = data;
445     if (this.isValidCanonicalBrowsingContext(browsingContext)) {
446       this.#navigableIds.set(
447         browsingContext,
448         this.getIdForBrowsingContext(browsingContext)
449       );
450     }
451   };
454 // Expose a shared singleton.
455 export const TabManager = new TabManagerClass();