Source code

Revision control

Copy as Markdown

Other Tools

/* 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.importESModule(
);
var { XPCOMUtils } = ChromeUtils.importESModule(
);
ChromeUtils.defineESModuleGetters(this, {
RemoteSecuritySettings:
});
ChromeUtils.defineLazyGetter(this, "WindowEventDispatcher", () =>
EventDispatcher.for(window)
);
XPCOMUtils.defineLazyScriptGetter(
this,
"PrintUtils",
);
// 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 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);
// GeckoView browsers start off as active (for now at least).
// See bug 1815015 for an attempt at making them start off inactive.
aBrowser.docShellIsActive = 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();
});
},
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) {
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 `<browser>` 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");
browser.setAttribute("manualactiveness", "true");
// 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: {
actors: {
GeckoViewContent: {
},
child: {
events: {
mozcaretstatechanged: { capture: true, mozSystemGroup: true },
pageshow: { mozSystemGroup: true },
},
},
allFrames: true,
messageManagerGroups: ["browsers"],
},
},
},
onEnable: {
actors: {
ContentDelegate: {
},
child: {
events: {
DOMContentLoaded: {},
DOMMetaViewportFitChanged: {},
"MozDOMFullscreen:Entered": {},
"MozDOMFullscreen:Exit": {},
"MozDOMFullscreen:Exited": {},
"MozDOMFullscreen:Request": {},
MozFirstContentfulPaint: {},
MozPaintStatusReset: {},
contextmenu: {},
},
},
allFrames: true,
messageManagerGroups: ["browsers"],
},
},
},
},
{
name: "GeckoViewNavigation",
},
},
{
name: "GeckoViewProcessHangMonitor",
},
},
{
name: "GeckoViewProgress",
onEnable: {
actors: {
ProgressDelegate: {
},
child: {
events: {
MozAfterPaint: { capture: false, mozSystemGroup: true },
DOMContentLoaded: { capture: false, mozSystemGroup: true },
pageshow: { capture: false, mozSystemGroup: true },
},
},
messageManagerGroups: ["browsers"],
},
},
},
},
{
name: "GeckoViewScroll",
onEnable: {
actors: {
ScrollDelegate: {
},
child: {
events: {
mozvisualscroll: { mozSystemGroup: true },
},
},
messageManagerGroups: ["browsers"],
},
},
},
},
{
name: "GeckoViewSelectionAction",
onEnable: {
actors: {
SelectionActionDelegate: {
},
child: {
esModuleURI:
events: {
mozcaretstatechanged: { mozSystemGroup: true },
pagehide: { capture: true, mozSystemGroup: true },
deactivate: { mozSystemGroup: true },
},
},
allFrames: true,
messageManagerGroups: ["browsers"],
},
},
},
},
{
name: "GeckoViewSettings",
onInit: {
actors: {
GeckoViewSettings: {
},
},
},
},
},
{
name: "GeckoViewTab",
},
},
{
name: "GeckoViewContentBlocking",
},
},
{
name: "SessionStateAggregator",
},
},
{
name: "GeckoViewAutofill",
onInit: {
actors: {
GeckoViewAutoFill: {
},
child: {
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: {
actors: {
MediaControlDelegate: {
},
child: {
esModuleURI:
events: {
"MozDOMFullscreen:Entered": {},
"MozDOMFullscreen:Exited": {},
},
},
allFrames: true,
messageManagerGroups: ["browsers"],
},
},
},
},
{
name: "GeckoViewAutocomplete",
onInit: {
actors: {
FormAutofill: {
},
child: {
events: {
focusin: {},
DOMFormBeforeSubmit: {},
},
},
allFrames: true,
messageManagerGroups: ["browsers"],
},
},
},
},
{
name: "GeckoViewPrompter",
onInit: {
actors: {
GeckoViewPrompter: {
},
},
allFrames: true,
includeChrome: true,
},
},
},
},
{
name: "GeckoViewPrintDelegate",
onInit: {
actors: {
GeckoViewPrintDelegate: {
},
},
allFrames: true,
},
},
},
},
{
name: "GeckoViewExperimentDelegate",
onInit: {
actors: {
GeckoViewExperimentDelegate: {
},
allFrames: true,
},
},
},
},
{
name: "GeckoViewTranslations",
},
},
]);
if (!Services.appinfo.sessionHistoryInParent) {
browser.prepareToChangeRemoteness = () =>
ModuleManager.prepareToChangeRemoteness();
browser.afterChangeRemoteness = switchId =>
ModuleManager.afterBrowserRemotenessChange(switchId);
}
browser.addEventListener("WillChangeBrowserRemoteness", () =>
ModuleManager.willChangeBrowserRemoteness()
);
browser.addEventListener("DidChangeBrowserRemoteness", () =>
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());
}