diff options
Diffstat (limited to '')
-rw-r--r-- | mobile/android/chrome/geckoview/SessionStateAggregator.js | 676 | ||||
-rw-r--r-- | mobile/android/chrome/geckoview/config.js | 719 | ||||
-rw-r--r-- | mobile/android/chrome/geckoview/config.xhtml | 83 | ||||
-rw-r--r-- | mobile/android/chrome/geckoview/geckoview.js | 891 | ||||
-rw-r--r-- | mobile/android/chrome/geckoview/geckoview.xhtml | 14 | ||||
-rw-r--r-- | mobile/android/chrome/geckoview/jar.mn | 14 | ||||
-rw-r--r-- | mobile/android/chrome/geckoview/moz.build | 7 |
7 files changed, 2404 insertions, 0 deletions
diff --git a/mobile/android/chrome/geckoview/SessionStateAggregator.js b/mobile/android/chrome/geckoview/SessionStateAggregator.js new file mode 100644 index 0000000000..193d27e3fa --- /dev/null +++ b/mobile/android/chrome/geckoview/SessionStateAggregator.js @@ -0,0 +1,676 @@ +/* -*- 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/. */ + +/* eslint-env mozilla/frame-script */ + +"use strict"; + +const { GeckoViewChildModule } = ChromeUtils.import( + "resource://gre/modules/GeckoViewChildModule.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + SessionHistory: "resource://gre/modules/sessionstore/SessionHistory.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeoutWithTarget: "resource://gre/modules/Timer.sys.mjs", +}); + +const NO_INDEX = Number.MAX_SAFE_INTEGER; +const LAST_INDEX = Number.MAX_SAFE_INTEGER - 1; +const DEFAULT_INTERVAL_MS = 1500; + +// This pref controls whether or not we send updates to the parent on a timeout +// or not, and should only be used for tests or debugging. +const TIMEOUT_DISABLED_PREF = "browser.sessionstore.debug.no_auto_updates"; + +const PREF_INTERVAL = "browser.sessionstore.interval"; +const PREF_SESSION_COLLECTION = "browser.sessionstore.platform_collection"; + +class Handler { + constructor(store) { + this.store = store; + } + + get mm() { + return this.store.mm; + } + + get eventDispatcher() { + return this.store.eventDispatcher; + } + + get messageQueue() { + return this.store.messageQueue; + } + + get stateChangeNotifier() { + return this.store.stateChangeNotifier; + } +} + +/** + * Listens for state change notifcations from webProgress and notifies each + * registered observer for either the start of a page load, or its completion. + */ +class StateChangeNotifier extends Handler { + constructor(store) { + super(store); + + this._observers = new Set(); + const ifreq = this.mm.docShell.QueryInterface(Ci.nsIInterfaceRequestor); + const webProgress = ifreq.getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener( + this, + Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT + ); + } + + /** + * Adds a given observer |obs| to the set of observers that will be notified + * when when a new document starts or finishes loading. + * + * @param obs (object) + */ + addObserver(obs) { + this._observers.add(obs); + } + + /** + * Notifies all observers that implement the given |method|. + * + * @param method (string) + */ + notifyObservers(method) { + for (const obs of this._observers) { + if (typeof obs[method] == "function") { + obs[method](); + } + } + } + + /** + * @see nsIWebProgressListener.onStateChange + */ + onStateChange(webProgress, request, stateFlags, status) { + // Ignore state changes for subframes because we're only interested in the + // top-document starting or stopping its load. + if (!webProgress.isTopLevel || webProgress.DOMWindow != this.mm.content) { + return; + } + + // onStateChange will be fired when loading the initial about:blank URI for + // a browser, which we don't actually care about. This is particularly for + // the case of unrestored background tabs, where the content has not yet + // been restored: we don't want to accidentally send any updates to the + // parent when the about:blank placeholder page has loaded. + if (!this.mm.docShell.hasLoadedNonBlankURI) { + return; + } + + if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { + this.notifyObservers("onPageLoadStarted"); + } else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this.notifyObservers("onPageLoadCompleted"); + } + } +} +StateChangeNotifier.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", +]); + +/** + * Listens for changes to the session history. Whenever the user navigates + * we will collect URLs and everything belonging to session history. + * + * Causes a SessionStore:update message to be sent that contains the current + * session history. + * + * Example: + * {entries: [{url: "about:mozilla", ...}, ...], index: 1} + */ +class SessionHistoryListener extends Handler { + constructor(store) { + super(store); + + this._fromIdx = NO_INDEX; + + // The state change observer is needed to handle initial subframe loads. + // It will redundantly invalidate with the SHistoryListener in some cases + // but these invalidations are very cheap. + this.stateChangeNotifier.addObserver(this); + + // By adding the SHistoryListener immediately, we will unfortunately be + // notified of every history entry as the tab is restored. We don't bother + // waiting to add the listener later because these notifications are cheap. + // We will likely only collect once since we are batching collection on + // a delay. + this.mm.docShell + .QueryInterface(Ci.nsIWebNavigation) + .sessionHistory.legacySHistory.addSHistoryListener(this); + + // Listen for page title changes. + this.mm.addEventListener("DOMTitleChanged", this); + } + + uninit() { + const sessionHistory = this.mm.docShell.QueryInterface(Ci.nsIWebNavigation) + .sessionHistory; + if (sessionHistory) { + sessionHistory.legacySHistory.removeSHistoryListener(this); + } + } + + collect() { + // We want to send down a historychange even for full collects in case our + // session history is a partial session history, in which case we don't have + // enough information for a full update. collectFrom(-1) tells the collect + // function to collect all data avaliable in this process. + if (this.mm.docShell) { + this.collectFrom(-1); + } + } + + // History can grow relatively big with the nested elements, so if we don't have to, we + // don't want to send the entire history all the time. For a simple optimization + // we keep track of the smallest index from after any change has occured and we just send + // the elements from that index. If something more complicated happens we just clear it + // and send the entire history. We always send the additional info like the current selected + // index (so for going back and forth between history entries we set the index to LAST_INDEX + // if nothing else changed send an empty array and the additonal info like the selected index) + collectFrom(idx) { + if (this._fromIdx <= idx) { + // If we already know that we need to update history fromn index N we can ignore any changes + // tha happened with an element with index larger than N. + // Note: initially we use NO_INDEX which is MAX_SAFE_INTEGER which means we don't ignore anything + // here, and in case of navigation in the history back and forth we use LAST_INDEX which ignores + // only the subsequent navigations, but not any new elements added. + return; + } + + this._fromIdx = idx; + this.messageQueue.push("historychange", () => { + if (this._fromIdx === NO_INDEX) { + return null; + } + + const history = SessionHistory.collect(this.mm.docShell, this._fromIdx); + this._fromIdx = NO_INDEX; + return history; + }); + } + + handleEvent(event) { + this.collect(); + } + + onPageLoadCompleted() { + this.collect(); + } + + onPageLoadStarted() { + this.collect(); + } + + OnHistoryNewEntry(newURI, oldIndex) { + // We ought to collect the previously current entry as well, see bug 1350567. + // TODO: Reenable partial history collection for performance + // this.collectFrom(oldIndex); + this.collect(); + } + + OnHistoryGotoIndex(index, gotoURI) { + // We ought to collect the previously current entry as well, see bug 1350567. + // TODO: Reenable partial history collection for performance + // this.collectFrom(LAST_INDEX); + this.collect(); + } + + OnHistoryPurge(numEntries) { + this.collect(); + } + + OnHistoryReload(reloadURI, reloadFlags) { + this.collect(); + return true; + } + + OnHistoryReplaceEntry(index) { + this.collect(); + } +} +SessionHistoryListener.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsISHistoryListener", + "nsISupportsWeakReference", +]); + +/** + * Listens for scroll position changes. Whenever the user scrolls the top-most + * frame we update the scroll position and will restore it when requested. + * + * Causes a SessionStore:update message to be sent that contains the current + * scroll positions as a tree of strings. If no frame of the whole frame tree + * is scrolled this will return null so that we don't tack a property onto + * the tabData object in the parent process. + * + * Example: + * {scroll: "100,100", zoom: {resolution: "1.5", displaySize: + * {height: "1600", width: "1000"}}, children: + * [null, null, {scroll: "200,200"}]} + */ +class ScrollPositionListener extends Handler { + constructor(store) { + super(store); + + SessionStoreUtils.addDynamicFrameFilteredListener( + this.mm, + "mozvisualscroll", + this, + /* capture */ false, + /* system group */ true + ); + + SessionStoreUtils.addDynamicFrameFilteredListener( + this.mm, + "mozvisualresize", + this, + /* capture */ false, + /* system group */ true + ); + + this.stateChangeNotifier.addObserver(this); + } + + handleEvent() { + this.messageQueue.push("scroll", () => this.collect()); + } + + onPageLoadCompleted() { + this.messageQueue.push("scroll", () => this.collect()); + } + + onPageLoadStarted() { + this.messageQueue.push("scroll", () => null); + } + + collect() { + // TODO: Keep an eye on bug 1525259; we may not have to manually store zoom + // Save the current document resolution. + let zoom = 1; + const scrolldata = + SessionStoreUtils.collectScrollPosition(this.mm.content) || {}; + const domWindowUtils = this.mm.content.windowUtils; + zoom = domWindowUtils.getResolution(); + scrolldata.zoom = {}; + scrolldata.zoom.resolution = zoom; + + // Save some data that'll help in adjusting the zoom level + // when restoring in a different screen orientation. + const displaySize = {}; + const width = {}, + height = {}; + domWindowUtils.getContentViewerSize(width, height); + + displaySize.width = width.value; + displaySize.height = height.value; + + scrolldata.zoom.displaySize = displaySize; + + return scrolldata; + } +} + +/** + * Listens for changes to input elements. Whenever the value of an input + * element changes we will re-collect data for the current frame tree and send + * a message to the parent process. + * + * Causes a SessionStore:update message to be sent that contains the form data + * for all reachable frames. + * + * Example: + * { + * formdata: {url: "http://mozilla.org/", id: {input_id: "input value"}}, + * children: [ + * null, + * {url: "http://sub.mozilla.org/", id: {input_id: "input value 2"}} + * ] + * } + */ +class FormDataListener extends Handler { + constructor(store) { + super(store); + + SessionStoreUtils.addDynamicFrameFilteredListener( + this.mm, + "input", + this, + true + ); + this.stateChangeNotifier.addObserver(this); + } + + handleEvent() { + this.messageQueue.push("formdata", () => this.collect()); + } + + onPageLoadStarted() { + this.messageQueue.push("formdata", () => null); + } + + collect() { + return SessionStoreUtils.collectFormData(this.mm.content); + } +} + +/** + * A message queue that takes collected data and will take care of sending it + * to the chrome process. It allows flushing using synchronous messages and + * takes care of any race conditions that might occur because of that. Changes + * will be batched if they're pushed in quick succession to avoid a message + * flood. + */ +class MessageQueue extends Handler { + constructor(store) { + super(store); + + /** + * A map (string -> lazy fn) holding lazy closures of all queued data + * collection routines. These functions will return data collected from the + * docShell. + */ + this._data = new Map(); + + /** + * The delay (in ms) used to delay sending changes after data has been + * invalidated. + */ + this.BATCH_DELAY_MS = 1000; + + /** + * The minimum idle period (in ms) we need for sending data to chrome process. + */ + this.NEEDED_IDLE_PERIOD_MS = 5; + + /** + * Timeout for waiting an idle period to send data. We will set this from + * the pref "browser.sessionstore.interval". + */ + this._timeoutWaitIdlePeriodMs = null; + + /** + * The current timeout ID, null if there is no queue data. We use timeouts + * to damp a flood of data changes and send lots of changes as one batch. + */ + this._timeout = null; + + /** + * Whether or not sending batched messages on a timer is disabled. This should + * only be used for debugging or testing. If you need to access this value, + * you should probably use the timeoutDisabled getter. + */ + this._timeoutDisabled = false; + + /** + * True if there is already a send pending idle dispatch, set to prevent + * scheduling more than one. If false there may or may not be one scheduled. + */ + this._idleScheduled = false; + + this.timeoutDisabled = Services.prefs.getBoolPref( + TIMEOUT_DISABLED_PREF, + false + ); + this._timeoutWaitIdlePeriodMs = Services.prefs.getIntPref( + PREF_INTERVAL, + DEFAULT_INTERVAL_MS + ); + + Services.prefs.addObserver(TIMEOUT_DISABLED_PREF, this); + Services.prefs.addObserver(PREF_INTERVAL, this); + } + + /** + * True if batched messages are not being fired on a timer. This should only + * ever be true when debugging or during tests. + */ + get timeoutDisabled() { + return this._timeoutDisabled; + } + + /** + * Disables sending batched messages on a timer. Also cancels any pending + * timers. + */ + set timeoutDisabled(val) { + this._timeoutDisabled = val; + + if (val && this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + } + + uninit() { + this.cleanupTimers(); + } + + /** + * Cleanup pending idle callback and timer. + */ + cleanupTimers() { + this._idleScheduled = false; + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + } + + observe(subject, topic, data) { + if (topic == "nsPref:changed") { + switch (data) { + case TIMEOUT_DISABLED_PREF: + this.timeoutDisabled = Services.prefs.getBoolPref( + TIMEOUT_DISABLED_PREF, + false + ); + break; + case PREF_INTERVAL: + this._timeoutWaitIdlePeriodMs = Services.prefs.getIntPref( + PREF_INTERVAL, + DEFAULT_INTERVAL_MS + ); + break; + default: + debug`Received unknown message: ${data}`; + break; + } + } + } + + /** + * Pushes a given |value| onto the queue. The given |key| represents the type + * of data that is stored and can override data that has been queued before + * but has not been sent to the parent process, yet. + * + * @param key (string) + * A unique identifier specific to the type of data this is passed. + * @param fn (function) + * A function that returns the value that will be sent to the parent + * process. + */ + push(key, fn) { + this._data.set(key, fn); + + if (!this._timeout && !this._timeoutDisabled) { + // Wait a little before sending the message to batch multiple changes. + this._timeout = setTimeoutWithTarget( + () => this.sendWhenIdle(), + this.BATCH_DELAY_MS, + this.mm.tabEventTarget + ); + } + } + + /** + * Sends queued data when the remaining idle time is enough or waiting too + * long; otherwise, request an idle time again. If the |deadline| is not + * given, this function is going to schedule the first request. + * + * @param deadline (object) + * An IdleDeadline object passed by idleDispatch(). + */ + sendWhenIdle(deadline) { + if (!this.mm.content) { + // The frameloader is being torn down. Nothing more to do. + return; + } + + if (deadline) { + if ( + deadline.didTimeout || + deadline.timeRemaining() > this.NEEDED_IDLE_PERIOD_MS + ) { + this.send(); + return; + } + } else if (this._idleScheduled) { + // Bail out if there's a pending run. + return; + } + ChromeUtils.idleDispatch(deadline_ => this.sendWhenIdle(deadline_), { + timeout: this._timeoutWaitIdlePeriodMs, + }); + this._idleScheduled = true; + } + + /** + * Sends queued data to the chrome process. + * + * @param options (object) + * {isFinal: true} to signal this is the final message sent on unload + */ + send(options = {}) { + // Looks like we have been called off a timeout after the tab has been + // closed. The docShell is gone now and we can just return here as there + // is nothing to do. + if (!this.mm.docShell) { + return; + } + + this.cleanupTimers(); + + const data = {}; + for (const [key, func] of this._data) { + const value = func(); + + if (value || (key != "storagechange" && key != "historychange")) { + data[key] = value; + } + } + + this._data.clear(); + + try { + // Send all data to the parent process. + this.eventDispatcher.sendRequest({ + type: "GeckoView:StateUpdated", + data, + isFinal: options.isFinal || false, + epoch: this.store.epoch, + }); + } catch (ex) { + if (ex && ex.result == Cr.NS_ERROR_OUT_OF_MEMORY) { + warn`Failed to save session state`; + } + } + } +} + +class SessionStateAggregator extends GeckoViewChildModule { + constructor(aModuleName, aMessageManager) { + super(aModuleName, aMessageManager); + + this.mm = aMessageManager; + this.messageQueue = new MessageQueue(this); + this.stateChangeNotifier = new StateChangeNotifier(this); + + this.handlers = [ + new SessionHistoryListener(this), + this.stateChangeNotifier, + this.messageQueue, + ]; + + if (!Services.prefs.getBoolPref(PREF_SESSION_COLLECTION, false)) { + this.handlers.push( + new FormDataListener(this), + new ScrollPositionListener(this) + ); + } + + this.messageManager.addMessageListener("GeckoView:FlushSessionState", this); + } + + receiveMessage(aMsg) { + debug`receiveMessage: ${aMsg.name}`; + + switch (aMsg.name) { + case "GeckoView:FlushSessionState": + this.flush(); + break; + } + } + + flush() { + // Flush the message queue, send the latest updates. + this.messageQueue.send(); + } + + onUnload() { + // Upon frameLoader destruction, send a final update message to + // the parent and flush all data currently held in the child. + this.messageQueue.send({ isFinal: true }); + + for (const handler of this.handlers) { + if (handler.uninit) { + handler.uninit(); + } + } + + // We don't need to take care of any StateChangeNotifier observers as they + // will die with the content script. + } +} + +// TODO: Bug 1648158 Move SessionAggregator to the parent process +class DummySessionStateAggregator extends GeckoViewChildModule { + constructor(aModuleName, aMessageManager) { + super(aModuleName, aMessageManager); + this.messageManager.addMessageListener("GeckoView:FlushSessionState", this); + } + + receiveMessage(aMsg) { + debug`receiveMessage: ${aMsg.name}`; + + switch (aMsg.name) { + case "GeckoView:FlushSessionState": + // Do nothing + break; + } + } +} + +const { debug, warn } = SessionStateAggregator.initLogging( + "SessionStateAggregator" +); + +const module = Services.appinfo.sessionHistoryInParent + ? // If history is handled in the parent we don't need a session aggregator + // TODO: Bug 1648158 remove this and do everything in the parent + DummySessionStateAggregator.create(this) + : SessionStateAggregator.create(this); diff --git a/mobile/android/chrome/geckoview/config.js b/mobile/android/chrome/geckoview/config.js new file mode 100644 index 0000000000..3c596022ab --- /dev/null +++ b/mobile/android/chrome/geckoview/config.js @@ -0,0 +1,719 @@ +/* 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 Cm = Components.manager; + +const VKB_ENTER_KEY = 13; // User press of VKB enter key +const INITIAL_PAGE_DELAY = 500; // Initial pause on program start for scroll alignment +const PREFS_BUFFER_MAX = 30; // Max prefs buffer size for getPrefsBuffer() +const PAGE_SCROLL_TRIGGER = 200; // Triggers additional getPrefsBuffer() on user scroll-to-bottom +const FILTER_CHANGE_TRIGGER = 200; // Delay between responses to filterInput changes +const INNERHTML_VALUE_DELAY = 100; // Delay before providing prefs innerHTML value + +var gClipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService( + Ci.nsIClipboardHelper +); + +/* ============================== NewPrefDialog ============================== + * + * New Preference Dialog Object and methods + * + * Implements User Interfaces for creation of a single(new) Preference setting + * + */ +var NewPrefDialog = { + _prefsShield: null, + + _newPrefsDialog: null, + _newPrefItem: null, + _prefNameInputElt: null, + _prefTypeSelectElt: null, + + _booleanValue: null, + _booleanToggle: null, + _stringValue: null, + _intValue: null, + + _positiveButton: null, + + get type() { + return this._prefTypeSelectElt.value; + }, + + set type(aType) { + this._prefTypeSelectElt.value = aType; + switch (this._prefTypeSelectElt.value) { + case "boolean": + this._prefTypeSelectElt.selectedIndex = 0; + break; + case "string": + this._prefTypeSelectElt.selectedIndex = 1; + break; + case "int": + this._prefTypeSelectElt.selectedIndex = 2; + break; + } + + this._newPrefItem.setAttribute("typestyle", aType); + }, + + // Init the NewPrefDialog + init: function AC_init() { + this._prefsShield = document.getElementById("prefs-shield"); + + this._newPrefsDialog = document.getElementById("new-pref-container"); + this._newPrefItem = document.getElementById("new-pref-item"); + this._prefNameInputElt = document.getElementById("new-pref-name"); + this._prefTypeSelectElt = document.getElementById("new-pref-type"); + + this._booleanValue = document.getElementById("new-pref-value-boolean"); + this._stringValue = document.getElementById("new-pref-value-string"); + this._intValue = document.getElementById("new-pref-value-int"); + + this._positiveButton = document.getElementById("positive-button"); + }, + + // Called to update positive button to display text ("Create"/"Change), and enabled/disabled status + // As new pref name is initially displayed, re-focused, or modifed during user input + _updatePositiveButton: function AC_updatePositiveButton(aPrefName) { + document.l10n.setAttributes( + this._positiveButton, + "config-new-pref-create-button" + ); + this._positiveButton.setAttribute("disabled", true); + if (aPrefName == "") { + return; + } + + // If item already in list, it's being changed, else added + const item = AboutConfig._list.filter(i => { + return i.name == aPrefName; + }); + if (item.length) { + document.l10n.setAttributes( + this._positiveButton, + "config-new-pref-change-button" + ); + } else { + this._positiveButton.removeAttribute("disabled"); + } + }, + + // When we want to cancel/hide an existing, or show a new pref dialog + toggleShowHide: function AC_toggleShowHide() { + if (this._newPrefsDialog.classList.contains("show")) { + this.hide(); + } else { + this._show(); + } + }, + + // When we want to show the new pref dialog / shield the prefs list + _show: function AC_show() { + this._newPrefsDialog.classList.add("show"); + this._prefsShield.setAttribute("shown", true); + + // Initial default field values + this._prefNameInputElt.value = ""; + this._updatePositiveButton(this._prefNameInputElt.value); + + this.type = "boolean"; + this._booleanValue.value = "false"; + this._stringValue.value = ""; + this._intValue.value = ""; + + this._prefNameInputElt.focus(); + + window.addEventListener("keypress", this.handleKeypress); + }, + + // When we want to cancel/hide the new pref dialog / un-shield the prefs list + hide: function AC_hide() { + this._newPrefsDialog.classList.remove("show"); + this._prefsShield.removeAttribute("shown"); + + window.removeEventListener("keypress", this.handleKeypress); + }, + + // Watch user key input so we can provide Enter key action, commit input values + handleKeypress: function AC_handleKeypress(aEvent) { + // Close our VKB on new pref enter key press + if (aEvent.keyCode == VKB_ENTER_KEY) { + aEvent.target.blur(); + } + }, + + // New prefs create dialog only allows creating a non-existing preference, doesn't allow for + // Changing an existing one on-the-fly, tap existing/displayed line item pref for that + create: function AC_create(aEvent) { + if (this._positiveButton.getAttribute("disabled") == "true") { + return; + } + + switch (this.type) { + case "boolean": + Services.prefs.setBoolPref( + this._prefNameInputElt.value, + !!(this._booleanValue.value == "true") + ); + break; + case "string": + Services.prefs.setCharPref( + this._prefNameInputElt.value, + this._stringValue.value + ); + break; + case "int": + Services.prefs.setIntPref( + this._prefNameInputElt.value, + this._intValue.value + ); + break; + } + + // Ensure pref adds flushed to disk immediately + Services.prefs.savePrefFile(null); + + this.hide(); + }, + + // Display proper positive button text/state on new prefs name input focus + focusName: function AC_focusName(aEvent) { + this._updatePositiveButton(aEvent.target.value); + }, + + // Display proper positive button text/state as user changes new prefs name + updateName: function AC_updateName(aEvent) { + this._updatePositiveButton(aEvent.target.value); + }, + + // In new prefs dialog, bool prefs are <input type="text">, as they aren't yet tied to an + // Actual Services.prefs.*etBoolPref() + toggleBoolValue: function AC_toggleBoolValue() { + this._booleanValue.value = + this._booleanValue.value == "true" ? "false" : "true"; + }, +}; + +/* ============================== AboutConfig ============================== + * + * Main AboutConfig object and methods + * + * Implements User Interfaces for maintenance of a list of Preference settings + * + */ +var AboutConfig = { + contextMenuLINode: null, + filterInput: null, + _filterPrevInput: null, + _filterChangeTimer: null, + _prefsContainer: null, + _loadingContainer: null, + _list: null, + + // Init the main AboutConfig dialog + init: function AC_init() { + this.filterInput = document.getElementById("filter-input"); + this._prefsContainer = document.getElementById("prefs-container"); + this._loadingContainer = document.getElementById("loading-container"); + + const list = Services.prefs.getChildList(""); + this._list = list.sort().map(function AC_getMapPref(aPref) { + return new Pref(aPref); + }, this); + + // Support filtering about:config via a ?filter=<string> param + const match = /[?&]filter=([^&]+)/i.exec(window.location.href); + if (match) { + this.filterInput.value = decodeURIComponent(match[1]); + } + + // Display the current prefs list (retains searchFilter value) + this.bufferFilterInput(); + + // Setup the prefs observers + Services.prefs.addObserver("", this); + }, + + // Uninit the main AboutConfig dialog + uninit: function AC_uninit() { + // Remove the prefs observer + Services.prefs.removeObserver("", this); + }, + + // Clear the filterInput value, to display the entire list + clearFilterInput: function AC_clearFilterInput() { + this.filterInput.value = ""; + this.bufferFilterInput(); + }, + + // Buffer down rapid changes in filterInput value from keyboard + bufferFilterInput: function AC_bufferFilterInput() { + if (this._filterChangeTimer) { + clearTimeout(this._filterChangeTimer); + } + + this._filterChangeTimer = setTimeout(() => { + this._filterChangeTimer = null; + // Display updated prefs list when filterInput value settles + this._displayNewList(); + }, FILTER_CHANGE_TRIGGER); + }, + + // Update displayed list when filterInput value changes + _displayNewList: function AC_displayNewList() { + // This survives the search filter value past a page refresh + this.filterInput.setAttribute("value", this.filterInput.value); + + // Don't start new filter search if same as last + if (this.filterInput.value == this._filterPrevInput) { + return; + } + this._filterPrevInput = this.filterInput.value; + + // Clear list item selection / context menu, prefs list, get first buffer, set scrolling on + this.selected = ""; + this._clearPrefsContainer(); + this._addMorePrefsToContainer(); + window.onscroll = this.onScroll.bind(this); + + // Pause for screen to settle, then ensure at top + setTimeout(() => { + window.scrollTo(0, 0); + }, INITIAL_PAGE_DELAY); + }, + + // Clear the displayed preferences list + _clearPrefsContainer: function AC_clearPrefsContainer() { + // Quick clear the prefsContainer list + const empty = this._prefsContainer.cloneNode(false); + this._prefsContainer.parentNode.replaceChild(empty, this._prefsContainer); + this._prefsContainer = empty; + + // Quick clear the prefs li.HTML list + this._list.forEach(function(item) { + delete item.li; + }); + }, + + // Get a small manageable block of prefs items, and add them to the displayed list + _addMorePrefsToContainer: function AC_addMorePrefsToContainer() { + // Create filter regex + const filterExp = this.filterInput.value + ? new RegExp(this.filterInput.value, "i") + : null; + + // Get a new block for the display list + const prefsBuffer = []; + for ( + let i = 0; + i < this._list.length && prefsBuffer.length < PREFS_BUFFER_MAX; + i++ + ) { + if (!this._list[i].li && this._list[i].test(filterExp)) { + prefsBuffer.push(this._list[i]); + } + } + + // Add the new block to the displayed list + for (let i = 0; i < prefsBuffer.length; i++) { + this._prefsContainer.appendChild(prefsBuffer[i].getOrCreateNewLINode()); + } + + // Determine if anything left to add later by scrolling + let anotherPrefsBufferRemains = false; + for (let i = 0; i < this._list.length; i++) { + if (!this._list[i].li && this._list[i].test(filterExp)) { + anotherPrefsBufferRemains = true; + break; + } + } + + if (anotherPrefsBufferRemains) { + // If still more could be displayed, show the throbber + this._loadingContainer.style.display = "block"; + } else { + // If no more could be displayed, hide the throbber, and stop noticing scroll events + this._loadingContainer.style.display = "none"; + window.onscroll = null; + } + }, + + // If scrolling at the bottom, maybe add some more entries + onScroll: function AC_onScroll(aEvent) { + if ( + this._prefsContainer.scrollHeight - + (window.pageYOffset + window.innerHeight) < + PAGE_SCROLL_TRIGGER + ) { + if (!this._filterChangeTimer) { + this._addMorePrefsToContainer(); + } + } + }, + + // Return currently selected list item node + get selected() { + return document.querySelector(".pref-item.selected"); + }, + + // Set list item node as selected + set selected(aSelection) { + const currentSelection = this.selected; + if (aSelection == currentSelection) { + return; + } + + // Clear any previous selection + if (currentSelection) { + currentSelection.classList.remove("selected"); + currentSelection.removeEventListener("keypress", this.handleKeypress); + } + + // Set any current selection + if (aSelection) { + aSelection.classList.add("selected"); + aSelection.addEventListener("keypress", this.handleKeypress); + } + }, + + // Watch user key input so we can provide Enter key action, commit input values + handleKeypress: function AC_handleKeypress(aEvent) { + if (aEvent.keyCode == VKB_ENTER_KEY) { + aEvent.target.blur(); + } + }, + + // Return the target list item node of an action event + getLINodeForEvent: function AC_getLINodeForEvent(aEvent) { + let node = aEvent.target; + while (node && node.nodeName != "li") { + node = node.parentNode; + } + + return node; + }, + + // Return a pref of a list item node + _getPrefForNode: function AC_getPrefForNode(aNode) { + const pref = aNode.getAttribute("name"); + + return new Pref(pref); + }, + + // When list item name or value are tapped + selectOrToggleBoolPref: function AC_selectOrToggleBoolPref(aEvent) { + const node = this.getLINodeForEvent(aEvent); + + // If not already selected, just do so + if (this.selected != node) { + this.selected = node; + return; + } + + // If already selected, and value is boolean, toggle it + const pref = this._getPrefForNode(node); + if (pref.type != Services.prefs.PREF_BOOL) { + return; + } + + this.toggleBoolPref(aEvent); + }, + + // When finalizing list input values due to blur + setIntOrStringPref: function AC_setIntOrStringPref(aEvent) { + const node = this.getLINodeForEvent(aEvent); + + // Skip if locked + const pref = this._getPrefForNode(node); + if (pref.locked) { + return; + } + + // Boolean inputs blur to remove focus from "button" + if (pref.type == Services.prefs.PREF_BOOL) { + return; + } + + // String and Int inputs change / commit on blur + pref.value = aEvent.target.value; + }, + + // When we reset a pref to it's default value (note resetting a user created pref will delete it) + resetDefaultPref: function AC_resetDefaultPref(aEvent) { + const node = this.getLINodeForEvent(aEvent); + + // If not already selected, do so + if (this.selected != node) { + this.selected = node; + } + + // Reset will handle any locked condition + const pref = this._getPrefForNode(node); + pref.reset(); + + // Ensure pref reset flushed to disk immediately + Services.prefs.savePrefFile(null); + }, + + // When we want to toggle a bool pref + toggleBoolPref: function AC_toggleBoolPref(aEvent) { + const node = this.getLINodeForEvent(aEvent); + + // Skip if locked, or not boolean + const pref = this._getPrefForNode(node); + if (pref.locked) { + return; + } + + // Toggle, and blur to remove field focus + pref.value = !pref.value; + aEvent.target.blur(); + }, + + // When Int inputs have their Up or Down arrows toggled + incrOrDecrIntPref: function AC_incrOrDecrIntPref(aEvent, aInt) { + const node = this.getLINodeForEvent(aEvent); + + // Skip if locked + const pref = this._getPrefForNode(node); + if (pref.locked) { + return; + } + + pref.value += aInt; + }, + + // Observe preference changes + observe: function AC_observe(aSubject, aTopic, aPrefName) { + const pref = new Pref(aPrefName); + + // Ignore uninteresting changes, and avoid "private" preferences + if (aTopic != "nsPref:changed") { + return; + } + + // If pref type invalid, refresh display as user reset/removed an item from the list + if (pref.type == Services.prefs.PREF_INVALID) { + document.location.reload(); + return; + } + + // If pref onscreen, update in place. + const item = document.querySelector( + '.pref-item[name="' + CSS.escape(pref.name) + '"]' + ); + if (item) { + item.setAttribute("value", pref.value); + const input = item.querySelector("input"); + input.setAttribute("value", pref.value); + input.value = pref.value; + + pref.default + ? item.querySelector(".reset").setAttribute("disabled", "true") + : item.querySelector(".reset").removeAttribute("disabled"); + return; + } + + // If pref not already in list, refresh display as it's being added + const anyWhere = this._list.filter(i => { + return i.name == pref.name; + }); + if (!anyWhere.length) { + document.location.reload(); + } + }, + + // Quick context menu helpers for about:config + clipboardCopy: function AC_clipboardCopy(aField) { + const pref = this._getPrefForNode(this.contextMenuLINode); + if (aField == "name") { + gClipboardHelper.copyString(pref.name); + } else { + gClipboardHelper.copyString(pref.value); + } + }, +}; + +/* ============================== Pref ============================== + * + * Individual Preference object / methods + * + * Defines a Pref object, a document list item tied to Preferences Services + * And the methods by which they interact. + * + */ +function Pref(aName) { + this.name = aName; +} + +Pref.prototype = { + get type() { + return Services.prefs.getPrefType(this.name); + }, + + get value() { + switch (this.type) { + case Services.prefs.PREF_BOOL: + return Services.prefs.getBoolPref(this.name); + case Services.prefs.PREF_INT: + return Services.prefs.getIntPref(this.name); + case Services.prefs.PREF_STRING: + default: + return Services.prefs.getCharPref(this.name); + } + }, + set value(aPrefValue) { + switch (this.type) { + case Services.prefs.PREF_BOOL: + Services.prefs.setBoolPref(this.name, aPrefValue); + break; + case Services.prefs.PREF_INT: + Services.prefs.setIntPref(this.name, aPrefValue); + break; + case Services.prefs.PREF_STRING: + default: + Services.prefs.setCharPref(this.name, aPrefValue); + } + + // Ensure pref change flushed to disk immediately + Services.prefs.savePrefFile(null); + }, + + get default() { + return !Services.prefs.prefHasUserValue(this.name); + }, + + get locked() { + return Services.prefs.prefIsLocked(this.name); + }, + + reset: function AC_reset() { + Services.prefs.clearUserPref(this.name); + }, + + test: function AC_test(aValue) { + return aValue ? aValue.test(this.name) : true; + }, + + // Get existing or create new LI node for the pref + getOrCreateNewLINode: function AC_getOrCreateNewLINode() { + if (!this.li) { + this.li = document.createElement("li"); + + this.li.className = "pref-item"; + this.li.setAttribute("name", this.name); + + // Click callback to ensure list item selected even on no-action tap events + this.li.addEventListener("click", function(aEvent) { + AboutConfig.selected = AboutConfig.getLINodeForEvent(aEvent); + }); + + // Contextmenu callback to identify selected list item + this.li.addEventListener("contextmenu", function(aEvent) { + AboutConfig.contextMenuLINode = AboutConfig.getLINodeForEvent(aEvent); + }); + + this.li.setAttribute("contextmenu", "prefs-context-menu"); + + const prefName = document.createElement("div"); + prefName.className = "pref-name"; + prefName.addEventListener("click", function(event) { + AboutConfig.selectOrToggleBoolPref(event); + }); + prefName.textContent = this.name; + + this.li.appendChild(prefName); + + const prefItemLine = document.createElement("div"); + prefItemLine.className = "pref-item-line"; + + const prefValue = document.createElement("input"); + prefValue.className = "pref-value"; + prefValue.addEventListener("blur", function(event) { + AboutConfig.setIntOrStringPref(event); + }); + prefValue.addEventListener("click", function(event) { + AboutConfig.selectOrToggleBoolPref(event); + }); + prefValue.value = ""; + prefItemLine.appendChild(prefValue); + + const resetButton = document.createElement("div"); + resetButton.className = "pref-button reset"; + resetButton.addEventListener("click", function(event) { + AboutConfig.resetDefaultPref(event); + }); + resetButton.setAttribute("data-l10n-id", "config-pref-reset-button"); + prefItemLine.appendChild(resetButton); + + const toggleButton = document.createElement("div"); + toggleButton.className = "pref-button toggle"; + toggleButton.addEventListener("click", function(event) { + AboutConfig.toggleBoolPref(event); + }); + toggleButton.setAttribute("data-l10n-id", "config-pref-toggle-button"); + prefItemLine.appendChild(toggleButton); + + const upButton = document.createElement("div"); + upButton.className = "pref-button up"; + upButton.addEventListener("click", function(event) { + AboutConfig.incrOrDecrIntPref(event, 1); + }); + prefItemLine.appendChild(upButton); + + const downButton = document.createElement("div"); + downButton.className = "pref-button down"; + downButton.addEventListener("click", function(event) { + AboutConfig.incrOrDecrIntPref(event, -1); + }); + prefItemLine.appendChild(downButton); + + this.li.appendChild(prefItemLine); + + // Delay providing the list item values, until the LI is returned and added to the document + setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY); + } + + return this.li; + }, + + // Initialize list item object values + _valueSetup: function AC_valueSetup() { + this.li.setAttribute("type", this.type); + this.li.setAttribute("value", this.value); + + const valDiv = this.li.querySelector(".pref-value"); + valDiv.value = this.value; + + switch (this.type) { + case Services.prefs.PREF_BOOL: + valDiv.setAttribute("type", "button"); + this.li.querySelector(".up").setAttribute("disabled", true); + this.li.querySelector(".down").setAttribute("disabled", true); + break; + case Services.prefs.PREF_STRING: + valDiv.setAttribute("type", "text"); + this.li.querySelector(".up").setAttribute("disabled", true); + this.li.querySelector(".down").setAttribute("disabled", true); + this.li.querySelector(".toggle").setAttribute("disabled", true); + break; + case Services.prefs.PREF_INT: + valDiv.setAttribute("type", "number"); + this.li.querySelector(".toggle").setAttribute("disabled", true); + break; + } + + this.li.setAttribute("default", this.default); + if (this.default) { + this.li.querySelector(".reset").setAttribute("disabled", true); + } + + if (this.locked) { + valDiv.setAttribute("disabled", this.locked); + this.li.querySelector(".pref-name").setAttribute("locked", true); + } + }, +}; diff --git a/mobile/android/chrome/geckoview/config.xhtml b/mobile/android/chrome/geckoview/config.xhtml new file mode 100644 index 0000000000..96a87a1dea --- /dev/null +++ b/mobile/android/chrome/geckoview/config.xhtml @@ -0,0 +1,83 @@ +<?xml version="1.0"?> + +<!-- 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/. --> + +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> + +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <meta name="viewport" content="width=device-width; user-scalable=0" /> + <title>about:config</title> + <meta charset="UTF-8" /> + + <link rel="localization" href="mobile/android/aboutConfig.ftl"/> + <link rel="stylesheet" href="chrome://geckoview/skin/config.css" type="text/css"/> + <script type="text/javascript" src="chrome://geckoview/content/config.js"></script> +</head> + +<body onload="NewPrefDialog.init(); AboutConfig.init();" + onunload="AboutConfig.uninit();"> + + <div class="toolbar"> + <div class="toolbar-container"> + <div id="new-pref-toggle-button" onclick="NewPrefDialog.toggleShowHide();"/> + + <div class="toolbar-item" id="filter-container"> + <div id="filter-search-button"/> + <input id="filter-input" type="search" data-l10n-id="config-toolbar-search" value="" + oninput="AboutConfig.bufferFilterInput();"/> + <div id="filter-input-clear-button" onclick="AboutConfig.clearFilterInput();"/> + </div> + </div> + </div> + + <div id="content" ontouchstart="AboutConfig.filterInput.blur();"> + + <div id="new-pref-container"> + <li class="pref-item" id="new-pref-item"> + <div class="pref-item-line"> + <input class="pref-name" id="new-pref-name" type="text" data-l10n-id="config-new-pref-name" + onfocus="NewPrefDialog.focusName(event);" + oninput="NewPrefDialog.updateName(event);"/> + <select id="new-pref-type" onchange="NewPrefDialog.type = event.target.value;"> + <option value="boolean" data-l10n-id="config-new-pref-value-boolean"></option> + <option value="string" data-l10n-id="config-new-pref-value-string"></option> + <option value="int" data-l10n-id="config-new-pref-value-integer"></option> + </select> + </div> + + <div class="pref-item-line" id="new-pref-line-boolean"> + <input class="pref-value" id="new-pref-value-boolean" disabled="disabled"/> + <div class="pref-button toggle" onclick="NewPrefDialog.toggleBoolValue();" data-l10n-id="config-pref-toggle-button"></div> + </div> + + <div class="pref-item-line" id="new-pref-line-input"> + <input class="pref-value" id="new-pref-value-string" data-l10n-id="config-new-pref-string"/> + <input class="pref-value" id="new-pref-value-int" data-l10n-id="config-new-pref-number" type="number"/> + </div> + + <div class="pref-item-line"> + <div class="pref-button cancel" id="negative-button" onclick="NewPrefDialog.hide();" data-l10n-id="config-new-pref-cancel-button"></div> + <div class="pref-button create" id="positive-button" onclick="NewPrefDialog.create(event);" data-l10n-id="config-new-pref-create-button"></div> + </div> + </li> + </div> + + <div id="prefs-shield"></div> + + <ul id="prefs-container"/> + + <div id="loading-container"></div> + + </div> + + <menu type="context" id="prefs-context-menu"> + <menuitem data-l10n-id="config-context-menu-copy-pref-name" onclick="AboutConfig.clipboardCopy('name');"></menuitem> + <menuitem data-l10n-id="config-context-menu-copy-pref-value" onclick="AboutConfig.clipboardCopy('value');"></menuitem> + </menu> + +</body> +</html> diff --git a/mobile/android/chrome/geckoview/geckoview.js b/mobile/android/chrome/geckoview/geckoview.js new file mode 100644 index 0000000000..c3e7150fd9 --- /dev/null +++ b/mobile/android/chrome/geckoview/geckoview.js @@ -0,0 +1,891 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { DelayedInit } = ChromeUtils.import( + "resource://gre/modules/DelayedInit.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", + GeckoViewActorManager: "resource://gre/modules/GeckoViewActorManager.sys.mjs", + GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + Blocklist: "resource://gre/modules/Blocklist.jsm", + GeckoViewSettings: "resource://gre/modules/GeckoViewSettings.jsm", + HistogramStopwatch: "resource://gre/modules/GeckoViewTelemetry.jsm", + InitializationTracker: "resource://gre/modules/GeckoViewTelemetry.jsm", + SafeBrowsing: "resource://gre/modules/SafeBrowsing.jsm", + RemoteSecuritySettings: + "resource://gre/modules/psm/RemoteSecuritySettings.jsm", +}); + +XPCOMUtils.defineLazyGetter(this, "WindowEventDispatcher", () => + EventDispatcher.for(window) +); + +XPCOMUtils.defineLazyScriptGetter( + this, + "PrintUtils", + "chrome://global/content/printUtils.js" +); + +// This file assumes `warn` and `debug` are imported into scope +// by the child scripts. +/* global debug, warn */ + +/** + * ModuleManager creates and manages GeckoView modules. Each GeckoView module + * normally consists of a JSM module file with an optional content module file. + * The module file contains a class that extends GeckoViewModule, and the + * content module file contains a class that extends GeckoViewChildModule. A + * module usually pairs with a particular GeckoSessionHandler or delegate on the + * Java side, and automatically receives module lifetime events such as + * initialization, change in enabled state, and change in settings. + */ +var ModuleManager = { + get _initData() { + return window.arguments[0].QueryInterface(Ci.nsIAndroidView).initData; + }, + + init(aBrowser, aModules) { + const MODULES_INIT_PROBE = new HistogramStopwatch( + "GV_STARTUP_MODULES_MS", + aBrowser + ); + + MODULES_INIT_PROBE.start(); + + const initData = this._initData; + this._browser = aBrowser; + this._settings = initData.settings; + this._frozenSettings = Object.freeze(Object.assign({}, this._settings)); + + const self = this; + this._modules = new Map( + (function*() { + for (const module of aModules) { + yield [ + module.name, + new ModuleInfo({ + enabled: !!initData.modules[module.name], + manager: self, + ...module, + }), + ]; + } + })() + ); + + window.document.documentElement.appendChild(aBrowser); + + // By default all layers are discarded when a browser is set to inactive. + // GeckoView by default sets browsers to inactive every time they're not + // visible. To avoid flickering when changing tabs, we preserve layers for + // all loaded tabs. + aBrowser.preserveLayers(true); + + WindowEventDispatcher.registerListener(this, [ + "GeckoView:UpdateModuleState", + "GeckoView:UpdateInitData", + "GeckoView:UpdateSettings", + ]); + + this.messageManager.addMessageListener( + "GeckoView:ContentModuleLoaded", + this + ); + + this._moduleByActorName = new Map(); + this.forEach(module => { + module.onInit(); + module.loadInitFrameScript(); + for (const actorName of module.actorNames) { + this._moduleByActorName[actorName] = module; + } + }); + + window.addEventListener("unload", () => { + this.forEach(module => { + module.enabled = false; + module.onDestroy(); + }); + + this._modules.clear(); + }); + + MODULES_INIT_PROBE.finish(); + }, + + onNewPrintWindow(aParams) { + return PrintUtils.handleStaticCloneCreatedForPrint(aParams.openWindowInfo); + }, + + get window() { + return window; + }, + + get browser() { + return this._browser; + }, + + get messageManager() { + return this._browser.messageManager; + }, + + get eventDispatcher() { + return WindowEventDispatcher; + }, + + get settings() { + return this._frozenSettings; + }, + + forEach(aCallback) { + this._modules.forEach(aCallback, this); + }, + + getActor(aActorName) { + return this.browser.browsingContext.currentWindowGlobal?.getActor( + aActorName + ); + }, + + // Ensures that session history has been flushed before changing remoteness + async prepareToChangeRemoteness() { + // Session state like history is maintained at the process level so we need + // to collect it and restore it in the other process when switching. + // TODO: This should go away when we migrate the history to the main + // process Bug 1507287. + const { history } = await this.getActor("GeckoViewContent").collectState(); + + // Ignore scroll and form data since we're navigating away from this page + // anyway + this.sessionState = { history }; + }, + + willChangeBrowserRemoteness() { + debug`WillChangeBrowserRemoteness`; + + // Now we're switching the remoteness. + this.disabledModules = []; + this.forEach(module => { + if (module.enabled && module.disableOnProcessSwitch) { + module.enabled = false; + this.disabledModules.push(module); + } + }); + + this.forEach(module => { + module.onDestroyBrowser(); + }); + }, + + didChangeBrowserRemoteness() { + debug`DidChangeBrowserRemoteness`; + + this.forEach(module => { + if (module.impl) { + module.impl.onInitBrowser(); + } + }); + + this.messageManager.addMessageListener( + "GeckoView:ContentModuleLoaded", + this + ); + + this.forEach(module => { + // We're attaching a new browser so we have to reload the frame scripts + module.loadInitFrameScript(); + }); + + this.disabledModules.forEach(module => { + module.enabled = true; + }); + this.disabledModules = null; + }, + + afterBrowserRemotenessChange(aSwitchId) { + const { sessionState } = this; + this.sessionState = null; + + sessionState.switchId = aSwitchId; + + this.getActor("GeckoViewContent").restoreState(sessionState); + this.browser.focus(); + + // Load was handled + return true; + }, + + _updateSettings(aSettings) { + Object.assign(this._settings, aSettings); + this._frozenSettings = Object.freeze(Object.assign({}, this._settings)); + + const windowType = aSettings.isPopup + ? "navigator:popup" + : "navigator:geckoview"; + window.document.documentElement.setAttribute("windowtype", windowType); + + this.forEach(module => { + if (module.impl) { + module.impl.onSettingsUpdate(); + } + }); + }, + + onMessageFromActor(aActorName, aMessage) { + this._moduleByActorName[aActorName].receiveMessage(aMessage); + }, + + onEvent(aEvent, aData, aCallback) { + debug`onEvent ${aEvent} ${aData}`; + switch (aEvent) { + case "GeckoView:UpdateModuleState": { + const module = this._modules.get(aData.module); + if (module) { + module.enabled = aData.enabled; + } + break; + } + + case "GeckoView:UpdateInitData": { + // Replace all settings during a transfer. + const initData = this._initData; + this._updateSettings(initData.settings); + + // Update module enabled states. + for (const name in initData.modules) { + const module = this._modules.get(name); + if (module) { + module.enabled = initData.modules[name]; + } + } + + // Notify child of the transfer. + this._browser.messageManager.sendAsyncMessage(aEvent); + break; + } + + case "GeckoView:UpdateSettings": { + this._updateSettings(aData); + break; + } + } + }, + + receiveMessage(aMsg) { + debug`receiveMessage ${aMsg.name} ${aMsg.data}`; + switch (aMsg.name) { + case "GeckoView:ContentModuleLoaded": { + const module = this._modules.get(aMsg.data.module); + if (module) { + module.onContentModuleLoaded(); + } + break; + } + } + }, +}; + +/** + * ModuleInfo is the structure used by ModuleManager to represent individual + * modules. It is responsible for loading the module JSM file if necessary, + * and it acts as the intermediary between ModuleManager and the module + * object that extends GeckoViewModule. + */ +class ModuleInfo { + /** + * Create a ModuleInfo instance. See _loadPhase for phase object description. + * + * @param manager the ModuleManager instance. + * @param name Name of the module. + * @param enabled Enabled state of the module at startup. + * @param onInit Phase object for the init phase, when the window is created. + * @param onEnable Phase object for the enable phase, when the module is first + * enabled by setting a delegate in Java. + */ + constructor({ manager, name, enabled, onInit, onEnable }) { + this._manager = manager; + this._name = name; + + // We don't support having more than one main process script, so let's + // check that we're not accidentally defining two. We could support this if + // needed by making _impl an array for each phase impl. + if (onInit?.resource !== undefined && onEnable?.resource !== undefined) { + throw new Error( + "Only one main process script is allowed for each module." + ); + } + + this._impl = null; + this._contentModuleLoaded = false; + this._enabled = false; + // Only enable once we performed initialization. + this._enabledOnInit = enabled; + + // For init, load resource _before_ initializing browser to support the + // onInitBrowser() override. However, load content module after initializing + // browser, because we don't have a message manager before then. + this._loadResource(onInit); + this._loadActors(onInit); + if (this._enabledOnInit) { + this._loadActors(onEnable); + } + + this._onInitPhase = onInit; + this._onEnablePhase = onEnable; + + const actorNames = []; + if (this._onInitPhase?.actors) { + actorNames.push(Object.keys(this._onInitPhase.actors)); + } + if (this._onEnablePhase?.actors) { + actorNames.push(Object.keys(this._onEnablePhase.actors)); + } + this._actorNames = Object.freeze(actorNames); + } + + get actorNames() { + return this._actorNames; + } + + onInit() { + if (this._impl) { + this._impl.onInit(); + this._impl.onSettingsUpdate(); + } + + this.enabled = this._enabledOnInit; + } + + /** + * Loads the onInit frame script + */ + loadInitFrameScript() { + this._loadFrameScript(this._onInitPhase); + } + + onDestroy() { + if (this._impl) { + this._impl.onDestroy(); + } + } + + /** + * Called before the browser is removed + */ + onDestroyBrowser() { + 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.import(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"); + + // This is only needed for mochitests, so that they honor the + // prefers-color-scheme.content-override pref. GeckoView doesn't set this + // pref to anything other than the default value otherwise. + browser.setAttribute( + "style", + "color-scheme: env(-moz-content-preferred-color-scheme)" + ); + + return browser; +} + +function InitLater(fn, object, name) { + return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */); +} + +function startup() { + GeckoViewUtils.initLogging("XUL", window); + + const browser = createBrowser(); + ModuleManager.init(browser, [ + { + name: "GeckoViewContent", + onInit: { + resource: "resource://gre/modules/GeckoViewContent.jsm", + actors: { + GeckoViewContent: { + parent: { + moduleURI: "resource:///actors/GeckoViewContentParent.jsm", + }, + child: { + moduleURI: "resource:///actors/GeckoViewContentChild.jsm", + events: { + mozcaretstatechanged: { capture: true, mozSystemGroup: true }, + pageshow: { mozSystemGroup: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + onEnable: { + actors: { + ContentDelegate: { + parent: { + moduleURI: "resource:///actors/ContentDelegateParent.jsm", + }, + child: { + moduleURI: "resource:///actors/ContentDelegateChild.jsm", + events: { + DOMContentLoaded: {}, + DOMMetaViewportFitChanged: {}, + "MozDOMFullscreen:Entered": {}, + "MozDOMFullscreen:Exit": {}, + "MozDOMFullscreen:Exited": {}, + "MozDOMFullscreen:Request": {}, + MozFirstContentfulPaint: {}, + MozPaintStatusReset: {}, + contextmenu: { capture: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewNavigation", + onInit: { + resource: "resource://gre/modules/GeckoViewNavigation.jsm", + }, + }, + { + name: "GeckoViewProcessHangMonitor", + onInit: { + resource: "resource://gre/modules/GeckoViewProcessHangMonitor.jsm", + }, + }, + { + name: "GeckoViewProgress", + onEnable: { + resource: "resource://gre/modules/GeckoViewProgress.jsm", + actors: { + ProgressDelegate: { + parent: { + moduleURI: "resource:///actors/ProgressDelegateParent.jsm", + }, + child: { + moduleURI: "resource:///actors/ProgressDelegateChild.jsm", + events: { + MozAfterPaint: { capture: false, mozSystemGroup: true }, + DOMContentLoaded: { capture: false, mozSystemGroup: true }, + pageshow: { capture: false, mozSystemGroup: true }, + }, + }, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewScroll", + onEnable: { + actors: { + ScrollDelegate: { + parent: { + moduleURI: "resource:///actors/ScrollDelegateParent.jsm", + }, + child: { + moduleURI: "resource:///actors/ScrollDelegateChild.jsm", + events: { + mozvisualscroll: { mozSystemGroup: true }, + }, + }, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewSelectionAction", + onEnable: { + resource: "resource://gre/modules/GeckoViewSelectionAction.jsm", + actors: { + SelectionActionDelegate: { + parent: { + moduleURI: "resource:///actors/SelectionActionDelegateParent.jsm", + }, + child: { + moduleURI: "resource:///actors/SelectionActionDelegateChild.jsm", + events: { + mozcaretstatechanged: { mozSystemGroup: true }, + pagehide: { capture: true, mozSystemGroup: true }, + deactivate: { mozSystemGroup: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewSettings", + onInit: { + resource: "resource://gre/modules/GeckoViewSettings.jsm", + actors: { + GeckoViewSettings: { + child: { + moduleURI: "resource:///actors/GeckoViewSettingsChild.jsm", + }, + }, + }, + }, + }, + { + name: "GeckoViewTab", + onInit: { + resource: "resource://gre/modules/GeckoViewTab.jsm", + }, + }, + { + name: "GeckoViewContentBlocking", + onInit: { + resource: "resource://gre/modules/GeckoViewContentBlocking.jsm", + }, + }, + { + name: "SessionStateAggregator", + onInit: { + frameScript: "chrome://geckoview/content/SessionStateAggregator.js", + }, + }, + { + name: "GeckoViewAutofill", + onInit: { + actors: { + GeckoViewAutoFill: { + parent: { + moduleURI: "resource:///actors/GeckoViewAutoFillParent.jsm", + }, + child: { + moduleURI: "resource:///actors/GeckoViewAutoFillChild.jsm", + events: { + DOMFormHasPassword: { + mozSystemGroup: true, + capture: false, + }, + DOMInputPasswordAdded: { + mozSystemGroup: true, + capture: false, + }, + pagehide: { + mozSystemGroup: true, + capture: false, + }, + pageshow: { + mozSystemGroup: true, + capture: false, + }, + focusin: { + mozSystemGroup: true, + capture: false, + }, + focusout: { + mozSystemGroup: true, + capture: false, + }, + "PasswordManager:ShowDoorhanger": {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewMediaControl", + onEnable: { + resource: "resource://gre/modules/GeckoViewMediaControl.jsm", + actors: { + MediaControlDelegate: { + parent: { + moduleURI: "resource:///actors/MediaControlDelegateParent.jsm", + }, + child: { + moduleURI: "resource:///actors/MediaControlDelegateChild.jsm", + events: { + "MozDOMFullscreen:Entered": {}, + "MozDOMFullscreen:Exited": {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewAutocomplete", + onInit: { + actors: { + FormAutofill: { + parent: { + moduleURI: "resource://autofill/FormAutofillParent.jsm", + }, + child: { + moduleURI: "resource://autofill/FormAutofillChild.jsm", + events: { + focusin: {}, + DOMFormBeforeSubmit: {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + }, + }, + }, + { + name: "GeckoViewPrompter", + onInit: { + actors: { + GeckoViewPrompter: { + parent: { + moduleURI: "resource:///actors/GeckoViewPrompterParent.jsm", + }, + child: { + moduleURI: "resource:///actors/GeckoViewPrompterChild.jsm", + }, + allFrames: true, + includeChrome: true, + }, + }, + }, + }, + ]); + + if (!Services.appinfo.sessionHistoryInParent) { + browser.prepareToChangeRemoteness = () => + ModuleManager.prepareToChangeRemoteness(); + browser.afterChangeRemoteness = switchId => + ModuleManager.afterBrowserRemotenessChange(switchId); + } + + browser.addEventListener("WillChangeBrowserRemoteness", event => + ModuleManager.willChangeBrowserRemoteness() + ); + + browser.addEventListener("DidChangeBrowserRemoteness", event => + ModuleManager.didChangeBrowserRemoteness() + ); + + // Allows actors to access ModuleManager. + window.moduleManager = ModuleManager; + + window.prompts = () => { + return window.ModuleManager.getActor("GeckoViewPrompter").getPrompts(); + }; + + Services.tm.dispatchToMainThread(() => { + // This should always be the first thing we do here - any additional delayed + // initialisation tasks should be added between "browser-delayed-startup-finished" + // and "browser-idle-startup-tasks-finished". + + // Bug 1496684: Various bits of platform stuff depend on this notification + // to learn when a browser window has finished its initial (chrome) + // initialisation, especially with regards to the very first window that is + // created. Therefore, GeckoView "windows" need to send this, too. + InitLater(() => + Services.obs.notifyObservers(window, "browser-delayed-startup-finished") + ); + + // Let the extension code know it can start loading things that were delayed + // while GeckoView started up. + InitLater(() => { + Services.obs.notifyObservers(window, "extensions-late-startup"); + }); + + InitLater(() => { + // TODO bug 1730026: this runs too often. It should run once. + RemoteSecuritySettings.init(); + }); + + InitLater(() => { + // Initialize safe browsing module. This is required for content + // blocking features and manages blocklist downloads and updates. + SafeBrowsing.init(); + }); + + InitLater(() => { + // It's enough to run this once to set up FOG. + // (See also bug 1730026.) + Services.fog.registerCustomPings(); + }); + + InitLater(() => { + // Initialize the blocklist module. + // TODO bug 1730026: this runs too often. It should run once. + Blocklist.loadBlocklistAsync(); + }); + + // This should always go last, since the idle tasks (except for the ones with + // timeouts) should execute in order. Note that this observer notification is + // not guaranteed to fire, since the window could close before we get here. + + // This notification in particular signals the ScriptPreloader that we have + // finished startup, so it can now stop recording script usage and start + // updating the startup cache for faster script loading. + InitLater(() => + Services.obs.notifyObservers( + window, + "browser-idle-startup-tasks-finished" + ) + ); + }); + + // Move focus to the content window at the end of startup, + // so things like text selection can work properly. + browser.focus(); + + InitializationTracker.onInitialized(performance.now()); +} diff --git a/mobile/android/chrome/geckoview/geckoview.xhtml b/mobile/android/chrome/geckoview/geckoview.xhtml new file mode 100644 index 0000000000..12a5f9ac7b --- /dev/null +++ b/mobile/android/chrome/geckoview/geckoview.xhtml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- 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/. --> + +<window id="main-window" + windowtype="navigator:geckoview" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <script type="application/javascript" src="chrome://geckoview/content/geckoview.js"/> + <script> + /* import-globals-from geckoview.js */ + window.addEventListener("DOMContentLoaded", startup, { once: true }); + </script> +</window> diff --git a/mobile/android/chrome/geckoview/jar.mn b/mobile/android/chrome/geckoview/jar.mn new file mode 100644 index 0000000000..c2905f6fc4 --- /dev/null +++ b/mobile/android/chrome/geckoview/jar.mn @@ -0,0 +1,14 @@ +# 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/. + +geckoview.jar: +% content geckoview %content/ + + content/config.xhtml + content/config.js + content/geckoview.xhtml + content/geckoview.js + content/SessionStateAggregator.js + +% content branding %content/branding/ diff --git a/mobile/android/chrome/geckoview/moz.build b/mobile/android/chrome/geckoview/moz.build new file mode 100644 index 0000000000..d988c0ff9b --- /dev/null +++ b/mobile/android/chrome/geckoview/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] |