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 --- browser/modules/AboutNewTab.jsm | 256 ++++ browser/modules/AsyncTabSwitcher.jsm | 1500 ++++++++++++++++++++ browser/modules/BackgroundTask_install.sys.mjs | 29 + browser/modules/BackgroundTask_uninstall.sys.mjs | 58 + browser/modules/BrowserUIUtils.jsm | 179 +++ browser/modules/BrowserUsageTelemetry.jsm | 1454 +++++++++++++++++++ browser/modules/BrowserWindowTracker.jsm | 321 +++++ browser/modules/ContentCrashHandlers.jsm | 1144 +++++++++++++++ browser/modules/Discovery.jsm | 156 ++ browser/modules/EveryWindow.jsm | 109 ++ browser/modules/ExtensionsUI.jsm | 669 +++++++++ browser/modules/FaviconLoader.jsm | 710 +++++++++ browser/modules/FeatureCallout.sys.mjs | 1222 ++++++++++++++++ browser/modules/HomePage.jsm | 359 +++++ browser/modules/LaterRun.jsm | 192 +++ browser/modules/NewTabPagePreloading.jsm | 211 +++ browser/modules/OpenInTabsUtils.jsm | 85 ++ browser/modules/PageActions.jsm | 1266 +++++++++++++++++ browser/modules/PartnerLinkAttribution.sys.mjs | 218 +++ browser/modules/PermissionUI.sys.mjs | 1429 +++++++++++++++++++ browser/modules/PingCentre.jsm | 175 +++ browser/modules/ProcessHangMonitor.jsm | 693 +++++++++ browser/modules/Sanitizer.sys.mjs | 1146 +++++++++++++++ browser/modules/SelectionChangedMenulist.jsm | 32 + browser/modules/SiteDataManager.jsm | 667 +++++++++ browser/modules/SitePermissions.sys.mjs | 1326 +++++++++++++++++ browser/modules/TabUnloader.jsm | 523 +++++++ browser/modules/TabsList.jsm | 566 ++++++++ browser/modules/TransientPrefs.jsm | 27 + browser/modules/URILoadingHelper.sys.mjs | 739 ++++++++++ browser/modules/WindowsJumpLists.jsm | 665 +++++++++ browser/modules/WindowsPreviewPerTab.jsm | 910 ++++++++++++ browser/modules/ZoomUI.jsm | 213 +++ browser/modules/metrics.yaml | 155 ++ browser/modules/moz.build | 165 +++ browser/modules/test/browser/blank_iframe.html | 7 + browser/modules/test/browser/browser.ini | 73 + .../test/browser/browser_BrowserWindowTracker.js | 234 +++ .../modules/test/browser/browser_ContentSearch.js | 519 +++++++ .../modules/test/browser/browser_EveryWindow.js | 161 +++ .../test/browser/browser_HomePage_add_button.js | 159 +++ .../modules/test/browser/browser_PageActions.js | 1402 ++++++++++++++++++ .../browser/browser_PageActions_contextMenus.js | 226 +++ .../test/browser/browser_PageActions_newWindow.js | 377 +++++ .../test/browser/browser_PartnerLinkAttribution.js | 428 ++++++ .../modules/test/browser/browser_PermissionUI.js | 692 +++++++++ .../test/browser/browser_PermissionUI_prompts.js | 284 ++++ .../browser/browser_ProcessHangNotifications.js | 484 +++++++ .../test/browser/browser_SitePermissions.js | 227 +++ .../browser_SitePermissions_combinations.js | 144 ++ .../test/browser/browser_SitePermissions_expiry.js | 44 + .../browser/browser_SitePermissions_tab_urls.js | 128 ++ .../modules/test/browser/browser_TabUnloader.js | 381 +++++ .../browser_Telemetry_numberOfSiteOrigins.js | 53 + ...ser_Telemetry_numberOfSiteOriginsPerDocument.js | 134 ++ .../browser/browser_UnsubmittedCrashHandler.js | 819 +++++++++++ .../modules/test/browser/browser_UsageTelemetry.js | 684 +++++++++ ..._UsageTelemetry_content_aboutRestartRequired.js | 33 + .../test/browser/browser_UsageTelemetry_domains.js | 190 +++ .../browser/browser_UsageTelemetry_interaction.js | 967 +++++++++++++ .../browser_UsageTelemetry_private_and_restore.js | 164 +++ .../browser/browser_UsageTelemetry_toolbars.js | 550 +++++++ ...eTelemetry_uniqueOriginsVisitedInPast24Hours.js | 89 ++ .../test/browser/browser_preloading_tab_moving.js | 150 ++ .../test/browser/browser_taskbar_preview.js | 129 ++ .../modules/test/browser/browser_urlBar_zoom.js | 107 ++ browser/modules/test/browser/contain_iframe.html | 7 + .../modules/test/browser/contentSearchBadImage.xml | 6 + .../test/browser/contentSearchSuggestions.sjs | 9 + .../test/browser/contentSearchSuggestions.xml | 6 + browser/modules/test/browser/file_webrtc.html | 11 + .../test/browser/formValidation/browser.ini | 7 + .../formValidation/browser_form_validation.js | 519 +++++++ .../formValidation/browser_validation_iframe.js | 67 + .../formValidation/browser_validation_invisible.js | 67 + .../browser_validation_navigation.js | 49 + .../browser_validation_other_popups.js | 123 ++ browser/modules/test/browser/head.js | 331 +++++ .../browser/search-engines/basic/manifest.json | 19 + .../test/browser/search-engines/engines.json | 28 + .../browser/search-engines/simple/manifest.json | 29 + .../modules/test/browser/testEngine_chromeicon.xml | 12 + .../test/unit/test_E10SUtils_nested_URIs.js | 90 ++ browser/modules/test/unit/test_HomePage.js | 92 ++ browser/modules/test/unit/test_HomePage_ignore.js | 135 ++ .../test/unit/test_InstallationTelemetry.js | 284 ++++ browser/modules/test/unit/test_LaterRun.js | 242 ++++ .../test/unit/test_PartnerLinkAttribution.js | 54 + browser/modules/test/unit/test_PingCentre.js | 194 +++ browser/modules/test/unit/test_ProfileCounter.js | 239 ++++ .../test/unit/test_Sanitizer_interrupted.js | 139 ++ browser/modules/test/unit/test_SiteDataManager.js | 277 ++++ .../test/unit/test_SiteDataManagerContainers.js | 140 ++ browser/modules/test/unit/test_SitePermissions.js | 401 ++++++ .../test/unit/test_SitePermissions_temporary.js | 710 +++++++++ browser/modules/test/unit/test_TabUnloader.js | 449 ++++++ browser/modules/test/unit/test_discovery.js | 138 ++ browser/modules/test/unit/xpcshell.ini | 23 + browser/modules/webrtcUI.jsm | 1296 +++++++++++++++++ 99 files changed, 35801 insertions(+) create mode 100644 browser/modules/AboutNewTab.jsm create mode 100644 browser/modules/AsyncTabSwitcher.jsm create mode 100644 browser/modules/BackgroundTask_install.sys.mjs create mode 100644 browser/modules/BackgroundTask_uninstall.sys.mjs create mode 100644 browser/modules/BrowserUIUtils.jsm create mode 100644 browser/modules/BrowserUsageTelemetry.jsm create mode 100644 browser/modules/BrowserWindowTracker.jsm create mode 100644 browser/modules/ContentCrashHandlers.jsm create mode 100644 browser/modules/Discovery.jsm create mode 100644 browser/modules/EveryWindow.jsm create mode 100644 browser/modules/ExtensionsUI.jsm create mode 100644 browser/modules/FaviconLoader.jsm create mode 100644 browser/modules/FeatureCallout.sys.mjs create mode 100644 browser/modules/HomePage.jsm create mode 100644 browser/modules/LaterRun.jsm create mode 100644 browser/modules/NewTabPagePreloading.jsm create mode 100644 browser/modules/OpenInTabsUtils.jsm create mode 100644 browser/modules/PageActions.jsm create mode 100644 browser/modules/PartnerLinkAttribution.sys.mjs create mode 100644 browser/modules/PermissionUI.sys.mjs create mode 100644 browser/modules/PingCentre.jsm create mode 100644 browser/modules/ProcessHangMonitor.jsm create mode 100644 browser/modules/Sanitizer.sys.mjs create mode 100644 browser/modules/SelectionChangedMenulist.jsm create mode 100644 browser/modules/SiteDataManager.jsm create mode 100644 browser/modules/SitePermissions.sys.mjs create mode 100644 browser/modules/TabUnloader.jsm create mode 100644 browser/modules/TabsList.jsm create mode 100644 browser/modules/TransientPrefs.jsm create mode 100644 browser/modules/URILoadingHelper.sys.mjs create mode 100644 browser/modules/WindowsJumpLists.jsm create mode 100644 browser/modules/WindowsPreviewPerTab.jsm create mode 100644 browser/modules/ZoomUI.jsm create mode 100644 browser/modules/metrics.yaml create mode 100644 browser/modules/moz.build create mode 100644 browser/modules/test/browser/blank_iframe.html create mode 100644 browser/modules/test/browser/browser.ini create mode 100644 browser/modules/test/browser/browser_BrowserWindowTracker.js create mode 100644 browser/modules/test/browser/browser_ContentSearch.js create mode 100644 browser/modules/test/browser/browser_EveryWindow.js create mode 100644 browser/modules/test/browser/browser_HomePage_add_button.js create mode 100644 browser/modules/test/browser/browser_PageActions.js create mode 100644 browser/modules/test/browser/browser_PageActions_contextMenus.js create mode 100644 browser/modules/test/browser/browser_PageActions_newWindow.js create mode 100644 browser/modules/test/browser/browser_PartnerLinkAttribution.js create mode 100644 browser/modules/test/browser/browser_PermissionUI.js create mode 100644 browser/modules/test/browser/browser_PermissionUI_prompts.js create mode 100644 browser/modules/test/browser/browser_ProcessHangNotifications.js create mode 100644 browser/modules/test/browser/browser_SitePermissions.js create mode 100644 browser/modules/test/browser/browser_SitePermissions_combinations.js create mode 100644 browser/modules/test/browser/browser_SitePermissions_expiry.js create mode 100644 browser/modules/test/browser/browser_SitePermissions_tab_urls.js create mode 100644 browser/modules/test/browser/browser_TabUnloader.js create mode 100644 browser/modules/test/browser/browser_Telemetry_numberOfSiteOrigins.js create mode 100644 browser/modules/test/browser/browser_Telemetry_numberOfSiteOriginsPerDocument.js create mode 100644 browser/modules/test/browser/browser_UnsubmittedCrashHandler.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry_content_aboutRestartRequired.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry_domains.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry_interaction.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry_private_and_restore.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry_toolbars.js create mode 100644 browser/modules/test/browser/browser_UsageTelemetry_uniqueOriginsVisitedInPast24Hours.js create mode 100644 browser/modules/test/browser/browser_preloading_tab_moving.js create mode 100644 browser/modules/test/browser/browser_taskbar_preview.js create mode 100644 browser/modules/test/browser/browser_urlBar_zoom.js create mode 100644 browser/modules/test/browser/contain_iframe.html create mode 100644 browser/modules/test/browser/contentSearchBadImage.xml create mode 100644 browser/modules/test/browser/contentSearchSuggestions.sjs create mode 100644 browser/modules/test/browser/contentSearchSuggestions.xml create mode 100644 browser/modules/test/browser/file_webrtc.html create mode 100644 browser/modules/test/browser/formValidation/browser.ini create mode 100644 browser/modules/test/browser/formValidation/browser_form_validation.js create mode 100644 browser/modules/test/browser/formValidation/browser_validation_iframe.js create mode 100644 browser/modules/test/browser/formValidation/browser_validation_invisible.js create mode 100644 browser/modules/test/browser/formValidation/browser_validation_navigation.js create mode 100644 browser/modules/test/browser/formValidation/browser_validation_other_popups.js create mode 100644 browser/modules/test/browser/head.js create mode 100644 browser/modules/test/browser/search-engines/basic/manifest.json create mode 100644 browser/modules/test/browser/search-engines/engines.json create mode 100644 browser/modules/test/browser/search-engines/simple/manifest.json create mode 100644 browser/modules/test/browser/testEngine_chromeicon.xml create mode 100644 browser/modules/test/unit/test_E10SUtils_nested_URIs.js create mode 100644 browser/modules/test/unit/test_HomePage.js create mode 100644 browser/modules/test/unit/test_HomePage_ignore.js create mode 100644 browser/modules/test/unit/test_InstallationTelemetry.js create mode 100644 browser/modules/test/unit/test_LaterRun.js create mode 100644 browser/modules/test/unit/test_PartnerLinkAttribution.js create mode 100644 browser/modules/test/unit/test_PingCentre.js create mode 100644 browser/modules/test/unit/test_ProfileCounter.js create mode 100644 browser/modules/test/unit/test_Sanitizer_interrupted.js create mode 100644 browser/modules/test/unit/test_SiteDataManager.js create mode 100644 browser/modules/test/unit/test_SiteDataManagerContainers.js create mode 100644 browser/modules/test/unit/test_SitePermissions.js create mode 100644 browser/modules/test/unit/test_SitePermissions_temporary.js create mode 100644 browser/modules/test/unit/test_TabUnloader.js create mode 100644 browser/modules/test/unit/test_discovery.js create mode 100644 browser/modules/test/unit/xpcshell.ini create mode 100644 browser/modules/webrtcUI.jsm (limited to 'browser/modules') diff --git a/browser/modules/AboutNewTab.jsm b/browser/modules/AboutNewTab.jsm new file mode 100644 index 0000000000..07d3d9b1bd --- /dev/null +++ b/browser/modules/AboutNewTab.jsm @@ -0,0 +1,256 @@ +/* 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"; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + ActivityStream: "resource://activity-stream/lib/ActivityStream.jsm", + ObjectUtils: "resource://gre/modules/ObjectUtils.jsm", +}); + +const ABOUT_URL = "about:newtab"; +const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug"; +const TOPIC_APP_QUIT = "quit-application-granted"; +const BROWSER_READY_NOTIFICATION = "sessionstore-windows-restored"; + +const AboutNewTab = { + QueryInterface: ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + ]), + + // AboutNewTab + initialized: false, + + willNotifyUser: false, + + _activityStreamEnabled: false, + activityStream: null, + activityStreamDebug: false, + + _cachedTopSites: null, + + _newTabURL: ABOUT_URL, + _newTabURLOverridden: false, + + /** + * init - Initializes an instance of Activity Stream if one doesn't exist already. + */ + init() { + Services.obs.addObserver(this, TOPIC_APP_QUIT); + if (!AppConstants.RELEASE_OR_BETA) { + XPCOMUtils.defineLazyPreferenceGetter( + this, + "activityStreamDebug", + PREF_ACTIVITY_STREAM_DEBUG, + false, + () => { + this.notifyChange(); + } + ); + } + + XPCOMUtils.defineLazyPreferenceGetter( + this, + "privilegedAboutProcessEnabled", + "browser.tabs.remote.separatePrivilegedContentProcess", + false, + () => { + this.notifyChange(); + } + ); + + // More initialization happens here + this.toggleActivityStream(true); + this.initialized = true; + + Services.obs.addObserver(this, BROWSER_READY_NOTIFICATION); + }, + + /** + * React to changes to the activity stream being enabled or not. + * + * This will only act if there is a change of state and if not overridden. + * + * @returns {Boolean} Returns if there has been a state change + * + * @param {Boolean} stateEnabled activity stream enabled state to set to + * @param {Boolean} forceState force state change + */ + toggleActivityStream(stateEnabled, forceState = false) { + if ( + !forceState && + (this._newTabURLOverridden || + stateEnabled === this._activityStreamEnabled) + ) { + // exit there is no change of state + return false; + } + if (stateEnabled) { + this._activityStreamEnabled = true; + } else { + this._activityStreamEnabled = false; + } + + this._newTabURL = ABOUT_URL; + return true; + }, + + get newTabURL() { + return this._newTabURL; + }, + + set newTabURL(aNewTabURL) { + let newTabURL = aNewTabURL.trim(); + if (newTabURL === ABOUT_URL) { + // avoid infinite redirects in case one sets the URL to about:newtab + this.resetNewTabURL(); + return; + } else if (newTabURL === "") { + newTabURL = "about:blank"; + } + + this.toggleActivityStream(false); + this._newTabURL = newTabURL; + this._newTabURLOverridden = true; + this.notifyChange(); + }, + + get newTabURLOverridden() { + return this._newTabURLOverridden; + }, + + get activityStreamEnabled() { + return this._activityStreamEnabled; + }, + + resetNewTabURL() { + this._newTabURLOverridden = false; + this._newTabURL = ABOUT_URL; + this.toggleActivityStream(true, true); + this.notifyChange(); + }, + + notifyChange() { + Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL); + }, + + /** + * onBrowserReady - Continues the initialization of Activity Stream after browser is ready. + */ + onBrowserReady() { + if (this.activityStream && this.activityStream.initialized) { + return; + } + + this.activityStream = new lazy.ActivityStream(); + try { + this.activityStream.init(); + this._subscribeToActivityStream(); + } catch (e) { + console.error(e); + } + }, + + _subscribeToActivityStream() { + let unsubscribe = this.activityStream.store.subscribe(() => { + // If the top sites changed, broadcast "newtab-top-sites-changed". We + // ignore changes to the `screenshot` property in each site because + // screenshots are generated at times that are hard to predict and it ends + // up interfering with tests that rely on "newtab-top-sites-changed". + // Observers likely don't care about screenshots anyway. + let topSites = this.activityStream.store + .getState() + .TopSites.rows.map(site => { + site = { ...site }; + delete site.screenshot; + return site; + }); + if (!lazy.ObjectUtils.deepEqual(topSites, this._cachedTopSites)) { + this._cachedTopSites = topSites; + Services.obs.notifyObservers(null, "newtab-top-sites-changed"); + } + }); + this._unsubscribeFromActivityStream = () => { + try { + unsubscribe(); + } catch (e) { + console.error(e); + } + }; + }, + + /** + * uninit - Uninitializes Activity Stream if it exists. + */ + uninit() { + if (this.activityStream) { + this._unsubscribeFromActivityStream?.(); + this.activityStream.uninit(); + this.activityStream = null; + } + + this.initialized = false; + }, + + getTopSites() { + return this.activityStream + ? this.activityStream.store.getState().TopSites.rows + : []; + }, + + _alreadyRecordedTopsitesPainted: false, + _nonDefaultStartup: false, + + noteNonDefaultStartup() { + this._nonDefaultStartup = true; + }, + + maybeRecordTopsitesPainted(timestamp) { + if (this._alreadyRecordedTopsitesPainted || this._nonDefaultStartup) { + return; + } + + const SCALAR_KEY = "timestamps.about_home_topsites_first_paint"; + + let startupInfo = Services.startup.getStartupInfo(); + let processStartTs = startupInfo.process.getTime(); + let delta = Math.round(timestamp - processStartTs); + Services.telemetry.scalarSet(SCALAR_KEY, delta); + ChromeUtils.addProfilerMarker("aboutHomeTopsitesFirstPaint"); + this._alreadyRecordedTopsitesPainted = true; + }, + + // nsIObserver implementation + + observe(subject, topic, data) { + switch (topic) { + case TOPIC_APP_QUIT: { + // We defer to this to the next tick of the event loop since the + // AboutHomeStartupCache might want to read from the ActivityStream + // store during TOPIC_APP_QUIT. + Services.tm.dispatchToMainThread(() => this.uninit()); + break; + } + case BROWSER_READY_NOTIFICATION: { + Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION); + // Avoid running synchronously during this event that's used for timing + Services.tm.dispatchToMainThread(() => this.onBrowserReady()); + break; + } + } + }, +}; + +var EXPORTED_SYMBOLS = ["AboutNewTab"]; diff --git a/browser/modules/AsyncTabSwitcher.jsm b/browser/modules/AsyncTabSwitcher.jsm new file mode 100644 index 0000000000..b97a6e634b --- /dev/null +++ b/browser/modules/AsyncTabSwitcher.jsm @@ -0,0 +1,1500 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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 EXPORTED_SYMBOLS = ["AsyncTabSwitcher"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PictureInPicture: "resource://gre/modules/PictureInPicture.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gTabWarmingEnabled", + "browser.tabs.remote.warmup.enabled" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gTabWarmingMax", + "browser.tabs.remote.warmup.maxTabs" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gTabWarmingUnloadDelayMs", + "browser.tabs.remote.warmup.unloadDelayMs" +); +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gTabCacheSize", + "browser.tabs.remote.tabCacheSize" +); + +/** + * The tab switcher is responsible for asynchronously switching + * tabs in e10s. It waits until the new tab is ready (i.e., the + * layer tree is available) before switching to it. Then it + * unloads the layer tree for the old tab. + * + * The tab switcher is a state machine. For each tab, it + * maintains state about whether the layer tree for the tab is + * available, being loaded, being unloaded, or unavailable. It + * also keeps track of the tab currently being displayed, the tab + * it's trying to load, and the tab the user has asked to switch + * to. The switcher object is created upon tab switch. It is + * released when there are no pending tabs to load or unload. + * + * The following general principles have guided the design: + * + * 1. We only request one layer tree at a time. If the user + * switches to a different tab while waiting, we don't request + * the new layer tree until the old tab has loaded or timed out. + * + * 2. If loading the layers for a tab times out, we show the + * spinner and possibly request the layer tree for another tab if + * the user has requested one. + * + * 3. We discard layer trees on a delay. This way, if the user is + * switching among the same tabs frequently, we don't continually + * load the same tabs. + * + * It's important that we always show either the spinner or a tab + * whose layers are available. Otherwise the compositor will draw + * an entirely black frame, which is very jarring. To ensure this + * never happens when switching away from a tab, we assume the + * old tab might still be drawn until a MozAfterPaint event + * occurs. Because layout and compositing happen asynchronously, + * we don't have any other way of knowing when the switch + * actually takes place. Therefore, we don't unload the old tab + * until the next MozAfterPaint event. + */ +class AsyncTabSwitcher { + constructor(tabbrowser) { + this.log("START"); + + // How long to wait for a tab's layers to load. After this + // time elapses, we're free to put up the spinner and start + // trying to load a different tab. + this.TAB_SWITCH_TIMEOUT = 400; // ms + + // When the user hasn't switched tabs for this long, we unload + // layers for all tabs that aren't in use. + this.UNLOAD_DELAY = 300; // ms + + // The next three tabs form the principal state variables. + // See the assertions in postActions for their invariants. + + // Tab the user requested most recently. + this.requestedTab = tabbrowser.selectedTab; + + // Tab we're currently trying to load. + this.loadingTab = null; + + // We show this tab in case the requestedTab hasn't loaded yet. + this.lastVisibleTab = tabbrowser.selectedTab; + + // Auxilliary state variables: + + this.visibleTab = tabbrowser.selectedTab; // Tab that's on screen. + this.spinnerTab = null; // Tab showing a spinner. + this.blankTab = null; // Tab showing blank. + this.lastPrimaryTab = tabbrowser.selectedTab; // Tab with primary="true" + + this.tabbrowser = tabbrowser; + this.window = tabbrowser.ownerGlobal; + this.loadTimer = null; // TAB_SWITCH_TIMEOUT nsITimer instance. + this.unloadTimer = null; // UNLOAD_DELAY nsITimer instance. + + // Map from tabs to STATE_* (below). + this.tabState = new Map(); + + // True if we're in the midst of switching tabs. + this.switchInProgress = false; + + // Transaction id for the composite that will show the requested + // tab for the first tab after a tab switch. + // Set to -1 when we're not waiting for notification of a + // completed switch. + this.switchPaintId = -1; + + // Set of tabs that might be visible right now. We maintain + // this set because we can't be sure when a tab is actually + // drawn. A tab is added to this set when we ask to make it + // visible. All tabs but the most recently shown tab are + // removed from the set upon MozAfterPaint. + this.maybeVisibleTabs = new Set([tabbrowser.selectedTab]); + + // This holds onto the set of tabs that we've been asked to warm up, + // and tabs are evicted once they're done loading or are unloaded. + this.warmingTabs = new WeakSet(); + + this.STATE_UNLOADED = 0; + this.STATE_LOADING = 1; + this.STATE_LOADED = 2; + this.STATE_UNLOADING = 3; + + // re-entrancy guard: + this._processing = false; + + // For telemetry, keeps track of what most recently cleared + // the loadTimer, which can tell us something about the cause + // of tab switch spinners. + this._loadTimerClearedBy = "none"; + + this._useDumpForLogging = false; + this._logInit = false; + this._logFlags = []; + + this.window.addEventListener("MozAfterPaint", this); + this.window.addEventListener("MozLayerTreeReady", this); + this.window.addEventListener("MozLayerTreeCleared", this); + this.window.addEventListener("TabRemotenessChange", this); + this.window.addEventListener("SwapDocShells", this, true); + this.window.addEventListener("EndSwapDocShells", this, true); + this.window.document.addEventListener("visibilitychange", this); + + let initialTab = this.requestedTab; + let initialBrowser = initialTab.linkedBrowser; + + let tabIsLoaded = + !initialBrowser.isRemoteBrowser || + initialBrowser.frameLoader.remoteTab?.hasLayers; + + // If we minimized the window before the switcher was activated, + // we might have set the preserveLayers flag for the current + // browser. Let's clear it. + initialBrowser.preserveLayers(false); + + if (!this.windowHidden) { + this.log("Initial tab is loaded?: " + tabIsLoaded); + this.setTabState( + initialTab, + tabIsLoaded ? this.STATE_LOADED : this.STATE_LOADING + ); + } + + for (let ppBrowser of this.tabbrowser._printPreviewBrowsers) { + let ppTab = this.tabbrowser.getTabForBrowser(ppBrowser); + let state = ppBrowser.hasLayers ? this.STATE_LOADED : this.STATE_LOADING; + this.setTabState(ppTab, state); + } + } + + destroy() { + if (this.unloadTimer) { + this.clearTimer(this.unloadTimer); + this.unloadTimer = null; + } + if (this.loadTimer) { + this.clearTimer(this.loadTimer); + this.loadTimer = null; + } + + this.window.removeEventListener("MozAfterPaint", this); + this.window.removeEventListener("MozLayerTreeReady", this); + this.window.removeEventListener("MozLayerTreeCleared", this); + this.window.removeEventListener("TabRemotenessChange", this); + this.window.removeEventListener("SwapDocShells", this, true); + this.window.removeEventListener("EndSwapDocShells", this, true); + this.window.document.removeEventListener("visibilitychange", this); + + this.tabbrowser._switcher = null; + } + + // Wraps nsITimer. Must not use the vanilla setTimeout and + // clearTimeout, because they will be blocked by nsIPromptService + // dialogs. + setTimer(callback, timeout) { + let event = { + notify: callback, + }; + + var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + timer.initWithCallback(event, timeout, Ci.nsITimer.TYPE_ONE_SHOT); + return timer; + } + + clearTimer(timer) { + timer.cancel(); + } + + getTabState(tab) { + let state = this.tabState.get(tab); + + // As an optimization, we lazily evaluate the state of tabs + // that we've never seen before. Once we've figured it out, + // we stash it in our state map. + if (state === undefined) { + state = this.STATE_UNLOADED; + + if (tab && tab.linkedPanel) { + let b = tab.linkedBrowser; + if (b.renderLayers && b.hasLayers) { + state = this.STATE_LOADED; + } else if (b.renderLayers && !b.hasLayers) { + state = this.STATE_LOADING; + } else if (!b.renderLayers && b.hasLayers) { + state = this.STATE_UNLOADING; + } + } + + this.setTabStateNoAction(tab, state); + } + + return state; + } + + setTabStateNoAction(tab, state) { + if (state == this.STATE_UNLOADED) { + this.tabState.delete(tab); + } else { + this.tabState.set(tab, state); + } + } + + setTabState(tab, state) { + if (state == this.getTabState(tab)) { + return; + } + + this.setTabStateNoAction(tab, state); + + let browser = tab.linkedBrowser; + let { remoteTab } = browser.frameLoader; + if (state == this.STATE_LOADING) { + this.assert(!this.windowHidden); + + // If we're not in the process of warming this tab, we + // don't need to delay activating its DocShell. + if (!this.warmingTabs.has(tab)) { + browser.docShellIsActive = true; + } + + if (remoteTab) { + browser.renderLayers = true; + remoteTab.priorityHint = true; + } else { + this.onLayersReady(browser); + } + } else if (state == this.STATE_UNLOADING) { + this.unwarmTab(tab); + // Setting the docShell to be inactive will also cause it + // to stop rendering layers. + browser.docShellIsActive = false; + if (remoteTab) { + remoteTab.priorityHint = false; + } else { + this.onLayersCleared(browser); + } + } else if (state == this.STATE_LOADED) { + this.maybeActivateDocShell(tab); + } + + if (!tab.linkedBrowser.isRemoteBrowser) { + // setTabState is potentially re-entrant in the non-remote case, + // so we must re-get the state for this assertion. + let nonRemoteState = this.getTabState(tab); + // Non-remote tabs can never stay in the STATE_LOADING + // or STATE_UNLOADING states. By the time this function + // exits, a non-remote tab must be in STATE_LOADED or + // STATE_UNLOADED, since the painting and the layer + // upload happen synchronously. + this.assert( + nonRemoteState == this.STATE_UNLOADED || + nonRemoteState == this.STATE_LOADED + ); + } + } + + get windowHidden() { + return this.window.document.hidden; + } + + get tabLayerCache() { + return this.tabbrowser._tabLayerCache; + } + + finish() { + this.log("FINISH"); + + this.assert(this.tabbrowser._switcher); + this.assert(this.tabbrowser._switcher === this); + this.assert(!this.spinnerTab); + this.assert(!this.blankTab); + this.assert(!this.loadTimer); + this.assert(!this.loadingTab); + this.assert(this.lastVisibleTab === this.requestedTab); + this.assert( + this.windowHidden || + this.getTabState(this.requestedTab) == this.STATE_LOADED + ); + + this.destroy(); + + this.window.document.commandDispatcher.unlock(); + + let event = new this.window.CustomEvent("TabSwitchDone", { + bubbles: true, + cancelable: true, + }); + this.tabbrowser.dispatchEvent(event); + } + + // This function is called after all the main state changes to + // make sure we display the right tab. + updateDisplay() { + let requestedTabState = this.getTabState(this.requestedTab); + let requestedBrowser = this.requestedTab.linkedBrowser; + + // It is often more desirable to show a blank tab when appropriate than + // the tab switch spinner - especially since the spinner is usually + // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the + // tab switch. We can hide this lag, and hide the time being spent + // constructing BrowserChild's, layer trees, etc, by showing a blank + // tab instead and focusing it immediately. + let shouldBeBlank = false; + if (requestedBrowser.isRemoteBrowser) { + // If a tab is remote and the window is not minimized, we can show a + // blank tab instead of a spinner in the following cases: + // + // 1. The tab has just crashed, and we haven't started showing the + // tab crashed page yet (in this case, the RemoteTab is null) + // 2. The tab has never presented, and has not finished loading + // a non-local-about: page. + // + // For (2), "finished loading a non-local-about: page" is + // determined by the busy state on the tab element and checking + // if the loaded URI is local. + let isBusy = this.requestedTab.hasAttribute("busy"); + let isLocalAbout = requestedBrowser.currentURI.schemeIs("about"); + let hasSufficientlyLoaded = !isBusy && !isLocalAbout; + + let fl = requestedBrowser.frameLoader; + shouldBeBlank = + !this.windowHidden && + (!fl.remoteTab || + (!hasSufficientlyLoaded && !fl.remoteTab.hasPresented)); + + if (this.logging()) { + let flag = shouldBeBlank ? "blank" : "nonblank"; + this.addLogFlag( + flag, + this.windowHidden, + fl.remoteTab, + isBusy, + isLocalAbout, + fl.remoteTab ? fl.remoteTab.hasPresented : 0 + ); + } + } + + if (requestedBrowser.isRemoteBrowser) { + this.addLogFlag("isRemote"); + } + + // Figure out which tab we actually want visible right now. + let showTab = null; + if ( + requestedTabState != this.STATE_LOADED && + this.lastVisibleTab && + this.loadTimer && + !shouldBeBlank + ) { + // If we can't show the requestedTab, and lastVisibleTab is + // available, show it. + showTab = this.lastVisibleTab; + } else { + // Show the requested tab. If it's not available, we'll show the spinner or a blank tab. + showTab = this.requestedTab; + } + + // First, let's deal with blank tabs, which we show instead + // of the spinner when the tab is not currently set up + // properly in the content process. + if (!shouldBeBlank && this.blankTab) { + this.blankTab.linkedBrowser.removeAttribute("blank"); + this.blankTab = null; + } else if (shouldBeBlank && this.blankTab !== showTab) { + if (this.blankTab) { + this.blankTab.linkedBrowser.removeAttribute("blank"); + } + this.blankTab = showTab; + this.blankTab.linkedBrowser.setAttribute("blank", "true"); + } + + // Show or hide the spinner as needed. + let needSpinner = + this.getTabState(showTab) != this.STATE_LOADED && + !this.windowHidden && + !shouldBeBlank && + !this.loadTimer; + + if (!needSpinner && this.spinnerTab) { + this.noteSpinnerHidden(); + this.tabbrowser.tabpanels.removeAttribute("pendingpaint"); + this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint"); + this.spinnerTab = null; + } else if (needSpinner && this.spinnerTab !== showTab) { + if (this.spinnerTab) { + this.spinnerTab.linkedBrowser.removeAttribute("pendingpaint"); + } else { + this.noteSpinnerDisplayed(); + } + this.spinnerTab = showTab; + this.tabbrowser.tabpanels.setAttribute("pendingpaint", "true"); + this.spinnerTab.linkedBrowser.setAttribute("pendingpaint", "true"); + } + + // Switch to the tab we've decided to make visible. + if (this.visibleTab !== showTab) { + this.tabbrowser._adjustFocusBeforeTabSwitch(this.visibleTab, showTab); + this.visibleTab = showTab; + + this.maybeVisibleTabs.add(showTab); + + let tabpanels = this.tabbrowser.tabpanels; + let showPanel = this.tabbrowser.tabContainer.getRelatedElement(showTab); + let index = Array.prototype.indexOf.call(tabpanels.children, showPanel); + if (index != -1) { + this.log(`Switch to tab ${index} - ${this.tinfo(showTab)}`); + tabpanels.updateSelectedIndex(index); + if (showTab === this.requestedTab) { + if (requestedTabState == this.STATE_LOADED) { + // The new tab will be made visible in the next paint, record the expected + // transaction id for that, and we'll mark when we get notified of its + // completion. + this.switchPaintId = this.window.windowUtils.lastTransactionId + 1; + } else { + this.noteMakingTabVisibleWithoutLayers(); + } + + this.tabbrowser._adjustFocusAfterTabSwitch(showTab); + this.window.gURLBar.afterTabSwitchFocusChange(); + this.maybeActivateDocShell(this.requestedTab); + } + } + + // This doesn't necessarily exist if we're a new window and haven't switched tabs yet + if (this.lastVisibleTab) { + this.lastVisibleTab._visuallySelected = false; + } + + this.visibleTab._visuallySelected = true; + this.tabbrowser.tabContainer._setPositionalAttributes(); + } + + this.lastVisibleTab = this.visibleTab; + } + + assert(cond) { + if (!cond) { + dump("Assertion failure\n" + Error().stack); + + // Don't break a user's browser if an assertion fails. + if (AppConstants.DEBUG) { + throw new Error("Assertion failure"); + } + } + } + + maybeClearLoadTimer(caller) { + if (this.loadingTab) { + this._loadTimerClearedBy = caller; + this.loadingTab = null; + if (this.loadTimer) { + this.clearTimer(this.loadTimer); + this.loadTimer = null; + } + } + } + + // We've decided to try to load requestedTab. + loadRequestedTab() { + this.assert(!this.loadTimer); + this.assert(!this.windowHidden); + + // loadingTab can be non-null here if we timed out loading the current tab. + // In that case we just overwrite it with a different tab; it's had its chance. + this.loadingTab = this.requestedTab; + this.log("Loading tab " + this.tinfo(this.loadingTab)); + + this.loadTimer = this.setTimer( + () => this.handleEvent({ type: "loadTimeout" }), + this.TAB_SWITCH_TIMEOUT + ); + this.setTabState(this.requestedTab, this.STATE_LOADING); + } + + maybeActivateDocShell(tab) { + // If we've reached the point where the requested tab has entered + // the loaded state, but the DocShell is still not yet active, we + // should activate it. + let browser = tab.linkedBrowser; + let state = this.getTabState(tab); + let canCheckDocShellState = + !browser.mDestroyed && + (browser.docShell || browser.frameLoader.remoteTab); + if ( + tab == this.requestedTab && + canCheckDocShellState && + state == this.STATE_LOADED && + !browser.docShellIsActive && + !this.windowHidden + ) { + browser.docShellIsActive = true; + this.logState( + "Set requested tab docshell to active and preserveLayers to false" + ); + // If we minimized the window before the switcher was activated, + // we might have set the preserveLayers flag for the current + // browser. Let's clear it. + browser.preserveLayers(false); + } + } + + // This function runs before every event. It fixes up the state + // to account for closed tabs. + preActions() { + this.assert(this.tabbrowser._switcher); + this.assert(this.tabbrowser._switcher === this); + + for (let i = 0; i < this.tabLayerCache.length; i++) { + let tab = this.tabLayerCache[i]; + if (!tab.linkedBrowser) { + this.tabState.delete(tab); + this.tabLayerCache.splice(i, 1); + i--; + } + } + + for (let [tab] of this.tabState) { + if (!tab.linkedBrowser) { + this.tabState.delete(tab); + this.unwarmTab(tab); + } + } + + if (this.lastVisibleTab && !this.lastVisibleTab.linkedBrowser) { + this.lastVisibleTab = null; + } + if (this.lastPrimaryTab && !this.lastPrimaryTab.linkedBrowser) { + this.lastPrimaryTab = null; + } + if (this.blankTab && !this.blankTab.linkedBrowser) { + this.blankTab = null; + } + if (this.spinnerTab && !this.spinnerTab.linkedBrowser) { + this.noteSpinnerHidden(); + this.spinnerTab = null; + } + if (this.loadingTab && !this.loadingTab.linkedBrowser) { + this.maybeClearLoadTimer("preActions"); + } + } + + // This code runs after we've responded to an event or requested a new + // tab. It's expected that we've already updated all the principal + // state variables. This function takes care of updating any auxilliary + // state. + postActions(eventString) { + // Once we finish loading loadingTab, we null it out. So the state should + // always be LOADING. + this.assert( + !this.loadingTab || + this.getTabState(this.loadingTab) == this.STATE_LOADING + ); + + // We guarantee that loadingTab is non-null iff loadTimer is non-null. So + // the timer is set only when we're loading something. + this.assert(!this.loadTimer || this.loadingTab); + this.assert(!this.loadingTab || this.loadTimer); + + // If we're switching to a non-remote tab, there's no need to wait + // for it to send layers to the compositor, as this will happen + // synchronously. Clearing this here means that in the next step, + // we can load the non-remote browser immediately. + if (!this.requestedTab.linkedBrowser.isRemoteBrowser) { + this.maybeClearLoadTimer("postActions"); + } + + // If we're not loading anything, try loading the requested tab. + let stateOfRequestedTab = this.getTabState(this.requestedTab); + if ( + !this.loadTimer && + !this.windowHidden && + (stateOfRequestedTab == this.STATE_UNLOADED || + stateOfRequestedTab == this.STATE_UNLOADING || + this.warmingTabs.has(this.requestedTab)) + ) { + this.assert(stateOfRequestedTab != this.STATE_LOADED); + this.loadRequestedTab(); + } + + let numBackgroundCached = 0; + for (let tab of this.tabLayerCache) { + if (tab !== this.requestedTab) { + numBackgroundCached++; + } + } + + // See how many tabs still have work to do. + let numPending = 0; + let numWarming = 0; + for (let [tab, state] of this.tabState) { + if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) { + continue; + } + + if ( + state == this.STATE_LOADED && + tab !== this.requestedTab && + !this.tabLayerCache.includes(tab) + ) { + numPending++; + + if (tab !== this.visibleTab) { + numWarming++; + } + } + if (state == this.STATE_LOADING || state == this.STATE_UNLOADING) { + numPending++; + } + } + + this.updateDisplay(); + + // It's possible for updateDisplay to trigger one of our own event + // handlers, which might cause finish() to already have been called. + // Check for that before calling finish() again. + if (!this.tabbrowser._switcher) { + return; + } + + this.maybeFinishTabSwitch(); + + if (numBackgroundCached > 0) { + this.deactivateCachedBackgroundTabs(); + } + + if (numWarming > lazy.gTabWarmingMax) { + this.logState("Hit tabWarmingMax"); + if (this.unloadTimer) { + this.clearTimer(this.unloadTimer); + } + this.unloadNonRequiredTabs(); + } + + if (numPending == 0) { + this.finish(); + } + + this.logState("/" + eventString); + } + + // Fires when we're ready to unload unused tabs. + onUnloadTimeout() { + this.unloadTimer = null; + this.unloadNonRequiredTabs(); + } + + deactivateCachedBackgroundTabs() { + for (let tab of this.tabLayerCache) { + if (tab !== this.requestedTab) { + let browser = tab.linkedBrowser; + browser.preserveLayers(true); + browser.docShellIsActive = false; + } + } + } + + // If there are any non-visible and non-requested tabs in + // STATE_LOADED, sets them to STATE_UNLOADING. Also queues + // up the unloadTimer to run onUnloadTimeout if there are still + // tabs in the process of unloading. + unloadNonRequiredTabs() { + this.warmingTabs = new WeakSet(); + let numPending = 0; + + // Unload any tabs that can be unloaded. + for (let [tab, state] of this.tabState) { + if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) { + continue; + } + + let isInLayerCache = this.tabLayerCache.includes(tab); + + if ( + state == this.STATE_LOADED && + !this.maybeVisibleTabs.has(tab) && + tab !== this.lastVisibleTab && + tab !== this.loadingTab && + tab !== this.requestedTab && + !isInLayerCache + ) { + this.setTabState(tab, this.STATE_UNLOADING); + } + + if ( + state != this.STATE_UNLOADED && + tab !== this.requestedTab && + !isInLayerCache + ) { + numPending++; + } + } + + if (numPending) { + // Keep the timer going since there may be more tabs to unload. + this.unloadTimer = this.setTimer( + () => this.handleEvent({ type: "unloadTimeout" }), + this.UNLOAD_DELAY + ); + } + } + + // Fires when an ongoing load has taken too long. + onLoadTimeout() { + this.maybeClearLoadTimer("onLoadTimeout"); + } + + // Fires when the layers become available for a tab. + onLayersReady(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + if (!tab) { + // We probably got a layer update from a tab that got before + // the switcher was created, or for browser that's not being + // tracked by the async tab switcher (like the preloaded about:newtab). + return; + } + + this.logState(`onLayersReady(${tab._tPos}, ${browser.isRemoteBrowser})`); + + this.assert( + this.getTabState(tab) == this.STATE_LOADING || + this.getTabState(tab) == this.STATE_LOADED + ); + this.setTabState(tab, this.STATE_LOADED); + this.unwarmTab(tab); + + if (this.loadingTab === tab) { + this.maybeClearLoadTimer("onLayersReady"); + } + } + + // Fires when we paint the screen. Any tab switches we initiated + // previously are done, so there's no need to keep the old layers + // around. + onPaint(event) { + this.addLogFlag( + "onPaint", + this.switchPaintId != -1, + event.transactionId >= this.switchPaintId + ); + this.notePaint(event); + this.maybeVisibleTabs.clear(); + } + + // Called when we're done clearing the layers for a tab. + onLayersCleared(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + if (tab) { + this.logState(`onLayersCleared(${tab._tPos})`); + this.assert( + this.getTabState(tab) == this.STATE_UNLOADING || + this.getTabState(tab) == this.STATE_UNLOADED + ); + this.setTabState(tab, this.STATE_UNLOADED); + } + } + + // Called when a tab switches from remote to non-remote. In this case + // a MozLayerTreeReady notification that we requested may never fire, + // so we need to simulate it. + onRemotenessChange(tab) { + this.logState( + `onRemotenessChange(${tab._tPos}, ${tab.linkedBrowser.isRemoteBrowser})` + ); + if (!tab.linkedBrowser.isRemoteBrowser) { + if (this.getTabState(tab) == this.STATE_LOADING) { + this.onLayersReady(tab.linkedBrowser); + } else if (this.getTabState(tab) == this.STATE_UNLOADING) { + this.onLayersCleared(tab.linkedBrowser); + } + } else if (this.getTabState(tab) == this.STATE_LOADED) { + // A tab just changed from non-remote to remote, which means + // that it's gone back into the STATE_LOADING state until + // it sends up a layer tree. + this.setTabState(tab, this.STATE_LOADING); + } + } + + onTabRemoved(tab) { + if (this.lastVisibleTab == tab) { + this.handleEvent({ type: "tabRemoved", tab }); + } + } + + // Called when a tab has been removed, and the browser node is + // about to be removed from the DOM. + onTabRemovedImpl(tab) { + this.lastVisibleTab = null; + } + + onVisibilityChange() { + if (this.windowHidden) { + for (let [tab, state] of this.tabState) { + if (!this.shouldDeactivateDocShell(tab.linkedBrowser)) { + continue; + } + + if (state == this.STATE_LOADING || state == this.STATE_LOADED) { + this.setTabState(tab, this.STATE_UNLOADING); + } + } + this.maybeClearLoadTimer("onSizeModeOrOcc"); + } else { + // We're no longer minimized or occluded. This means we might want + // to activate the current tab's docShell. + this.maybeActivateDocShell(this.tabbrowser.selectedTab); + } + } + + onSwapDocShells(ourBrowser, otherBrowser) { + // This event fires before the swap. ourBrowser is from + // our window. We save the state of otherBrowser since ourBrowser + // needs to take on that state at the end of the swap. + + let otherTabbrowser = otherBrowser.ownerGlobal.gBrowser; + let otherState; + if (otherTabbrowser && otherTabbrowser._switcher) { + let otherTab = otherTabbrowser.getTabForBrowser(otherBrowser); + let otherSwitcher = otherTabbrowser._switcher; + otherState = otherSwitcher.getTabState(otherTab); + } else { + otherState = otherBrowser.docShellIsActive + ? this.STATE_LOADED + : this.STATE_UNLOADED; + } + if (!this.swapMap) { + this.swapMap = new WeakMap(); + } + this.swapMap.set(otherBrowser, { + state: otherState, + }); + } + + onEndSwapDocShells(ourBrowser, otherBrowser) { + // The swap has happened. We reset the loadingTab in + // case it has been swapped. We also set ourBrowser's state + // to whatever otherBrowser's state was before the swap. + + // Clearing the load timer means that we will + // immediately display a spinner if ourBrowser isn't + // ready yet. Typically it will already be ready + // though. If it's not, we're probably in a new window, + // in which case we have no other tabs to display anyway. + this.maybeClearLoadTimer("onEndSwapDocShells"); + + let { state: otherState } = this.swapMap.get(otherBrowser); + + this.swapMap.delete(otherBrowser); + + let ourTab = this.tabbrowser.getTabForBrowser(ourBrowser); + if (ourTab) { + this.setTabStateNoAction(ourTab, otherState); + } + } + + /** + * Check if the browser should be deactivated. If the browser is a print preivew or + * PiP browser then we won't deactive it. + * @param browser The browser to check if it should be deactivated + * @returns false if a print preview or PiP browser else true + */ + shouldDeactivateDocShell(browser) { + return !( + this.tabbrowser._printPreviewBrowsers.has(browser) || + lazy.PictureInPicture.isOriginatingBrowser(browser) + ); + } + + shouldActivateDocShell(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + let state = this.getTabState(tab); + return state == this.STATE_LOADING || state == this.STATE_LOADED; + } + + activateBrowserForPrintPreview(browser) { + let tab = this.tabbrowser.getTabForBrowser(browser); + let state = this.getTabState(tab); + if (state != this.STATE_LOADING && state != this.STATE_LOADED) { + this.setTabState(tab, this.STATE_LOADING); + this.logState( + "Activated browser " + this.tinfo(tab) + " for print preview" + ); + } + } + + canWarmTab(tab) { + if (!lazy.gTabWarmingEnabled) { + return false; + } + + if (!tab) { + return false; + } + + // If the tab is not yet inserted, closing, not remote, + // crashed, already visible, or already requested, warming + // up the tab makes no sense. + if ( + this.windowHidden || + !tab.linkedPanel || + tab.closing || + !tab.linkedBrowser.isRemoteBrowser || + !tab.linkedBrowser.frameLoader.remoteTab + ) { + return false; + } + + return true; + } + + shouldWarmTab(tab) { + if (this.canWarmTab(tab)) { + // Tabs that are already in STATE_LOADING or STATE_LOADED + // have no need to be warmed up. + let state = this.getTabState(tab); + if (state === this.STATE_UNLOADING || state === this.STATE_UNLOADED) { + return true; + } + } + + return false; + } + + unwarmTab(tab) { + this.warmingTabs.delete(tab); + } + + warmupTab(tab) { + if (!this.shouldWarmTab(tab)) { + return; + } + + this.logState("warmupTab " + this.tinfo(tab)); + + this.warmingTabs.add(tab); + this.setTabState(tab, this.STATE_LOADING); + this.queueUnload(lazy.gTabWarmingUnloadDelayMs); + } + + cleanUpTabAfterEviction(tab) { + this.assert(tab !== this.requestedTab); + let browser = tab.linkedBrowser; + if (browser) { + browser.preserveLayers(false); + } + this.setTabState(tab, this.STATE_UNLOADING); + } + + evictOldestTabFromCache() { + let tab = this.tabLayerCache.shift(); + this.cleanUpTabAfterEviction(tab); + } + + maybePromoteTabInLayerCache(tab) { + if ( + lazy.gTabCacheSize > 1 && + tab.linkedBrowser.isRemoteBrowser && + tab.linkedBrowser.currentURI.spec != "about:blank" + ) { + let tabIndex = this.tabLayerCache.indexOf(tab); + + if (tabIndex != -1) { + this.tabLayerCache.splice(tabIndex, 1); + } + + this.tabLayerCache.push(tab); + + if (this.tabLayerCache.length > lazy.gTabCacheSize) { + this.evictOldestTabFromCache(); + } + } + } + + // Called when the user asks to switch to a given tab. + requestTab(tab) { + if (tab === this.requestedTab) { + return; + } + + let tabState = this.getTabState(tab); + this.noteTabRequested(tab, tabState); + + this.logState("requestTab " + this.tinfo(tab)); + this.startTabSwitch(); + + let oldBrowser = this.requestedTab.linkedBrowser; + oldBrowser.deprioritize(); + this.requestedTab = tab; + if (tabState == this.STATE_LOADED) { + this.maybeVisibleTabs.clear(); + } + + tab.linkedBrowser.setAttribute("primary", "true"); + if (this.lastPrimaryTab && this.lastPrimaryTab != tab) { + this.lastPrimaryTab.linkedBrowser.removeAttribute("primary"); + } + this.lastPrimaryTab = tab; + + this.queueUnload(this.UNLOAD_DELAY); + } + + queueUnload(unloadTimeout) { + this.handleEvent({ type: "queueUnload", unloadTimeout }); + } + + onQueueUnload(unloadTimeout) { + if (this.unloadTimer) { + this.clearTimer(this.unloadTimer); + } + this.unloadTimer = this.setTimer( + () => this.handleEvent({ type: "unloadTimeout" }), + unloadTimeout + ); + } + + handleEvent(event, delayed = false) { + if (this._processing) { + this.setTimer(() => this.handleEvent(event, true), 0); + return; + } + if (delayed && this.tabbrowser._switcher != this) { + // if we delayed processing this event, we might be out of date, in which + // case we drop the delayed events + return; + } + this._processing = true; + try { + this.preActions(); + + switch (event.type) { + case "queueUnload": + this.onQueueUnload(event.unloadTimeout); + break; + case "unloadTimeout": + this.onUnloadTimeout(); + break; + case "loadTimeout": + this.onLoadTimeout(); + break; + case "tabRemoved": + this.onTabRemovedImpl(event.tab); + break; + case "MozLayerTreeReady": + this.onLayersReady(event.originalTarget); + break; + case "MozAfterPaint": + this.onPaint(event); + break; + case "MozLayerTreeCleared": + this.onLayersCleared(event.originalTarget); + break; + case "TabRemotenessChange": + this.onRemotenessChange(event.target); + break; + case "visibilitychange": + this.onVisibilityChange(); + break; + case "SwapDocShells": + this.onSwapDocShells(event.originalTarget, event.detail); + break; + case "EndSwapDocShells": + this.onEndSwapDocShells(event.originalTarget, event.detail); + break; + } + + this.postActions(event.type); + } finally { + this._processing = false; + } + } + + /* + * Telemetry and Profiler related helpers for recording tab switch + * timing. + */ + + startTabSwitch() { + this.noteStartTabSwitch(); + this.switchInProgress = true; + } + + /** + * Something has occurred that might mean that we've completed + * the tab switch (layers are ready, paints are done, spinners + * are hidden). This checks to make sure all conditions are + * satisfied, and then records the tab switch as finished. + */ + maybeFinishTabSwitch() { + if ( + this.switchInProgress && + this.requestedTab && + (this.getTabState(this.requestedTab) == this.STATE_LOADED || + this.requestedTab === this.blankTab) + ) { + if (this.requestedTab !== this.blankTab) { + this.maybePromoteTabInLayerCache(this.requestedTab); + } + + this.noteFinishTabSwitch(); + this.switchInProgress = false; + + let event = new this.window.CustomEvent("TabSwitched", { + bubbles: true, + detail: { + tab: this.requestedTab, + }, + }); + this.tabbrowser.dispatchEvent(event); + } + } + + /* + * Debug related logging for switcher. + */ + logging() { + if (this._useDumpForLogging) { + return true; + } + if (this._logInit) { + return this._shouldLog; + } + let result = Services.prefs.getBoolPref( + "browser.tabs.remote.logSwitchTiming", + false + ); + this._shouldLog = result; + this._logInit = true; + return this._shouldLog; + } + + tinfo(tab) { + if (tab) { + return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")"; + } + return "null"; + } + + log(s) { + if (!this.logging()) { + return; + } + if (this._useDumpForLogging) { + dump(s + "\n"); + } else { + Services.console.logStringMessage(s); + } + } + + addLogFlag(flag, ...subFlags) { + if (this.logging()) { + if (subFlags.length) { + flag += `(${subFlags.map(f => (f ? 1 : 0)).join("")})`; + } + this._logFlags.push(flag); + } + } + + logState(suffix) { + if (!this.logging()) { + return; + } + + let getTabString = tab => { + let tabString = ""; + + let state = this.getTabState(tab); + let isWarming = this.warmingTabs.has(tab); + let isCached = this.tabLayerCache.includes(tab); + let isClosing = tab.closing; + let linkedBrowser = tab.linkedBrowser; + let isActive = linkedBrowser && linkedBrowser.docShellIsActive; + let isRendered = linkedBrowser && linkedBrowser.renderLayers; + let isPiP = + linkedBrowser && + lazy.PictureInPicture.isOriginatingBrowser(linkedBrowser); + + if (tab === this.lastVisibleTab) { + tabString += "V"; + } + if (tab === this.loadingTab) { + tabString += "L"; + } + if (tab === this.requestedTab) { + tabString += "R"; + } + if (tab === this.blankTab) { + tabString += "B"; + } + if (this.maybeVisibleTabs.has(tab)) { + tabString += "M"; + } + + let extraStates = ""; + if (isWarming) { + extraStates += "W"; + } + if (isCached) { + extraStates += "C"; + } + if (isClosing) { + extraStates += "X"; + } + if (isActive) { + extraStates += "A"; + } + if (isRendered) { + extraStates += "R"; + } + if (isPiP) { + extraStates += "P"; + } + if (extraStates != "") { + tabString += `(${extraStates})`; + } + + switch (state) { + case this.STATE_LOADED: { + tabString += "(loaded)"; + break; + } + case this.STATE_LOADING: { + tabString += "(loading)"; + break; + } + case this.STATE_UNLOADING: { + tabString += "(unloading)"; + break; + } + case this.STATE_UNLOADED: { + tabString += "(unloaded)"; + break; + } + } + + return tabString; + }; + + let accum = ""; + + // This is a bit tricky to read, but what we're doing here is collapsing + // identical tab states down to make the overal string shorter and easier + // to read, and we move all simply unloaded tabs to the back of the list. + // I.e., we turn + // "0:(unloaded) 1:(unloaded) 2:(unloaded) 3:(loaded)"" + // into + // "3:(loaded) 0...2:(unloaded)" + let tabStrings = this.tabbrowser.tabs.map(t => getTabString(t)); + let lastMatch = -1; + let unloadedTabsStrings = []; + for (let i = 0; i <= tabStrings.length; i++) { + if (i > 0) { + if (i < tabStrings.length && tabStrings[i] == tabStrings[lastMatch]) { + continue; + } + + if (tabStrings[lastMatch] == "(unloaded)") { + if (lastMatch == i - 1) { + unloadedTabsStrings.push(lastMatch.toString()); + } else { + unloadedTabsStrings.push(`${lastMatch}...${i - 1}`); + } + } else if (lastMatch == i - 1) { + accum += `${lastMatch}:${tabStrings[lastMatch]} `; + } else { + accum += `${lastMatch}...${i - 1}:${tabStrings[lastMatch]} `; + } + } + + lastMatch = i; + } + + if (unloadedTabsStrings.length) { + accum += `${unloadedTabsStrings.join(",")}:(unloaded) `; + } + + accum += "cached: " + this.tabLayerCache.length + " "; + + if (this._logFlags.length) { + accum += `[${this._logFlags.join(",")}] `; + this._logFlags = []; + } + + // It can be annoying to read through the entirety of a log string just + // to check if something changed or not. So if we can tell that nothing + // changed, just write "unchanged" to save the reader's time. + let logString; + if (this._lastLogString == accum) { + accum = "unchanged"; + } else { + this._lastLogString = accum; + } + logString = `ATS: ${accum}{${suffix}}`; + + if (this._useDumpForLogging) { + dump(logString + "\n"); + } else { + Services.console.logStringMessage(logString); + } + } + + noteMakingTabVisibleWithoutLayers() { + // We're making the tab visible even though we haven't yet got layers for it. + // It's hard to know which composite the layers will first be available in (and + // the parent process might not even get MozAfterPaint delivered for it), so just + // give up measuring this for now. :( + TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window); + } + + notePaint(event) { + if (this.switchPaintId != -1 && event.transactionId >= this.switchPaintId) { + if ( + TelemetryStopwatch.running( + "FX_TAB_SWITCH_COMPOSITE_E10S_MS", + this.window + ) + ) { + let time = TelemetryStopwatch.timeElapsed( + "FX_TAB_SWITCH_COMPOSITE_E10S_MS", + this.window + ); + if (time != -1) { + TelemetryStopwatch.finish( + "FX_TAB_SWITCH_COMPOSITE_E10S_MS", + this.window + ); + } + } + let { innerWindowId } = this.window.windowGlobalChild; + ChromeUtils.addProfilerMarker("AsyncTabSwitch:Composited", { + innerWindowId, + }); + this.switchPaintId = -1; + } + } + + noteTabRequested(tab, tabState) { + if (lazy.gTabWarmingEnabled) { + let warmingState = "disqualified"; + + if (this.canWarmTab(tab)) { + if (tabState == this.STATE_LOADING) { + warmingState = "stillLoading"; + } else if (tabState == this.STATE_LOADED) { + warmingState = "loaded"; + } else if ( + tabState == this.STATE_UNLOADING || + tabState == this.STATE_UNLOADED + ) { + // At this point, if the tab's browser was being inserted + // lazily, we never had a chance to warm it up, and unfortunately + // there's no great way to detect that case. Those cases will + // end up in the "notWarmed" bucket, along with legitimate cases + // where tabs could have been warmed but weren't. + warmingState = "notWarmed"; + } + } + + Services.telemetry + .getHistogramById("FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE") + .add(warmingState); + } + } + + noteStartTabSwitch() { + TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window); + TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window); + + if ( + TelemetryStopwatch.running("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window) + ) { + TelemetryStopwatch.cancel("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window); + } + TelemetryStopwatch.start("FX_TAB_SWITCH_COMPOSITE_E10S_MS", this.window); + let { innerWindowId } = this.window.windowGlobalChild; + ChromeUtils.addProfilerMarker("AsyncTabSwitch:Start", { innerWindowId }); + } + + noteFinishTabSwitch() { + // After this point the tab has switched from the content thread's point of view. + // The changes will be visible after the next refresh driver tick + composite. + let time = TelemetryStopwatch.timeElapsed( + "FX_TAB_SWITCH_TOTAL_E10S_MS", + this.window + ); + if (time != -1) { + TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_E10S_MS", this.window); + this.log("DEBUG: tab switch time = " + time); + let { innerWindowId } = this.window.windowGlobalChild; + ChromeUtils.addProfilerMarker("AsyncTabSwitch:Finish", { innerWindowId }); + } + } + + noteSpinnerDisplayed() { + this.assert(!this.spinnerTab); + let browser = this.requestedTab.linkedBrowser; + this.assert(browser.isRemoteBrowser); + TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window); + // We have a second, similar probe for capturing recordings of + // when the spinner is displayed for very long periods. + TelemetryStopwatch.start( + "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", + this.window + ); + let { innerWindowId } = this.window.windowGlobalChild; + ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerShown", { + innerWindowId, + }); + Services.telemetry + .getHistogramById("FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER") + .add(this._loadTimerClearedBy); + if (AppConstants.NIGHTLY_BUILD) { + Services.obs.notifyObservers(null, "tabswitch-spinner"); + } + } + + noteSpinnerHidden() { + this.assert(this.spinnerTab); + this.log( + "DEBUG: spinner time = " + + TelemetryStopwatch.timeElapsed( + "FX_TAB_SWITCH_SPINNER_VISIBLE_MS", + this.window + ) + ); + TelemetryStopwatch.finish("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", this.window); + TelemetryStopwatch.finish( + "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", + this.window + ); + let { innerWindowId } = this.window.windowGlobalChild; + ChromeUtils.addProfilerMarker("AsyncTabSwitch:SpinnerHidden", { + innerWindowId, + }); + // we do not get a onPaint after displaying the spinner + this._loadTimerClearedBy = "none"; + } +} diff --git a/browser/modules/BackgroundTask_install.sys.mjs b/browser/modules/BackgroundTask_install.sys.mjs new file mode 100644 index 0000000000..8f13aa8789 --- /dev/null +++ b/browser/modules/BackgroundTask_install.sys.mjs @@ -0,0 +1,29 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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/. */ + +/** + * This task ought to have an ephemeral profile and should not apply updates. + * These settings are controlled externally, by + * `BackgroundTasks::IsUpdatingTaskName` and + * `BackgroundTasks::IsEphemeralProfileTaskName`. + */ + +// This happens synchronously during installation. It shouldn't take that long +// and if something goes wrong we really don't want to sit around waiting for +// it. +export const backgroundTaskTimeoutSec = 30; + +export async function runBackgroundTask(commandLine) { + console.log("Running BackgroundTask_install."); + + console.log("Cleaning up update files."); + try { + Cc["@mozilla.org/updates/update-manager;1"] + .getService(Ci.nsIUpdateManager) + .doInstallCleanup(); + } catch (ex) { + console.error(ex); + } +} diff --git a/browser/modules/BackgroundTask_uninstall.sys.mjs b/browser/modules/BackgroundTask_uninstall.sys.mjs new file mode 100644 index 0000000000..bb83a194f8 --- /dev/null +++ b/browser/modules/BackgroundTask_uninstall.sys.mjs @@ -0,0 +1,58 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * 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/. */ + +/** + * This task ought to have an ephemeral profile and should not apply updates. + * These settings are controlled externally, by + * `BackgroundTasks::IsUpdatingTaskName` and + * `BackgroundTasks::IsEphemeralProfileTaskName`. + */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +export async function runBackgroundTask(commandLine) { + console.log("Running BackgroundTask_uninstall."); + + if (AppConstants.platform === "win") { + try { + removeNotifications(); + } catch (ex) { + console.error(ex); + } + } else { + console.log("Not a Windows install. Skipping notification removal."); + } + + console.log("Cleaning up update files."); + try { + Cc["@mozilla.org/updates/update-manager;1"] + .getService(Ci.nsIUpdateManager) + .doUninstallCleanup(); + } catch (ex) { + console.error(ex); + } +} + +function removeNotifications() { + console.log("Removing Windows toast notifications."); + + if (!("nsIWindowsAlertsService" in Ci)) { + console.log("nsIWindowsAlertService not present."); + return; + } + + let alertsService; + try { + alertsService = Cc["@mozilla.org/system-alerts-service;1"] + .getService(Ci.nsIAlertsService) + .QueryInterface(Ci.nsIWindowsAlertsService); + } catch (e) { + console.error("Error retrieving nsIWindowsAlertService: " + e.message); + return; + } + + alertsService.removeAllNotificationsForInstall(); + console.log("Finished removing Windows toast notifications."); +} diff --git a/browser/modules/BrowserUIUtils.jsm b/browser/modules/BrowserUIUtils.jsm new file mode 100644 index 0000000000..c338b26fca --- /dev/null +++ b/browser/modules/BrowserUIUtils.jsm @@ -0,0 +1,179 @@ +/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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 { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +var EXPORTED_SYMBOLS = ["BrowserUIUtils"]; + +var BrowserUIUtils = { + /** + * Check whether a page can be considered as 'empty', that its URI + * reflects its origin, and that if it's loaded in a tab, that tab + * could be considered 'empty' (e.g. like the result of opening + * a 'blank' new tab). + * + * We have to do more than just check the URI, because especially + * for things like about:blank, it is possible that the opener or + * some other page has control over the contents of the page. + * + * @param {Browser} browser + * The browser whose page we're checking. + * @param {nsIURI} [uri] + * The URI against which we're checking (the browser's currentURI + * if omitted). + * + * @return {boolean} false if the page was opened by or is controlled by + * arbitrary web content, unless that content corresponds with the URI. + * true if the page is blank and controlled by a principal matching + * that URI (or the system principal if the principal has no URI) + */ + checkEmptyPageOrigin(browser, uri = browser.currentURI) { + // If another page opened this page with e.g. window.open, this page might + // be controlled by its opener. + if (browser.hasContentOpener) { + return false; + } + let contentPrincipal = browser.contentPrincipal; + // Not all principals have URIs... + // There are two special-cases involving about:blank. One is where + // the user has manually loaded it and it got created with a null + // principal. The other involves the case where we load + // some other empty page in a browser and the current page is the + // initial about:blank page (which has that as its principal, not + // just URI in which case it could be web-based). Especially in + // e10s, we need to tackle that case specifically to avoid race + // conditions when updating the URL bar. + // + // Note that we check the documentURI here, since the currentURI on + // the browser might have been set by SessionStore in order to + // support switch-to-tab without having actually loaded the content + // yet. + let uriToCheck = browser.documentURI || uri; + if ( + (uriToCheck.spec == "about:blank" && contentPrincipal.isNullPrincipal) || + contentPrincipal.spec == "about:blank" + ) { + return true; + } + if (contentPrincipal.isContentPrincipal) { + return contentPrincipal.equalsURI(uri); + } + // ... so for those that don't have them, enforce that the page has the + // system principal (this matches e.g. on about:newtab). + return contentPrincipal.isSystemPrincipal; + }, + + /** + * Generate a document fragment for a localized string that has DOM + * node replacements. This avoids using getFormattedString followed + * by assigning to innerHTML. Fluent can probably replace this when + * it is in use everywhere. + * + * @param {Document} doc + * @param {String} msg + * The string to put replacements in. Fetch from + * a stringbundle using getString or GetStringFromName, + * or even an inserted dtd string. + * @param {Node|String} nodesOrStrings + * The replacement items. Can be a mix of Nodes + * and Strings. However, for correct behaviour, the + * number of items provided needs to exactly match + * the number of replacement strings in the l10n string. + * @returns {DocumentFragment} + * A document fragment. In the trivial case (no + * replacements), this will simply be a fragment with 1 + * child, a text node containing the localized string. + */ + getLocalizedFragment(doc, msg, ...nodesOrStrings) { + // Ensure replacement points are indexed: + for (let i = 1; i <= nodesOrStrings.length; i++) { + if (!msg.includes("%" + i + "$S")) { + msg = msg.replace(/%S/, "%" + i + "$S"); + } + } + let numberOfInsertionPoints = msg.match(/%\d+\$S/g).length; + if (numberOfInsertionPoints != nodesOrStrings.length) { + console.error( + `Message has ${numberOfInsertionPoints} insertion points, ` + + `but got ${nodesOrStrings.length} replacement parameters!` + ); + } + + let fragment = doc.createDocumentFragment(); + let parts = [msg]; + let insertionPoint = 1; + for (let replacement of nodesOrStrings) { + let insertionString = "%" + insertionPoint++ + "$S"; + let partIndex = parts.findIndex( + part => typeof part == "string" && part.includes(insertionString) + ); + if (partIndex == -1) { + fragment.appendChild(doc.createTextNode(msg)); + return fragment; + } + + if (typeof replacement == "string") { + parts[partIndex] = parts[partIndex].replace( + insertionString, + replacement + ); + } else { + let [firstBit, lastBit] = parts[partIndex].split(insertionString); + parts.splice(partIndex, 1, firstBit, replacement, lastBit); + } + } + + // Put everything in a document fragment: + for (let part of parts) { + if (typeof part == "string") { + if (part) { + fragment.appendChild(doc.createTextNode(part)); + } + } else { + fragment.appendChild(part); + } + } + return fragment; + }, + + removeSingleTrailingSlashFromURL(aURL) { + // remove single trailing slash for http/https/ftp URLs + return aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1"); + }, + + /** + * Returns a URL which has been trimmed by removing 'http://' and any + * trailing slash (in http/https/ftp urls). + * Note that a trimmed url may not load the same page as the original url, so + * before loading it, it must be passed through URIFixup, to check trimming + * doesn't change its destination. We don't run the URIFixup check here, + * because trimURL is in the page load path (see onLocationChange), so it + * must be fast and simple. + * + * @param {string} aURL The URL to trim. + * @returns {string} The trimmed string. + */ + get trimURLProtocol() { + return "http://"; + }, + trimURL(aURL) { + let url = this.removeSingleTrailingSlashFromURL(aURL); + // Remove "http://" prefix. + return url.startsWith(this.trimURLProtocol) + ? url.substring(this.trimURLProtocol.length) + : url; + }, +}; + +XPCOMUtils.defineLazyPreferenceGetter( + BrowserUIUtils, + "quitShortcutDisabled", + "browser.quitShortcut.disabled", + false +); diff --git a/browser/modules/BrowserUsageTelemetry.jsm b/browser/modules/BrowserUsageTelemetry.jsm new file mode 100644 index 0000000000..41525f06d0 --- /dev/null +++ b/browser/modules/BrowserUsageTelemetry.jsm @@ -0,0 +1,1454 @@ +/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */ +/* 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 EXPORTED_SYMBOLS = [ + "BrowserUsageTelemetry", + "getUniqueDomainsVisitedInPast24Hours", + "URICountListener", + "MINIMUM_TAB_COUNT_INTERVAL_MS", +]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ClientID: "resource://gre/modules/ClientID.sys.mjs", + CustomizableUI: "resource:///modules/CustomizableUI.sys.mjs", + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + ProvenanceData: "resource:///modules/ProvenanceData.sys.mjs", + SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.sys.mjs", + SearchSERPTelemetryUtils: "resource:///modules/SearchSERPTelemetry.sys.mjs", + + WindowsInstallsInfo: + "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs", + + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + PageActions: "resource:///modules/PageActions.jsm", +}); + +// This pref is in seconds! +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "gRecentVisitedOriginsExpiry", + "browser.engagement.recent_visited_origins.expiry" +); + +// The upper bound for the count of the visited unique domain names. +const MAX_UNIQUE_VISITED_DOMAINS = 100; + +// Observed topic names. +const TAB_RESTORING_TOPIC = "SSTabRestoring"; +const TELEMETRY_SUBSESSIONSPLIT_TOPIC = + "internal-telemetry-after-subsession-split"; +const DOMWINDOW_OPENED_TOPIC = "domwindowopened"; + +// Probe names. +const MAX_TAB_COUNT_SCALAR_NAME = "browser.engagement.max_concurrent_tab_count"; +const MAX_WINDOW_COUNT_SCALAR_NAME = + "browser.engagement.max_concurrent_window_count"; +const TAB_OPEN_EVENT_COUNT_SCALAR_NAME = + "browser.engagement.tab_open_event_count"; +const MAX_TAB_PINNED_COUNT_SCALAR_NAME = + "browser.engagement.max_concurrent_tab_pinned_count"; +const TAB_PINNED_EVENT_COUNT_SCALAR_NAME = + "browser.engagement.tab_pinned_event_count"; +const WINDOW_OPEN_EVENT_COUNT_SCALAR_NAME = + "browser.engagement.window_open_event_count"; +const UNIQUE_DOMAINS_COUNT_SCALAR_NAME = + "browser.engagement.unique_domains_count"; +const TOTAL_URI_COUNT_SCALAR_NAME = "browser.engagement.total_uri_count"; +const UNFILTERED_URI_COUNT_SCALAR_NAME = + "browser.engagement.unfiltered_uri_count"; +const TOTAL_URI_COUNT_NORMAL_AND_PRIVATE_MODE_SCALAR_NAME = + "browser.engagement.total_uri_count_normal_and_private_mode"; + +const MINIMUM_TAB_COUNT_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes, in ms + +// The elements we consider to be interactive. +const UI_TARGET_ELEMENTS = [ + "menuitem", + "toolbarbutton", + "key", + "command", + "checkbox", + "input", + "button", + "image", + "radio", + "richlistitem", +]; + +// The containers of interactive elements that we care about and their pretty +// names. These should be listed in order of most-specific to least-specific, +// when iterating JavaScript will guarantee that ordering and so we will find +// the most specific area first. +const BROWSER_UI_CONTAINER_IDS = { + "toolbar-menubar": "menu-bar", + TabsToolbar: "tabs-bar", + PersonalToolbar: "bookmarks-bar", + "appMenu-popup": "app-menu", + tabContextMenu: "tabs-context", + contentAreaContextMenu: "content-context", + "widget-overflow-list": "overflow-menu", + "widget-overflow-fixed-list": "pinned-overflow-menu", + "page-action-buttons": "pageaction-urlbar", + pageActionPanel: "pageaction-panel", + "unified-extensions-area": "unified-extensions-area", + "allTabsMenu-allTabsView": "alltabs-menu", + + // This should appear last as some of the above are inside the nav bar. + "nav-bar": "nav-bar", +}; + +const ENTRYPOINT_TRACKED_CONTEXT_MENU_IDS = { + [BROWSER_UI_CONTAINER_IDS.tabContextMenu]: "tabs-context-entrypoint", +}; + +// A list of the expected panes in about:preferences +const PREFERENCES_PANES = [ + "paneHome", + "paneGeneral", + "panePrivacy", + "paneSearch", + "paneSearchResults", + "paneSync", + "paneContainers", + "paneExperimental", + "paneMoreFromMozilla", +]; + +const IGNORABLE_EVENTS = new WeakMap(); + +const KNOWN_ADDONS = []; + +// Buttons that, when clicked, set a preference to true. The convention +// is that the preference is named: +// +// browser.engagement.