Bug 1415318 - Optionally send web console output to logcat in GeckoView r=jchen,rbarker
This adds a GeckoRuntimeSetting that allows apps to direct the web
console to logcat.
MozReview-Commit-ID: 7KgX5Ol6D3E
--- a/mobile/android/app/geckoview-prefs.js
+++ b/mobile/android/app/geckoview-prefs.js
@@ -8,8 +8,10 @@
pref("privacy.trackingprotection.pbmode.enabled", false);
pref("dom.ipc.processCount", 1);
pref("dom.ipc.keepProcessesAlive.web", 1);
pref("dom.ipc.processPrelaunch.enabled", false);
// Tell Telemetry that we're in GeckoView mode.
pref("toolkit.telemetry.isGeckoViewMode", true);
+
+pref("geckoview.console.enabled", false);
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -5,16 +5,18 @@
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
GeckoViewTelemetryController: "resource://gre/modules/GeckoViewTelemetryController.jsm",
GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
Services: "resource://gre/modules/Services.jsm",
});
+const {debug, warn} = GeckoViewUtils.initLogging("GeckoViewStartup", this);
+
function GeckoViewStartup() {
}
GeckoViewStartup.prototype = {
classID: Components.ID("{8e993c34-fdd6-432c-967e-f995d888777f}"),
QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
@@ -31,31 +33,43 @@ GeckoViewStartup.prototype = {
url = url.substring(4, url.indexOf("!/") + 2);
let protocolHandler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
protocolHandler.setSubstitution("android", Services.io.newURI(url));
},
/* ---------- nsIObserver ---------- */
observe: function(aSubject, aTopic, aData) {
+ debug `observe: ${aTopic}`;
switch (aTopic) {
case "app-startup": {
// Parent and content process.
GeckoViewUtils.addLazyGetter(this, "GeckoViewPermission", {
service: "@mozilla.org/content-permission/prompt;1",
observers: [
"getUserMedia:ask-device-permission",
"getUserMedia:request",
"PeerConnection:request",
],
ppmm: [
"GeckoView:AddCameraPermission",
],
});
+ GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", {
+ module: "resource://gre/modules/GeckoViewConsole.jsm",
+ });
+
+ GeckoViewUtils.addLazyPrefObserver({
+ name: "geckoview.console.enabled",
+ default: false,
+ }, {
+ handler: _ => this.GeckoViewConsole,
+ });
+
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
// Parent process only.
this.setResourceSubstitutions();
Services.mm.loadFrameScript(
"chrome://geckoview/content/GeckoViewPromptContent.js", true);
GeckoViewUtils.addLazyGetter(this, "ContentCrashHandler", {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -117,17 +117,18 @@ public class TestRunnerActivity extends
runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" });
final Bundle extras = intent.getExtras();
if (extras != null) {
runtimeSettingsBuilder.extras(extras);
}
runtimeSettingsBuilder
.nativeCrashReportingEnabled(true)
- .javaCrashReportingEnabled(true);
+ .javaCrashReportingEnabled(true)
+ .consoleOutput(true);
sRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
sRuntime.setDelegate(new GeckoRuntime.Delegate() {
@Override
public void onShutdown() {
mKillProcessOnDestroy = true;
finish();
}
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -1247,17 +1247,18 @@ public class GeckoSessionTestRule extend
classes, recorder);
mAllDelegates = new HashSet<>(DEFAULT_DELEGATES);
if (sRuntime == null) {
final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
new GeckoRuntimeSettings.Builder();
runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" })
.extras(InstrumentationRegistry.getArguments())
- .remoteDebuggingEnabled(true);
+ .remoteDebuggingEnabled(true)
+ .consoleOutput(true);
if (env.isAutomation()) {
runtimeSettingsBuilder
.nativeCrashReportingEnabled(true)
.javaCrashReportingEnabled(true);
}
sRuntime = GeckoRuntime.create(
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
@@ -209,16 +209,31 @@ public final class GeckoRuntimeSettings
* @return This Builder instance.
**/
public @NonNull Builder trackingProtectionCategories(
@TrackingProtectionDelegate.Category int categories) {
mSettings.mTrackingProtection
.set(TrackingProtection.buildPrefValue(categories));
return this;
}
+
+ /**
+ * Set whether or not web console messages should go to logcat.
+ *
+ * Note: If enabled, Gecko performance may be negatively impacted if
+ * content makes heavy use of the console API.
+ *
+ * @param enabled A flag determining whether or not web console messages should be
+ * printed to logcat.
+ * @return The builder instance.
+ */
+ public @NonNull Builder consoleOutput(boolean enabled) {
+ mSettings.mConsoleOutput.set(enabled);
+ return this;
+ }
}
/* package */ GeckoRuntime runtime;
/* package */ boolean mUseContentProcess;
/* package */ String[] mArgs;
/* package */ Bundle mExtras;
/* package */ int prefCount;
@@ -260,24 +275,26 @@ public final class GeckoRuntimeSettings
/* package */ Pref<Integer> mCookieBehavior = new Pref<Integer>(
"network.cookie.cookieBehavior", COOKIE_ACCEPT_ALL);
/* package */ Pref<Integer> mCookieLifetime = new Pref<Integer>(
"network.cookie.lifetimePolicy", COOKIE_LIFETIME_NORMAL);
/* package */ Pref<Integer> mCookieLifetimeDays = new Pref<Integer>(
"network.cookie.lifetime.days", 90);
/* package */ Pref<String> mTrackingProtection = new Pref<String>(
"urlclassifier.trackingTable", "");
+ /* package */ Pref<Boolean> mConsoleOutput = new Pref<Boolean>(
+ "geckoview.console.enabled", false);
/* package */ boolean mNativeCrashReporting;
/* package */ boolean mJavaCrashReporting;
/* package */ boolean mDebugPause;
private final Pref<?>[] mPrefs = new Pref<?>[] {
mCookieBehavior, mCookieLifetime, mCookieLifetimeDays, mJavaScript,
- mRemoteDebugging, mTrackingProtection, mWebFonts
+ mRemoteDebugging, mTrackingProtection, mWebFonts, mConsoleOutput
};
/* package */ GeckoRuntimeSettings() {
this(null);
}
/* package */ GeckoRuntimeSettings(final @Nullable GeckoRuntimeSettings settings) {
if (BuildConfig.DEBUG && prefCount != mPrefs.length) {
@@ -559,16 +576,41 @@ public final class GeckoRuntimeSettings
* @return This GeckoRuntimeSettings instance.
**/
public @NonNull GeckoRuntimeSettings setTrackingProtectionCategories(
@TrackingProtectionDelegate.Category int categories) {
mTrackingProtection.set(TrackingProtection.buildPrefValue(categories));
return this;
}
+ /**
+ * Set whether or not web console messages should go to logcat.
+ *
+ * Note: If enabled, Gecko performance may be negatively impacted if
+ * content makes heavy use of the console API.
+ *
+ * @param enabled A flag determining whether or not web console messages should be
+ * printed to logcat.
+ * @return This GeckoRuntimeSettings instance.
+ */
+
+ public @NonNull GeckoRuntimeSettings setConsoleOutputEnabled(boolean enabled) {
+ mConsoleOutput.set(enabled);
+ return this;
+ }
+
+ /**
+ * Get whether or not web console messages are sent to logcat.
+ *
+ * @return This GeckoRuntimeSettings instance.
+ */
+ public @NonNull boolean getConsoleOutputEnabled() {
+ return mConsoleOutput.get();
+ }
+
@Override // Parcelable
public int describeContents() {
return 0;
}
@Override // Parcelable
public void writeToParcel(Parcel out, int flags) {
ParcelableUtils.writeBoolean(out, mUseContentProcess);
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -116,16 +116,17 @@ public class GeckoViewActivity extends A
if (extras != null) {
runtimeSettingsBuilder.extras(extras);
}
runtimeSettingsBuilder
.useContentProcessHint(mUseMultiprocess)
.remoteDebuggingEnabled(true)
.nativeCrashReportingEnabled(true)
.javaCrashReportingEnabled(true)
+ .consoleOutput(true)
.trackingProtectionCategories(TrackingProtectionDelegate.CATEGORY_ALL);
sGeckoRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
}
mGeckoSession = (GeckoSession)getIntent().getParcelableExtra("session");
if (mGeckoSession != null) {
connectSession(mGeckoSession);
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewConsole.jsm
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["GeckoViewConsole"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm"
+});
+
+GeckoViewUtils.initLogging("GeckoViewConsole", this);
+
+const LOG_EVENT_TOPIC = "console-api-log-event";
+
+var GeckoViewConsole = {
+ _isEnabled: false,
+
+ get enabled() {
+ return this._isEnabled;
+ },
+
+ set enabled(val) {
+ debug `enabled = ${val}`;
+ if (!!val === this._isEnabled) {
+ return;
+ }
+
+ this._isEnabled = !!val;
+ if (this._isEnabled) {
+ Services.obs.addObserver(this, LOG_EVENT_TOPIC);
+ } else {
+ Services.obs.removeObserver(this, LOG_EVENT_TOPIC);
+ }
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ this.enabled = Services.prefs.getBoolPref(aData, false);
+ break;
+ case LOG_EVENT_TOPIC:
+ this._handleConsoleMessage(aSubject);
+ break;
+ }
+ },
+
+ _handleConsoleMessage(aMessage) {
+ aMessage = aMessage.wrappedJSObject;
+
+ let mappedArguments = Array.map(aMessage.arguments, this.formatResult, this);
+ let joinedArguments = Array.join(mappedArguments, " ");
+
+ if (aMessage.level == "error" || aMessage.level == "warn") {
+ let flag = (aMessage.level == "error" ? Ci.nsIScriptError.errorFlag : Ci.nsIScriptError.warningFlag);
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+ consoleMsg.init(joinedArguments, null, null, 0, 0, flag, "content javascript");
+ Services.console.logMessage(consoleMsg);
+ } else if (aMessage.level == "trace") {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let args = aMessage.arguments;
+ let filename = this.abbreviateSourceURL(args[0].filename);
+ let functionName = args[0].functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+ let lineNumber = args[0].lineNumber;
+
+ let body = bundle.formatStringFromName("stacktrace.outputMessage", [filename, functionName, lineNumber], 3);
+ body += "\n";
+ args.forEach(function(aFrame) {
+ let functionName = aFrame.functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+ body += " " + aFrame.filename + " :: " + functionName + " :: " + aFrame.lineNumber + "\n";
+ });
+
+ Services.console.logStringMessage(body);
+ } else if (aMessage.level == "time" && aMessage.arguments) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let body = bundle.formatStringFromName("timer.start", [aMessage.arguments.name], 1);
+ Services.console.logStringMessage(body);
+ } else if (aMessage.level == "timeEnd" && aMessage.arguments) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+ let body = bundle.formatStringFromName("timer.end", [aMessage.arguments.name, aMessage.arguments.duration], 2);
+ Services.console.logStringMessage(body);
+ } else if (["group", "groupCollapsed", "groupEnd"].includes(aMessage.level)) {
+ // Do nothing yet
+ } else {
+ Services.console.logStringMessage(joinedArguments);
+ }
+ },
+
+ getResultType(aResult) {
+ let type = aResult === null ? "null" : typeof aResult;
+ if (type == "object" && aResult.constructor && aResult.constructor.name)
+ type = aResult.constructor.name;
+ return type.toLowerCase();
+ },
+
+ formatResult(aResult) {
+ let output = "";
+ let type = this.getResultType(aResult);
+ switch (type) {
+ case "string":
+ case "boolean":
+ case "date":
+ case "error":
+ case "number":
+ case "regexp":
+ output = aResult.toString();
+ break;
+ case "null":
+ case "undefined":
+ output = type;
+ break;
+ default:
+ output = aResult.toString();
+ break;
+ }
+
+ return output;
+ },
+
+ abbreviateSourceURL(aSourceURL) {
+ // Remove any query parameters.
+ let hookIndex = aSourceURL.indexOf("?");
+ if (hookIndex > -1)
+ aSourceURL = aSourceURL.substring(0, hookIndex);
+
+ // Remove a trailing "/".
+ if (aSourceURL[aSourceURL.length - 1] == "/")
+ aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
+
+ // Remove all but the last path component.
+ let slashIndex = aSourceURL.lastIndexOf("/");
+ if (slashIndex > -1)
+ aSourceURL = aSourceURL.substring(slashIndex + 1);
+
+ return aSourceURL;
+ }
+};
--- a/mobile/android/modules/geckoview/moz.build
+++ b/mobile/android/modules/geckoview/moz.build
@@ -3,16 +3,17 @@
# 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/.
EXTRA_JS_MODULES += [
'AndroidLog.jsm',
'ContentCrashHandler.jsm',
'GeckoViewAccessibility.jsm',
+ 'GeckoViewConsole.jsm',
'GeckoViewContent.jsm',
'GeckoViewContentModule.jsm',
'GeckoViewModule.jsm',
'GeckoViewNavigation.jsm',
'GeckoViewProgress.jsm',
'GeckoViewRemoteDebugger.jsm',
'GeckoViewSettings.jsm',
'GeckoViewTab.jsm',