summaryrefslogtreecommitdiffstats
path: root/mobile/android/chrome/geckoview
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--mobile/android/chrome/geckoview/SessionStateAggregator.js676
-rw-r--r--mobile/android/chrome/geckoview/config.js719
-rw-r--r--mobile/android/chrome/geckoview/config.xhtml83
-rw-r--r--mobile/android/chrome/geckoview/geckoview.js891
-rw-r--r--mobile/android/chrome/geckoview/geckoview.xhtml14
-rw-r--r--mobile/android/chrome/geckoview/jar.mn14
-rw-r--r--mobile/android/chrome/geckoview/moz.build7
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"]