From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- mobile/android/chrome/geckoview/geckoview.js | 934 +++++++++++++++++++++++++++ 1 file changed, 934 insertions(+) create mode 100644 mobile/android/chrome/geckoview/geckoview.js (limited to 'mobile/android/chrome/geckoview/geckoview.js') diff --git a/mobile/android/chrome/geckoview/geckoview.js b/mobile/android/chrome/geckoview/geckoview.js new file mode 100644 index 0000000000..69ca5e413f --- /dev/null +++ b/mobile/android/chrome/geckoview/geckoview.js @@ -0,0 +1,934 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { DelayedInit } = ChromeUtils.import( + "resource://gre/modules/DelayedInit.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + Blocklist: "resource://gre/modules/Blocklist.sys.mjs", + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", + GeckoViewActorManager: "resource://gre/modules/GeckoViewActorManager.sys.mjs", + GeckoViewSettings: "resource://gre/modules/GeckoViewSettings.sys.mjs", + GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.sys.mjs", + RemoteSecuritySettings: + "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs", + SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + HistogramStopwatch: "resource://gre/modules/GeckoViewTelemetry.jsm", + InitializationTracker: "resource://gre/modules/GeckoViewTelemetry.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "WindowEventDispatcher", () => + EventDispatcher.for(window) +); + +XPCOMUtils.defineLazyScriptGetter( + this, + "PrintUtils", + "chrome://global/content/printUtils.js" +); + +// This file assumes `warn` and `debug` are imported into scope +// by the child scripts. +/* global debug, warn */ + +/** + * ModuleManager creates and manages GeckoView modules. Each GeckoView module + * normally consists of a JSM module file with an optional content module file. + * The module file contains a class that extends GeckoViewModule, and the + * content module file contains a class that extends GeckoViewChildModule. A + * module usually pairs with a particular GeckoSessionHandler or delegate on the + * Java side, and automatically receives module lifetime events such as + * initialization, change in enabled state, and change in settings. + */ +var ModuleManager = { + get _initData() { + return window.arguments[0].QueryInterface(Ci.nsIAndroidView).initData; + }, + + init(aBrowser, aModules) { + const MODULES_INIT_PROBE = new HistogramStopwatch( + "GV_STARTUP_MODULES_MS", + aBrowser + ); + + MODULES_INIT_PROBE.start(); + + const initData = this._initData; + this._browser = aBrowser; + this._settings = initData.settings; + this._frozenSettings = Object.freeze(Object.assign({}, this._settings)); + + const self = this; + this._modules = new Map( + (function* () { + for (const module of aModules) { + yield [ + module.name, + new ModuleInfo({ + enabled: !!initData.modules[module.name], + manager: self, + ...module, + }), + ]; + } + })() + ); + + window.document.documentElement.appendChild(aBrowser); + + // By default all layers are discarded when a browser is set to inactive. + // GeckoView by default sets browsers to inactive every time they're not + // visible. To avoid flickering when changing tabs, we preserve layers for + // all loaded tabs. + aBrowser.preserveLayers(true); + + WindowEventDispatcher.registerListener(this, [ + "GeckoView:UpdateModuleState", + "GeckoView:UpdateInitData", + "GeckoView:UpdateSettings", + ]); + + this.messageManager.addMessageListener( + "GeckoView:ContentModuleLoaded", + this + ); + + this._moduleByActorName = new Map(); + this.forEach(module => { + module.onInit(); + module.loadInitFrameScript(); + for (const actorName of module.actorNames) { + this._moduleByActorName[actorName] = module; + } + }); + + window.addEventListener("unload", () => { + this.forEach(module => { + module.enabled = false; + module.onDestroy(); + }); + + this._modules.clear(); + }); + + MODULES_INIT_PROBE.finish(); + }, + + onPrintWindow(aParams) { + if (!aParams.openWindowInfo.isForWindowDotPrint) { + return PrintUtils.handleStaticCloneCreatedForPrint( + aParams.openWindowInfo + ); + } + const printActor = this.window.moduleManager.getActor( + "GeckoViewPrintDelegate" + ); + // Prevents continually making new static browsers + if (printActor.browserStaticClone != null) { + throw new Error("A prior window.print is still in progress."); + } + const staticBrowser = PrintUtils.createParentBrowserForStaticClone( + aParams.openWindowInfo.parent, + aParams.openWindowInfo + ); + printActor.browserStaticClone = staticBrowser; + printActor.printRequest(); + return staticBrowser; + }, + + get window() { + return window; + }, + + get browser() { + return this._browser; + }, + + get messageManager() { + return this._browser.messageManager; + }, + + get eventDispatcher() { + return WindowEventDispatcher; + }, + + get settings() { + return this._frozenSettings; + }, + + forEach(aCallback) { + this._modules.forEach(aCallback, this); + }, + + getActor(aActorName) { + return this.browser.browsingContext.currentWindowGlobal?.getActor( + aActorName + ); + }, + + // Ensures that session history has been flushed before changing remoteness + async prepareToChangeRemoteness() { + // Session state like history is maintained at the process level so we need + // to collect it and restore it in the other process when switching. + // TODO: This should go away when we migrate the history to the main + // process Bug 1507287. + const { history } = await this.getActor("GeckoViewContent").collectState(); + + // Ignore scroll and form data since we're navigating away from this page + // anyway + this.sessionState = { history }; + }, + + willChangeBrowserRemoteness() { + debug`WillChangeBrowserRemoteness`; + + // Now we're switching the remoteness. + this.disabledModules = []; + this.forEach(module => { + if (module.enabled && module.disableOnProcessSwitch) { + module.enabled = false; + this.disabledModules.push(module); + } + }); + + this.forEach(module => { + module.onDestroyBrowser(); + }); + }, + + didChangeBrowserRemoteness() { + debug`DidChangeBrowserRemoteness`; + + this.forEach(module => { + if (module.impl) { + module.impl.onInitBrowser(); + } + }); + + this.messageManager.addMessageListener( + "GeckoView:ContentModuleLoaded", + this + ); + + this.forEach(module => { + // We're attaching a new browser so we have to reload the frame scripts + module.loadInitFrameScript(); + }); + + this.disabledModules.forEach(module => { + module.enabled = true; + }); + this.disabledModules = null; + }, + + afterBrowserRemotenessChange(aSwitchId) { + const { sessionState } = this; + this.sessionState = null; + + sessionState.switchId = aSwitchId; + + this.getActor("GeckoViewContent").restoreState(sessionState); + this.browser.focus(); + + // Load was handled + return true; + }, + + _updateSettings(aSettings) { + Object.assign(this._settings, aSettings); + this._frozenSettings = Object.freeze(Object.assign({}, this._settings)); + + const windowType = aSettings.isPopup + ? "navigator:popup" + : "navigator:geckoview"; + window.document.documentElement.setAttribute("windowtype", windowType); + + this.forEach(module => { + if (module.impl) { + module.impl.onSettingsUpdate(); + } + }); + }, + + onMessageFromActor(aActorName, aMessage) { + this._moduleByActorName[aActorName].receiveMessage(aMessage); + }, + + onEvent(aEvent, aData, aCallback) { + debug`onEvent ${aEvent} ${aData}`; + switch (aEvent) { + case "GeckoView:UpdateModuleState": { + const module = this._modules.get(aData.module); + if (module) { + module.enabled = aData.enabled; + } + break; + } + + case "GeckoView:UpdateInitData": { + // Replace all settings during a transfer. + const initData = this._initData; + this._updateSettings(initData.settings); + + // Update module enabled states. + for (const name in initData.modules) { + const module = this._modules.get(name); + if (module) { + module.enabled = initData.modules[name]; + } + } + + // Notify child of the transfer. + this._browser.messageManager.sendAsyncMessage(aEvent); + break; + } + + case "GeckoView:UpdateSettings": { + this._updateSettings(aData); + break; + } + } + }, + + receiveMessage(aMsg) { + debug`receiveMessage ${aMsg.name} ${aMsg.data}`; + switch (aMsg.name) { + case "GeckoView:ContentModuleLoaded": { + const module = this._modules.get(aMsg.data.module); + if (module) { + module.onContentModuleLoaded(); + } + break; + } + } + }, +}; + +/** + * ModuleInfo is the structure used by ModuleManager to represent individual + * modules. It is responsible for loading the module JSM file if necessary, + * and it acts as the intermediary between ModuleManager and the module + * object that extends GeckoViewModule. + */ +class ModuleInfo { + /** + * Create a ModuleInfo instance. See _loadPhase for phase object description. + * + * @param manager the ModuleManager instance. + * @param name Name of the module. + * @param enabled Enabled state of the module at startup. + * @param onInit Phase object for the init phase, when the window is created. + * @param onEnable Phase object for the enable phase, when the module is first + * enabled by setting a delegate in Java. + */ + constructor({ manager, name, enabled, onInit, onEnable }) { + this._manager = manager; + this._name = name; + + // We don't support having more than one main process script, so let's + // check that we're not accidentally defining two. We could support this if + // needed by making _impl an array for each phase impl. + if (onInit?.resource !== undefined && onEnable?.resource !== undefined) { + throw new Error( + "Only one main process script is allowed for each module." + ); + } + + this._impl = null; + this._contentModuleLoaded = false; + this._enabled = false; + // Only enable once we performed initialization. + this._enabledOnInit = enabled; + + // For init, load resource _before_ initializing browser to support the + // onInitBrowser() override. However, load content module after initializing + // browser, because we don't have a message manager before then. + this._loadResource(onInit); + this._loadActors(onInit); + if (this._enabledOnInit) { + this._loadActors(onEnable); + } + + this._onInitPhase = onInit; + this._onEnablePhase = onEnable; + + const actorNames = []; + if (this._onInitPhase?.actors) { + actorNames.push(Object.keys(this._onInitPhase.actors)); + } + if (this._onEnablePhase?.actors) { + actorNames.push(Object.keys(this._onEnablePhase.actors)); + } + this._actorNames = Object.freeze(actorNames); + } + + get actorNames() { + return this._actorNames; + } + + onInit() { + if (this._impl) { + this._impl.onInit(); + this._impl.onSettingsUpdate(); + } + + this.enabled = this._enabledOnInit; + } + + /** + * Loads the onInit frame script + */ + loadInitFrameScript() { + this._loadFrameScript(this._onInitPhase); + } + + onDestroy() { + if (this._impl) { + this._impl.onDestroy(); + } + } + + /** + * Called before the browser is removed + */ + onDestroyBrowser() { + if (this._impl) { + this._impl.onDestroyBrowser(); + } + this._contentModuleLoaded = false; + } + + _loadActors(aPhase) { + if (!aPhase || !aPhase.actors) { + return; + } + + GeckoViewActorManager.addJSWindowActors(aPhase.actors); + } + + /** + * Load resource according to a phase object that contains possible keys, + * + * "resource": specify the JSM resource to load for this module. + * "frameScript": specify a content JS frame script to load for this module. + */ + _loadResource(aPhase) { + if (!aPhase || !aPhase.resource || this._impl) { + return; + } + + const exports = ChromeUtils.importESModule(aPhase.resource); + this._impl = new exports[this._name](this); + } + + /** + * Load frameScript according to a phase object that contains possible keys, + * + * "frameScript": specify a content JS frame script to load for this module. + */ + _loadFrameScript(aPhase) { + if (!aPhase || !aPhase.frameScript || this._contentModuleLoaded) { + return; + } + + if (this._impl) { + this._impl.onLoadContentModule(); + } + this._manager.messageManager.loadFrameScript(aPhase.frameScript, true); + this._contentModuleLoaded = true; + } + + get manager() { + return this._manager; + } + + get disableOnProcessSwitch() { + // Only disable while process switching if it has a frameScript + return ( + !!this._onInitPhase?.frameScript || !!this._onEnablePhase?.frameScript + ); + } + + get name() { + return this._name; + } + + get impl() { + return this._impl; + } + + get enabled() { + return this._enabled; + } + + set enabled(aEnabled) { + if (aEnabled === this._enabled) { + return; + } + + if (!aEnabled && this._impl) { + this._impl.onDisable(); + } + + this._enabled = aEnabled; + + if (aEnabled) { + this._loadResource(this._onEnablePhase); + this._loadFrameScript(this._onEnablePhase); + this._loadActors(this._onEnablePhase); + if (this._impl) { + this._impl.onEnable(); + this._impl.onSettingsUpdate(); + } + } + + this._updateContentModuleState(); + } + + receiveMessage(aMessage) { + if (!this._impl) { + throw new Error(`No impl for message: ${aMessage.name}.`); + } + + try { + this._impl.receiveMessage(aMessage); + } catch (error) { + warn`this._impl.receiveMessage failed ${aMessage.name}`; + throw error; + } + } + + onContentModuleLoaded() { + this._updateContentModuleState(); + + if (this._impl) { + this._impl.onContentModuleLoaded(); + } + } + + _updateContentModuleState() { + this._manager.messageManager.sendAsyncMessage( + "GeckoView:UpdateModuleState", + { + module: this._name, + enabled: this.enabled, + } + ); + } +} + +function createBrowser() { + const browser = (window.browser = document.createXULElement("browser")); + // Identify this `` element uniquely to Marionette, devtools, etc. + // Use the JSM global to create the permanentKey, so that if the + // permanentKey is held by something after this window closes, it + // doesn't keep the window alive. See also Bug 1501789. + browser.permanentKey = new (Cu.getGlobalForObject(Services).Object)(); + + browser.setAttribute("nodefaultsrc", "true"); + browser.setAttribute("type", "content"); + browser.setAttribute("primary", "true"); + browser.setAttribute("flex", "1"); + browser.setAttribute("maychangeremoteness", "true"); + browser.setAttribute("remote", "true"); + browser.setAttribute("remoteType", E10SUtils.DEFAULT_REMOTE_TYPE); + browser.setAttribute("messagemanagergroup", "browsers"); + + // This is only needed for mochitests, so that they honor the + // prefers-color-scheme.content-override pref. GeckoView doesn't set this + // pref to anything other than the default value otherwise. + browser.setAttribute( + "style", + "color-scheme: env(-moz-content-preferred-color-scheme)" + ); + + return browser; +} + +function InitLater(fn, object, name) { + return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */); +} + +function startup() { + GeckoViewUtils.initLogging("XUL", window); + + const browser = createBrowser(); + ModuleManager.init(browser, [ + { + name: "GeckoViewContent", + onInit: { + resource: "resource://gre/modules/GeckoViewContent.sys.mjs", + actors: { + GeckoViewContent: { + parent: { + esModuleURI: "resource:///actors/GeckoViewContentParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/GeckoViewContentChild.sys.mjs", + events: { + mozcaretstatechanged: { capture: true, mozSystemGroup: true }, + pageshow: { mozSystemGroup: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + onEnable: { + actors: { + ContentDelegate: { + parent: { + esModuleURI: "resource:///actors/ContentDelegateParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/ContentDelegateChild.sys.mjs", + events: { + DOMContentLoaded: {}, + DOMMetaViewportFitChanged: {}, + "MozDOMFullscreen:Entered": {}, + "MozDOMFullscreen:Exit": {}, + "MozDOMFullscreen:Exited": {}, + "MozDOMFullscreen:Request": {}, + MozFirstContentfulPaint: {}, + MozPaintStatusReset: {}, + contextmenu: { capture: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewNavigation", + onInit: { + resource: "resource://gre/modules/GeckoViewNavigation.sys.mjs", + }, + }, + { + name: "GeckoViewProcessHangMonitor", + onInit: { + resource: "resource://gre/modules/GeckoViewProcessHangMonitor.sys.mjs", + }, + }, + { + name: "GeckoViewProgress", + onEnable: { + resource: "resource://gre/modules/GeckoViewProgress.sys.mjs", + actors: { + ProgressDelegate: { + parent: { + esModuleURI: "resource:///actors/ProgressDelegateParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/ProgressDelegateChild.sys.mjs", + events: { + MozAfterPaint: { capture: false, mozSystemGroup: true }, + DOMContentLoaded: { capture: false, mozSystemGroup: true }, + pageshow: { capture: false, mozSystemGroup: true }, + }, + }, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewScroll", + onEnable: { + actors: { + ScrollDelegate: { + parent: { + esModuleURI: "resource:///actors/ScrollDelegateParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/ScrollDelegateChild.sys.mjs", + events: { + mozvisualscroll: { mozSystemGroup: true }, + }, + }, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewSelectionAction", + onEnable: { + resource: "resource://gre/modules/GeckoViewSelectionAction.sys.mjs", + actors: { + SelectionActionDelegate: { + parent: { + esModuleURI: + "resource:///actors/SelectionActionDelegateParent.sys.mjs", + }, + child: { + esModuleURI: + "resource:///actors/SelectionActionDelegateChild.sys.mjs", + events: { + mozcaretstatechanged: { mozSystemGroup: true }, + pagehide: { capture: true, mozSystemGroup: true }, + deactivate: { mozSystemGroup: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewSettings", + onInit: { + resource: "resource://gre/modules/GeckoViewSettings.sys.mjs", + actors: { + GeckoViewSettings: { + child: { + esModuleURI: "resource:///actors/GeckoViewSettingsChild.sys.mjs", + }, + }, + }, + }, + }, + { + name: "GeckoViewTab", + onInit: { + resource: "resource://gre/modules/GeckoViewTab.sys.mjs", + }, + }, + { + name: "GeckoViewContentBlocking", + onInit: { + resource: "resource://gre/modules/GeckoViewContentBlocking.sys.mjs", + }, + }, + { + name: "SessionStateAggregator", + onInit: { + frameScript: "chrome://geckoview/content/SessionStateAggregator.js", + }, + }, + { + name: "GeckoViewAutofill", + onInit: { + actors: { + GeckoViewAutoFill: { + parent: { + esModuleURI: "resource:///actors/GeckoViewAutoFillParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/GeckoViewAutoFillChild.sys.mjs", + events: { + DOMFormHasPassword: { + mozSystemGroup: true, + capture: false, + }, + DOMInputPasswordAdded: { + mozSystemGroup: true, + capture: false, + }, + pagehide: { + mozSystemGroup: true, + capture: false, + }, + pageshow: { + mozSystemGroup: true, + capture: false, + }, + focusin: { + mozSystemGroup: true, + capture: false, + }, + focusout: { + mozSystemGroup: true, + capture: false, + }, + "PasswordManager:ShowDoorhanger": {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewMediaControl", + onEnable: { + resource: "resource://gre/modules/GeckoViewMediaControl.sys.mjs", + actors: { + MediaControlDelegate: { + parent: { + esModuleURI: + "resource:///actors/MediaControlDelegateParent.sys.mjs", + }, + child: { + esModuleURI: + "resource:///actors/MediaControlDelegateChild.sys.mjs", + events: { + "MozDOMFullscreen:Entered": {}, + "MozDOMFullscreen:Exited": {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewAutocomplete", + onInit: { + actors: { + FormAutofill: { + parent: { + esModuleURI: "resource://autofill/FormAutofillParent.sys.mjs", + }, + child: { + esModuleURI: "resource://autofill/FormAutofillChild.sys.mjs", + events: { + focusin: {}, + DOMFormBeforeSubmit: {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewPrompter", + onInit: { + actors: { + GeckoViewPrompter: { + parent: { + esModuleURI: "resource:///actors/GeckoViewPrompterParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/GeckoViewPrompterChild.sys.mjs", + }, + allFrames: true, + includeChrome: true, + }, + }, + }, + }, + { + name: "GeckoViewPrintDelegate", + onInit: { + actors: { + GeckoViewPrintDelegate: { + parent: { + esModuleURI: + "resource:///actors/GeckoViewPrintDelegateParent.sys.mjs", + }, + child: { + esModuleURI: + "resource:///actors/GeckoViewPrintDelegateChild.sys.mjs", + }, + allFrames: true, + }, + }, + }, + }, + ]); + + if (!Services.appinfo.sessionHistoryInParent) { + browser.prepareToChangeRemoteness = () => + ModuleManager.prepareToChangeRemoteness(); + browser.afterChangeRemoteness = switchId => + ModuleManager.afterBrowserRemotenessChange(switchId); + } + + browser.addEventListener("WillChangeBrowserRemoteness", event => + ModuleManager.willChangeBrowserRemoteness() + ); + + browser.addEventListener("DidChangeBrowserRemoteness", event => + ModuleManager.didChangeBrowserRemoteness() + ); + + // Allows actors to access ModuleManager. + window.moduleManager = ModuleManager; + + window.prompts = () => { + return window.ModuleManager.getActor("GeckoViewPrompter").getPrompts(); + }; + + Services.tm.dispatchToMainThread(() => { + // This should always be the first thing we do here - any additional delayed + // initialisation tasks should be added between "browser-delayed-startup-finished" + // and "browser-idle-startup-tasks-finished". + + // Bug 1496684: Various bits of platform stuff depend on this notification + // to learn when a browser window has finished its initial (chrome) + // initialisation, especially with regards to the very first window that is + // created. Therefore, GeckoView "windows" need to send this, too. + InitLater(() => + Services.obs.notifyObservers(window, "browser-delayed-startup-finished") + ); + + // Let the extension code know it can start loading things that were delayed + // while GeckoView started up. + InitLater(() => { + Services.obs.notifyObservers(window, "extensions-late-startup"); + }); + + InitLater(() => { + // TODO bug 1730026: this runs too often. It should run once. + RemoteSecuritySettings.init(); + }); + + InitLater(() => { + // Initialize safe browsing module. This is required for content + // blocking features and manages blocklist downloads and updates. + SafeBrowsing.init(); + }); + + InitLater(() => { + // It's enough to run this once to set up FOG. + // (See also bug 1730026.) + Services.fog.registerCustomPings(); + }); + + InitLater(() => { + // Initialize the blocklist module. + // TODO bug 1730026: this runs too often. It should run once. + Blocklist.loadBlocklistAsync(); + }); + + // This should always go last, since the idle tasks (except for the ones with + // timeouts) should execute in order. Note that this observer notification is + // not guaranteed to fire, since the window could close before we get here. + + // This notification in particular signals the ScriptPreloader that we have + // finished startup, so it can now stop recording script usage and start + // updating the startup cache for faster script loading. + InitLater(() => + Services.obs.notifyObservers( + window, + "browser-idle-startup-tasks-finished" + ) + ); + }); + + // Move focus to the content window at the end of startup, + // so things like text selection can work properly. + browser.focus(); + + InitializationTracker.onInitialized(performance.now()); +} -- cgit v1.2.3