diff options
Diffstat (limited to '')
-rw-r--r-- | mobile/android/modules/geckoview/GeckoViewUtils.sys.mjs | 514 |
1 files changed, 514 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewUtils.sys.mjs b/mobile/android/modules/geckoview/GeckoViewUtils.sys.mjs new file mode 100644 index 0000000000..581892337f --- /dev/null +++ b/mobile/android/modules/geckoview/GeckoViewUtils.sys.mjs @@ -0,0 +1,514 @@ +/* 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +import { Log } from "resource://gre/modules/Log.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", + clearTimeout: "resource://gre/modules/Timer.sys.mjs", + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AndroidLog: "resource://gre/modules/AndroidLog.jsm", +}); + +/** + * A formatter that does not prepend time/name/level information to messages, + * because those fields are logged separately when using the Android logger. + */ +class AndroidFormatter extends Log.BasicFormatter { + format(message) { + return this.formatText(message); + } +} + +/* + * AndroidAppender + * Logs to Android logcat using AndroidLog.jsm + */ +class AndroidAppender extends Log.Appender { + constructor(aFormatter) { + super(aFormatter || new AndroidFormatter()); + this._name = "AndroidAppender"; + + // Map log level to AndroidLog.foo method. + this._mapping = { + [Log.Level.Fatal]: "e", + [Log.Level.Error]: "e", + [Log.Level.Warn]: "w", + [Log.Level.Info]: "i", + [Log.Level.Config]: "d", + [Log.Level.Debug]: "d", + [Log.Level.Trace]: "v", + }; + } + + append(aMessage) { + if (!aMessage) { + return; + } + + // AndroidLog.jsm always prepends "Gecko" to the tag, so we strip any + // leading "Gecko" here. Also strip dots to save space. + const tag = aMessage.loggerName.replace(/^Gecko|\./g, ""); + const msg = this._formatter.format(aMessage); + lazy.AndroidLog[this._mapping[aMessage.level]](tag, msg); + } +} + +export var GeckoViewUtils = { + /** + * Define a lazy getter that loads an object from external code, and + * optionally handles observer and/or message manager notifications for the + * object, so the object only loads when a notification is received. + * + * @param scope Scope for holding the loaded object. + * @param name Name of the object to load. + * @param service If specified, load the object from a JS component; the + * component must include the line + * "this.wrappedJSObject = this;" in its constructor. + * @param module If specified, load the object from a JS module. + * @param init Optional post-load initialization function. + * @param observers If specified, listen to specified observer notifications. + * @param ppmm If specified, listen to specified process messages. + * @param mm If specified, listen to specified frame messages. + * @param ged If specified, listen to specified global EventDispatcher events. + * @param once if true, only listen to the specified + * events/messages/notifications once. + */ + addLazyGetter( + scope, + name, + { service, module, handler, observers, ppmm, mm, ged, init, once } + ) { + XPCOMUtils.defineLazyGetter(scope, name, _ => { + let ret = undefined; + if (module) { + ret = ChromeUtils.import(module)[name]; + } else if (service) { + ret = Cc[service].getService(Ci.nsISupports).wrappedJSObject; + } else if (typeof handler === "function") { + ret = { + handleEvent: handler, + observe: handler, + onEvent: handler, + receiveMessage: handler, + }; + } else if (handler) { + ret = handler; + } + if (ret && init) { + init.call(scope, ret); + } + return ret; + }); + + if (observers) { + const observer = (subject, topic, data) => { + Services.obs.removeObserver(observer, topic); + if (!once) { + Services.obs.addObserver(scope[name], topic); + } + scope[name].observe(subject, topic, data); // Explicitly notify new observer + }; + observers.forEach(topic => Services.obs.addObserver(observer, topic)); + } + + if (!this.IS_PARENT_PROCESS) { + // ppmm, mm, and ged are only available in the parent process. + return; + } + + const addMMListener = (target, names) => { + const listener = msg => { + target.removeMessageListener(msg.name, listener); + if (!once) { + target.addMessageListener(msg.name, scope[name]); + } + scope[name].receiveMessage(msg); + }; + names.forEach(msg => target.addMessageListener(msg, listener)); + }; + if (ppmm) { + addMMListener(Services.ppmm, ppmm); + } + if (mm) { + addMMListener(Services.mm, mm); + } + + if (ged) { + const listener = (event, data, callback) => { + lazy.EventDispatcher.instance.unregisterListener(listener, event); + if (!once) { + lazy.EventDispatcher.instance.registerListener(scope[name], event); + } + scope[name].onEvent(event, data, callback); + }; + lazy.EventDispatcher.instance.registerListener(listener, ged); + } + }, + + _addLazyListeners(events, handler, scope, name, addFn, handleFn) { + if (!handler) { + handler = _ => + Array.isArray(name) ? name.map(n => scope[n]) : scope[name]; + } + const listener = (...args) => { + let handlers = handler(...args); + if (!handlers) { + return; + } + if (!Array.isArray(handlers)) { + handlers = [handlers]; + } + handleFn(handlers, listener, args); + }; + if (Array.isArray(events)) { + addFn(events, listener); + } else { + addFn([events], listener); + } + }, + + /** + * Add lazy event listeners that only load the actual handler when an event + * is being handled. + * + * @param target Event target for the event listeners. + * @param events Event name as a string or array. + * @param handler If specified, function that, for a given event, returns the + * actual event handler as an object or an array of objects. + * If handler is not specified, the actual event handler is + * specified using the scope and name pair. + * @param scope See handler. + * @param name See handler. + * @param options Options for addEventListener. + */ + addLazyEventListener(target, events, { handler, scope, name, options }) { + this._addLazyListeners( + events, + handler, + scope, + name, + (events, listener) => { + events.forEach(event => + target.addEventListener(event, listener, options) + ); + }, + (handlers, listener, args) => { + if (!options || !options.once) { + target.removeEventListener(args[0].type, listener, options); + handlers.forEach(handler => + target.addEventListener(args[0].type, handler, options) + ); + } + handlers.forEach(handler => handler.handleEvent(args[0])); + } + ); + }, + + /** + * Add lazy pref observers, and only load the actual handler once the pref + * value changes from default, and every time the pref value changes + * afterwards. + * + * @param aPrefs Prefs as an object or array. Each pref object has fields + * "name" and "default", indicating the name and default value + * of the pref, respectively. + * @param handler If specified, function that, for a given pref, returns the + * actual event handler as an object or an array of objects. + * If handler is not specified, the actual event handler is + * specified using the scope and name pair. + * @param scope See handler. + * @param name See handler. + * @param once If true, only observe the specified prefs once. + */ + addLazyPrefObserver(aPrefs, { handler, scope, name, once }) { + this._addLazyListeners( + aPrefs, + handler, + scope, + name, + (prefs, observer) => { + prefs.forEach(pref => Services.prefs.addObserver(pref.name, observer)); + prefs.forEach(pref => { + if (pref.default === undefined) { + return; + } + let value; + switch (typeof pref.default) { + case "string": + value = Services.prefs.getCharPref(pref.name, pref.default); + break; + case "number": + value = Services.prefs.getIntPref(pref.name, pref.default); + break; + case "boolean": + value = Services.prefs.getBoolPref(pref.name, pref.default); + break; + } + if (pref.default !== value) { + // Notify observer if value already changed from default. + observer(Services.prefs, "nsPref:changed", pref.name); + } + }); + }, + (handlers, observer, args) => { + if (!once) { + Services.prefs.removeObserver(args[2], observer); + handlers.forEach(handler => + Services.prefs.addObserver(args[2], observer) + ); + } + handlers.forEach(handler => handler.observe(...args)); + } + ); + }, + + getRootDocShell(aWin) { + if (!aWin) { + return null; + } + let docShell; + try { + docShell = aWin.QueryInterface(Ci.nsIDocShell); + } catch (e) { + docShell = aWin.docShell; + } + return docShell.rootTreeItem.QueryInterface(Ci.nsIInterfaceRequestor); + }, + + /** + * Return the outermost chrome DOM window (the XUL window) for a given DOM + * window, in the parent process. + * + * @param aWin a DOM window. + */ + getChromeWindow(aWin) { + const docShell = this.getRootDocShell(aWin); + return docShell && docShell.domWindow; + }, + + /** + * Return the content frame message manager (aka the frame script global + * object) for a given DOM window, in a child process. + * + * @param aWin a DOM window. + */ + getContentFrameMessageManager(aWin) { + const docShell = this.getRootDocShell(aWin); + return docShell && docShell.getInterface(Ci.nsIBrowserChild).messageManager; + }, + + /** + * Return the per-nsWindow EventDispatcher for a given DOM window, in either + * the parent process or a child process. + * + * @param aWin a DOM window. + */ + getDispatcherForWindow(aWin) { + try { + if (!this.IS_PARENT_PROCESS) { + const mm = this.getContentFrameMessageManager(aWin.top || aWin); + return mm && lazy.EventDispatcher.forMessageManager(mm); + } + const win = this.getChromeWindow(aWin.top || aWin); + if (!win.closed) { + return win.WindowEventDispatcher || lazy.EventDispatcher.for(win); + } + } catch (e) {} + return null; + }, + + /** + * Return promise for waiting for finishing PanZoomState. + * + * @param aWindow a DOM window. + * @return promise + */ + waitForPanZoomState(aWindow) { + return new Promise((resolve, reject) => { + if ( + !aWindow?.windowUtils.asyncPanZoomEnabled || + !Services.prefs.getBoolPref("apz.zoom-to-focused-input.enabled") + ) { + // No zoomToFocusedInput. + resolve(); + return; + } + + let timerId = 0; + + const panZoomState = (aSubject, aTopic, aData) => { + if (timerId != 0) { + // aWindow may be dead object now. + try { + lazy.clearTimeout(timerId); + } catch (e) {} + timerId = 0; + } + + if (aData === "NOTHING") { + Services.obs.removeObserver(panZoomState, "PanZoom:StateChange"); + resolve(); + } + }; + + Services.obs.addObserver(panZoomState, "PanZoom:StateChange"); + + // "GeckoView:ZoomToInput" has the timeout as 500ms when window isn't + // resized (it means on-screen-keyboard is already shown). + // So after up to 500ms, APZ event is sent. So we need to wait for more + // 500ms. + timerId = lazy.setTimeout(() => { + // PanZoom state isn't changed. zoomToFocusedInput will return error. + Services.obs.removeObserver(panZoomState, "PanZoom:StateChange"); + reject(); + }, 600); + }); + }, + + /** + * Add logging functions to the specified scope that forward to the given + * Log.sys.mjs logger. Currently "debug" and "warn" functions are supported. To + * log something, call the function through a template literal: + * + * function foo(bar, baz) { + * debug `hello world`; + * debug `foo called with ${bar} as bar`; + * warn `this is a warning for ${baz}`; + * } + * + * An inline format can also be used for logging: + * + * let bar = 42; + * do_something(bar); // No log. + * do_something(debug.foo = bar); // Output "foo = 42" to the log. + * + * @param aTag Name of the Log.jsm logger to forward logs to. + * @param aScope Scope to add the logging functions to. + */ + initLogging(aTag, aScope) { + aScope = aScope || {}; + const tag = "GeckoView." + aTag.replace(/^GeckoView\.?/, ""); + + // Only provide two levels for simplicity. + // For "info", use "debug" instead. + // For "error", throw an actual JS error instead. + for (const level of ["DEBUG", "WARN"]) { + const log = (strings, ...exprs) => + this._log(log.logger, level, strings, exprs); + + XPCOMUtils.defineLazyGetter(log, "logger", _ => { + const logger = Log.repository.getLogger(tag); + logger.parent = this.rootLogger; + return logger; + }); + + aScope[level.toLowerCase()] = new Proxy(log, { + set: (obj, prop, value) => obj([prop + " = ", ""], value) || true, + }); + } + return aScope; + }, + + get rootLogger() { + if (!this._rootLogger) { + this._rootLogger = Log.repository.getLogger("GeckoView"); + this._rootLogger.addAppender(new AndroidAppender()); + this._rootLogger.manageLevelFromPref("geckoview.logging"); + } + return this._rootLogger; + }, + + _log(aLogger, aLevel, aStrings, aExprs) { + if (!Array.isArray(aStrings)) { + const [, file, line] = new Error().stack.match(/.*\n.*\n.*@(.*):(\d+):/); + throw Error( + `Expecting template literal: ${aLevel} \`foo \${bar}\``, + file, + +line + ); + } + + if (aLogger.level > Log.Level.Numbers[aLevel]) { + // Log disabled. + return; + } + + // Do some GeckoView-specific formatting: + // * Remove newlines so long log lines can be put into multiple lines: + // debug `foo=${foo} + // bar=${bar}`; + const strs = Array.from(aStrings); + const regex = /\n\s*/g; + for (let i = 0; i < strs.length; i++) { + strs[i] = strs[i].replace(regex, " "); + } + + // * Heuristically format flags as hex. + // * Heuristically format nsresult as string name or hex. + for (let i = 0; i < aExprs.length; i++) { + const expr = aExprs[i]; + switch (typeof expr) { + case "number": + if (expr > 0 && /\ba?[fF]lags?[\s=:]+$/.test(strs[i])) { + // Likely a flag; display in hex. + aExprs[i] = `0x${expr.toString(0x10)}`; + } else if (expr >= 0 && /\b(a?[sS]tatus|rv)[\s=:]+$/.test(strs[i])) { + // Likely an nsresult; display in name or hex. + aExprs[i] = `0x${expr.toString(0x10)}`; + for (const name in Cr) { + if (expr === Cr[name]) { + aExprs[i] = name; + break; + } + } + } + break; + } + } + + aLogger[aLevel.toLowerCase()](strs, ...aExprs); + }, + + /** + * Checks whether the principal is supported for permissions. + * + * @param {nsIPrincipal} principal + * The principal to check. + * + * @return {boolean} if the principal is supported. + */ + isSupportedPermissionsPrincipal(principal) { + if (!principal) { + return false; + } + if (!(principal instanceof Ci.nsIPrincipal)) { + throw new Error( + "Argument passed as principal is not an instance of Ci.nsIPrincipal" + ); + } + return this.isSupportedPermissionsScheme(principal.scheme); + }, + + /** + * Checks whether we support managing permissions for a specific scheme. + * @param {string} scheme - Scheme to test. + * @returns {boolean} Whether the scheme is supported. + */ + isSupportedPermissionsScheme(scheme) { + return ["http", "https", "moz-extension", "file"].includes(scheme); + }, +}; + +XPCOMUtils.defineLazyGetter( + GeckoViewUtils, + "IS_PARENT_PROCESS", + _ => Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT +); |