Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
authorNarcis Beleuzu <nbeleuzu@mozilla.com>
Mon, 16 Apr 2018 01:28:00 +0300
changeset 467354 2ad99f4d46d6c9ae17ee09ebc682fa8a045ad973
parent 467353 0e164dfe2a02bbd5e971883b210433541078e1af (current diff)
parent 466969 7ff499dfcd51cf4a95ebf0db506b415bf7bb27c3 (diff)
child 467355 c985ffee008d2d5daed2d5c83bf2bc2dbc221166
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to mozilla-inbound. a=merge CLOSED TREE
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
--- a/layout/style/ServoBindingList.h
+++ b/layout/style/ServoBindingList.h
@@ -284,18 +284,18 @@ SERVO_BINDING_FUNC(Servo_StyleRule_GetSe
 SERVO_BINDING_FUNC(Servo_StyleRule_GetSpecificityAtIndex, void,
                    RawServoStyleRuleBorrowed rule, uint32_t index,
                    uint64_t* specificity)
 SERVO_BINDING_FUNC(Servo_StyleRule_GetSelectorCount, void,
                    RawServoStyleRuleBorrowed rule, uint32_t* count)
 SERVO_BINDING_FUNC(Servo_StyleRule_SelectorMatchesElement, bool,
                    RawServoStyleRuleBorrowed, RawGeckoElementBorrowed,
                    uint32_t index, mozilla::CSSPseudoElementType pseudo_type)
-SERVO_BINDING_FUNC(Servo_StyleRule_SetSelectorText, bool,
-                   RawServoStyleSheetContentsBorrowed sheet,
+SERVO_BINDING_FUNC(Servo_StyleRule_SetSelectorText, bool,
+                   RawServoStyleSheetContentsBorrowed sheet,
                    RawServoStyleRuleBorrowed rule, const nsAString* text)
 SERVO_BINDING_FUNC(Servo_ImportRule_GetHref, void,
                    RawServoImportRuleBorrowed rule, nsAString* result)
 SERVO_BINDING_FUNC(Servo_ImportRule_GetSheet,
                    const mozilla::ServoStyleSheet*,
                    RawServoImportRuleBorrowed rule)
 SERVO_BINDING_FUNC(Servo_Keyframe_GetKeyText, void,
                    RawServoKeyframeBorrowed keyframe, nsAString* result)
--- a/layout/style/StyleSheetInlines.h
+++ b/layout/style/StyleSheetInlines.h
@@ -128,18 +128,18 @@ StyleSheet::GetIntegrity(dom::SRIMetadat
   aResult = SheetInfo().mIntegrity;
 }
 
 bool
 StyleSheet::HasUniqueInner() const
 {
   return mInner->mSheets.Length() == 1;
 }
-
-void
-StyleSheet::AssertHasUniqueInner() const
-{
-  MOZ_ASSERT(HasUniqueInner());
+
+void
+StyleSheet::AssertHasUniqueInner() const
+{
+  MOZ_ASSERT(HasUniqueInner());
 }
 
 }
 
 #endif // mozilla_StyleSheetInlines_h
--- a/mobile/android/chrome/geckoview/ErrorPageEventHandler.js
+++ b/mobile/android/chrome/geckoview/ErrorPageEventHandler.js
@@ -1,14 +1,18 @@
 /* -*- 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/. */
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
+
+/* global debug:false, warn:false */
+GeckoViewUtils.initLogging("GeckoView.ErrorPageEventHandler", this);
 
 ChromeUtils.defineModuleGetter(this, "SSLExceptions",
                                "resource://gre/modules/SSLExceptions.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
 });
 
@@ -37,17 +41,17 @@ var ErrorPageEventHandler = {
               let uri = Services.io.newURI(errorDoc.location.href);
               let sslExceptions = new SSLExceptions();
 
               if (target == perm)
                 sslExceptions.addPermanentException(uri, errorDoc.defaultView);
               else
                 sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
             } catch (e) {
-              dump("Failed to set cert exception: " + e + "\n");
+              warn `Failed to set cert exception: ${e}`;
             }
             errorDoc.location.reload();
           } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
             errorDoc.location = "about:home";
           }
         }
         break;
       }
--- a/mobile/android/chrome/geckoview/GeckoViewContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContent.js
@@ -5,27 +5,19 @@
 
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewContent"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 class GeckoViewContent extends GeckoViewContentModule {
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
 
     addEventListener("DOMTitleChanged", this, false);
     addEventListener("DOMWindowFocus", this, false);
     addEventListener("DOMWindowClose", this, false);
     addEventListener("MozDOMFullscreen:Entered", this, false);
     addEventListener("MozDOMFullscreen:Exit", this, false);
     addEventListener("MozDOMFullscreen:Exited", this, false);
     addEventListener("MozDOMFullscreen:Request", this, false);
@@ -35,17 +27,17 @@ class GeckoViewContent extends GeckoView
                                            this);
     this.messageManager.addMessageListener("GeckoView:DOMFullscreenExited",
                                            this);
     this.messageManager.addMessageListener("GeckoView:ZoomToInput",
                                            this);
   }
 
   onDisable() {
-    debug("onDisable");
+    debug `onDisable`;
 
     removeEventListener("DOMTitleChanged", this);
     removeEventListener("DOMWindowFocus", this);
     removeEventListener("DOMWindowClose", this);
     removeEventListener("MozDOMFullscreen:Entered", this);
     removeEventListener("MozDOMFullscreen:Exit", this);
     removeEventListener("MozDOMFullscreen:Exited", this);
     removeEventListener("MozDOMFullscreen:Request", this);
@@ -55,17 +47,17 @@ class GeckoViewContent extends GeckoView
                                               this);
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExited",
                                               this);
     this.messageManager.removeMessageListener("GeckoView:ZoomToInput",
                                               this);
   }
 
   receiveMessage(aMsg) {
-    debug("receiveMessage " + aMsg.name);
+    debug `receiveMessage: ${aMsg.name}`;
 
     switch (aMsg.name) {
       case "GeckoView:DOMFullscreenEntered":
         if (content) {
           content.QueryInterface(Ci.nsIInterfaceRequestor)
                  .getInterface(Ci.nsIDOMWindowUtils)
                  .handleFullscreenRequests();
         }
@@ -120,17 +112,17 @@ class GeckoViewContent extends GeckoView
           }
         }, 500);
       }
       break;
     }
   }
 
   handleEvent(aEvent) {
-    debug("handleEvent " + aEvent.type);
+    debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
       case "contextmenu":
         function nearestParentHref(node) {
           while (node && !node.href) {
             node = node.parentNode;
           }
           return node && node.href;
@@ -192,9 +184,10 @@ class GeckoViewContent extends GeckoView
         this.eventDispatcher.sendRequest({
           type: "GeckoView:DOMWindowClose"
         });
         break;
     }
   }
 }
 
-var contentListener = new GeckoViewContent("GeckoViewContent", this);
+let {debug, warn} = GeckoViewContent.initLogging("GeckoViewContent");
+let module = GeckoViewContent.create(this);
--- a/mobile/android/chrome/geckoview/GeckoViewContentSettings.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContentSettings.js
@@ -5,35 +5,27 @@
 
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewSettings[C]"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 // Handles GeckoView content settings including:
 // * tracking protection
 // * desktop mode
 class GeckoViewContentSettings extends GeckoViewContentModule {
   onInit() {
-    debug("onInit");
+    debug `onInit`;
     this._useDesktopMode = false;
   }
 
   onSettingsUpdate() {
-    debug("onSettingsUpdate");
+    debug `onSettingsUpdate`;
 
     this.displayMode = this.settings.displayMode;
     this.useTrackingProtection = !!this.settings.useTrackingProtection;
     this.useDesktopMode = !!this.settings.useDesktopMode;
   }
 
   get useTrackingProtection() {
     return docShell.useTrackingProtection;
@@ -66,9 +58,10 @@ class GeckoViewContentSettings extends G
   set displayMode(aMode) {
     const docShell = content && GeckoViewUtils.getRootDocShell(content);
     if (docShell) {
       docShell.displayMode = aMode;
     }
   }
 }
 
-var settings = new GeckoViewContentSettings("GeckoViewSettings", this);
+let {debug, warn} = GeckoViewContentSettings.initLogging("GeckoViewSettings");
+let module = GeckoViewContentSettings.create(this);
--- a/mobile/android/chrome/geckoview/GeckoViewNavigationContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewNavigationContent.js
@@ -6,45 +6,40 @@
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   ErrorPageEventHandler: "chrome://geckoview/content/ErrorPageEventHandler.js",
   LoadURIDelegate: "resource://gre/modules/LoadURIDelegate.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-  ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                     {}).AndroidLog.d.bind(null, "ViewNavigation[C]"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 // Implements nsILoadURIDelegate.
 class GeckoViewNavigationContent extends GeckoViewContentModule {
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
 
     docShell.loadURIDelegate = this;
   }
 
   onDisable() {
-    debug("onDisable");
+    debug `onDisable`;
 
     docShell.loadURIDelegate = null;
   }
 
   // nsILoadURIDelegate.
   loadURI(aUri, aWhere, aFlags, aTriggeringPrincipal) {
-    debug("loadURI " + (aUri && aUri.spec) + " " + aWhere + " " + aFlags);
+    debug `loadURI: uri=${ aUri && aUri.spec
+                  } where=${ aWhere
+                  } flags=${ aFlags }`;
 
     // TODO: Remove this when we have a sensible error API.
     if (aUri && aUri.displaySpec.startsWith("about:certerror")) {
       addEventListener("click", ErrorPageEventHandler, true);
     }
 
     return LoadURIDelegate.load(this.eventDispatcher, aUri, aWhere, aFlags,
                                 aTriggeringPrincipal);
   }
 }
 
-var navigationListener = new GeckoViewNavigationContent("GeckoViewNavigation", this);
+let {debug, warn} = GeckoViewNavigationContent.initLogging("GeckoViewNavigation");
+let module = GeckoViewNavigationContent.create(this);
--- a/mobile/android/chrome/geckoview/GeckoViewScrollContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewScrollContent.js
@@ -1,46 +1,39 @@
 /* -*- 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/. */
 
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewScrollContent"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
 
 class GeckoViewScrollContent extends GeckoViewContentModule {
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
     addEventListener("scroll", this, false);
   }
 
   onDisable() {
-    debug("onDisable");
+    debug `onDisable`;
     removeEventListener("scroll", this);
   }
 
   handleEvent(aEvent) {
     if (aEvent.originalTarget.defaultView != content) {
       return;
     }
 
-    debug("handleEvent " + aEvent.type);
+    debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
       case "scroll":
         this.eventDispatcher.sendRequest({
           type: "GeckoView:ScrollChanged",
           scrollX: Math.round(content.scrollX),
           scrollY: Math.round(content.scrollY)
         });
         break;
     }
   }
 }
-var scrollListener = new GeckoViewScrollContent("GeckoViewScroll", this);
+
+let {debug, warn} = GeckoViewScrollContent.initLogging("GeckoViewScroll");
+let module = GeckoViewScrollContent.create(this);
--- a/mobile/android/chrome/geckoview/GeckoViewSelectionActionContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewSelectionActionContent.js
@@ -5,24 +5,16 @@
 
 ChromeUtils.import("resource://gre/modules/GeckoViewContentModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   Services: "resource://gre/modules/Services.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewSelectionActionContent"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 // Dispatches GeckoView:ShowSelectionAction and GeckoView:HideSelectionAction to
 // the GeckoSession on accessible caret changes.
 class GeckoViewSelectionActionContent extends GeckoViewContentModule {
   constructor(aModuleName, aMessageManager) {
     super(aModuleName, aMessageManager);
 
     this._seqNo = 0;
     this._isActive = false;
@@ -124,22 +116,22 @@ class GeckoViewSelectionActionContent ex
         break;
       }
     }
 
     return offset;
   }
 
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
     addEventListener("mozcaretstatechanged", this, { mozSystemGroup: true });
   }
 
   onDisable() {
-    debug("onDisable");
+    debug `onDisable`;
     removeEventListener("mozcaretstatechanged", this, { mozSystemGroup: true });
   }
 
   /**
    * Receive and act on AccessibleCarets caret state-change
    * (mozcaretstatechanged) events.
    */
   handleEvent(aEvent) {
@@ -156,17 +148,17 @@ class GeckoViewSelectionActionContent ex
                reason !== "longpressonemptycontent" &&
                reason !== "taponcaret") {
       // Don't show selection actions when merely focusing on an editor or
       // repositioning the cursor. Wait until long press or the caret is tapped
       // in order to match Android behavior.
       reason = "visibilitychange";
     }
 
-    debug("handleEvent " + reason + " " + aEvent);
+    debug `handleEvent: ${reason}`;
 
     if (["longpressonemptycontent",
          "releasecaret",
          "taponcaret",
          "updateposition"].includes(reason)) {
 
       const actions = this._actions.filter(
           action => action.predicate.call(this, aEvent));
@@ -202,31 +194,29 @@ class GeckoViewSelectionActionContent ex
         // Don't call again if we're already active and things haven't changed.
         return;
       }
 
       msg.seqNo = ++this._seqNo;
       this._isActive = true;
       this._previousMessage = JSON.stringify(msg);
 
-      debug("onShowSelectionAction " + JSON.stringify(msg));
-
       // This event goes to GeckoViewSelectionAction.jsm, where the data is
       // further transformed and then sent to GeckoSession.
       this.eventDispatcher.sendRequest(msg, {
         onSuccess: response => {
           if (response.seqNo !== this._seqNo) {
             // Stale action.
             return;
           }
           let action = actions.find(action => action.id === response.id);
           if (action) {
             action.perform.call(this, aEvent, response);
           } else {
-            dump("Invalid action " + response.id);
+            warn `Invalid action ${response.id}`;
           }
         },
         onError: _ => {
           // Do nothing; we can get here if the delegate was just unregistered.
         },
       });
 
     } else if (["invisibleselection",
@@ -248,15 +238,16 @@ class GeckoViewSelectionActionContent ex
       }
 
       this.eventDispatcher.sendRequest({
         type: "GeckoView:HideSelectionAction",
         reason: reason,
       });
 
     } else {
-      dump("Unknown reason: " + reason);
+      warn `Unknown reason: ${reason}`;
     }
   }
 }
 
-var selectionActionListener =
-    new GeckoViewSelectionActionContent("GeckoViewSelectionAction", this);
+let {debug, warn} =
+    GeckoViewSelectionActionContent.initLogging("GeckoViewSelectionAction");
+let module = GeckoViewSelectionActionContent.create(this);
--- a/mobile/android/chrome/geckoview/geckoview.js
+++ b/mobile/android/chrome/geckoview/geckoview.js
@@ -1,44 +1,44 @@
 /* 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";
 
-ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-ChromeUtils.defineModuleGetter(this, "EventDispatcher",
-  "resource://gre/modules/Messaging.jsm");
-ChromeUtils.defineModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  EventDispatcher: "resource://gre/modules/Messaging.jsm",
+  GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+});
+
 XPCOMUtils.defineLazyGetter(this, "WindowEventDispatcher",
   () => EventDispatcher.for(window));
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "View"));
-
 // Creates and manages GeckoView modules.
 // A module must extend GeckoViewModule.
 // Instantiate a module by calling
 //   add(<resource path>, <type name>)
 // and remove by calling
 //   remove(<type name>)
 var ModuleManager = {
   init: function(aBrowser) {
     this.browser = aBrowser;
     this.modules = new Map();
   },
 
   add: function(aResource, aType, ...aArgs) {
     this.remove(aType);
-    let scope = {};
-    ChromeUtils.import(aResource, scope);
+
+    const scope = {};
+    const global = ChromeUtils.import(aResource, scope);
+    const tag = aType.replace("GeckoView", "GeckoView.");
+    GeckoViewUtils.initLogging(tag, global);
 
     this.modules.set(aType, new scope[aType](
       aType, window, this.browser, WindowEventDispatcher, ...aArgs
     ));
   },
 
   remove: function(aType) {
     this.modules.delete(aType);
@@ -53,16 +53,18 @@ function createBrowser() {
   const browser = window.browser = document.createElement("browser");
   browser.setAttribute("type", "content");
   browser.setAttribute("primary", "true");
   browser.setAttribute("flex", "1");
   return browser;
 }
 
 function startup() {
+  GeckoViewUtils.initLogging("GeckoView.XUL", window);
+
   const browser = createBrowser();
   ModuleManager.init(browser);
 
   ModuleManager.add("resource://gre/modules/GeckoViewNavigation.jsm",
                     "GeckoViewNavigation");
   ModuleManager.add("resource://gre/modules/GeckoViewSettings.jsm",
                     "GeckoViewSettings");
   ModuleManager.add("resource://gre/modules/GeckoViewContent.jsm",
--- a/mobile/android/components/geckoview/GeckoViewExternalAppService.js
+++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.js
@@ -1,55 +1,53 @@
 /* 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";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
+
+/* global debug:false, warn:false */
+GeckoViewUtils.initLogging("GeckoView.ExternalAppService", this);
 
 ChromeUtils.defineModuleGetter(this, "EventDispatcher",
   "resource://gre/modules/Messaging.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewContent"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 function ExternalAppService() {
   this.wrappedJSObject = this;
 }
 
 ExternalAppService.prototype = {
   classID: Components.ID("{a89eeec6-6608-42ee-a4f8-04d425992f45}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIExternalHelperAppService]),
 
   doContent(mimeType, request, context, forceSave) {
     const channel = request.QueryInterface(Ci.nsIChannel);
     const mm = context.QueryInterface(Ci.nsIDocShell).tabChild.messageManager;
 
-    debug(`doContent() URI=${channel.URI.displaySpec}, contentType=${channel.contentType}`);
+    debug `doContent: uri=${ channel.URI.displaySpec
+                    } contentType=${ channel.contentType }`;
 
     EventDispatcher.forMessageManager(mm).sendRequest({
       type: "GeckoView:ExternalResponse",
       uri: channel.URI.displaySpec,
       contentType: channel.contentType,
       contentLength: channel.contentLength,
       filename: channel.contentDispositionFilename
     });
 
     request.cancel(Cr.NS_ERROR_ABORT);
     Components.returnCode = Cr.NS_ERROR_ABORT;
   },
 
   applyDecodingForExtension(ext, encoding) {
-    debug(`applyDecodingForExtension() extension=${ext}, encoding=${encoding}`);
+    debug `applyDecodingForExtension: extension=${ ext
+                                    } encoding=${ encoding }`;
 
     // This doesn't matter for us right now because
     // we shouldn't end up reading the stream.
     return true;
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExternalAppService]);
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/geckoview/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = {
+  "globals": {
+    "debug": false,
+    "warn": false,
+  },
+};
--- a/mobile/android/modules/geckoview/AndroidLog.jsm
+++ b/mobile/android/modules/geckoview/AndroidLog.jsm
@@ -48,36 +48,36 @@ if (typeof Components != "undefined") {
 // From <https://android.googlesource.com/platform/system/core/+/master/include/android/log.h>.
 const ANDROID_LOG_VERBOSE = 2;
 const ANDROID_LOG_DEBUG = 3;
 const ANDROID_LOG_INFO = 4;
 const ANDROID_LOG_WARN = 5;
 const ANDROID_LOG_ERROR = 6;
 
 // android.util.Log.isLoggable throws IllegalArgumentException if a tag length
-// exceeds 23 characters, and we prepend five characters ("Gecko") to every tag,
-// so we truncate tags exceeding 18 characters (although __android_log_write
-// itself and other android.util.Log methods don't seem to mind longer tags).
+// exceeds 23 characters, and we prepend five characters ("Gecko") to every tag.
+// However, __android_log_write itself and other android.util.Log methods don't
+// seem to mind longer tags.
 const MAX_TAG_LENGTH = 18;
 
 var liblog = ctypes.open("liblog.so"); // /system/lib/liblog.so
 var __android_log_write = liblog.declare("__android_log_write",
                                          ctypes.default_abi,
                                          ctypes.int, // return value: num bytes logged
                                          ctypes.int, // priority (ANDROID_LOG_* constant)
                                          ctypes.char.ptr, // tag
                                          ctypes.char.ptr); // message
 
 var AndroidLog = {
   MAX_TAG_LENGTH: MAX_TAG_LENGTH,
-  v: (tag, msg) => __android_log_write(ANDROID_LOG_VERBOSE, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
-  d: (tag, msg) => __android_log_write(ANDROID_LOG_DEBUG, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
-  i: (tag, msg) => __android_log_write(ANDROID_LOG_INFO, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
-  w: (tag, msg) => __android_log_write(ANDROID_LOG_WARN, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
-  e: (tag, msg) => __android_log_write(ANDROID_LOG_ERROR, "Gecko" + tag.substring(0, MAX_TAG_LENGTH), msg),
+  v: (tag, msg) => __android_log_write(ANDROID_LOG_VERBOSE, "Gecko" + tag, msg),
+  d: (tag, msg) => __android_log_write(ANDROID_LOG_DEBUG, "Gecko" + tag, msg),
+  i: (tag, msg) => __android_log_write(ANDROID_LOG_INFO, "Gecko" + tag, msg),
+  w: (tag, msg) => __android_log_write(ANDROID_LOG_WARN, "Gecko" + tag, msg),
+  e: (tag, msg) => __android_log_write(ANDROID_LOG_ERROR, "Gecko" + tag, msg),
 
   bind: function(tag) {
     return {
       MAX_TAG_LENGTH: MAX_TAG_LENGTH,
       v: AndroidLog.v.bind(null, tag),
       d: AndroidLog.d.bind(null, tag),
       i: AndroidLog.i.bind(null, tag),
       w: AndroidLog.w.bind(null, tag),
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -4,24 +4,16 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewContent"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewContent"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 class GeckoViewContent extends GeckoViewModule {
   onInit() {
     this.eventDispatcher.registerListener(this, [
       "GeckoView:SetActive"
     ]);
   }
 
   onEnable() {
@@ -50,17 +42,18 @@ class GeckoViewContent extends GeckoView
     this.unregisterListener();
 
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExit", this);
     this.messageManager.removeMessageListener("GeckoView:DOMFullscreenRequest", this);
   }
 
   // Bundle event handler.
   onEvent(aEvent, aData, aCallback) {
-    debug("onEvent: " + aEvent);
+    debug `onEvent: event=${aEvent}, data=${aData}`;
+
     switch (aEvent) {
       case "GeckoViewContent:ExitFullScreen":
         this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited");
         break;
       case "GeckoView:ZoomToInput":
         this.messageManager.sendAsyncMessage(aEvent);
         break;
       case "GeckoView:SetActive":
@@ -74,34 +67,34 @@ class GeckoViewContent extends GeckoView
           this.browser.blur();
         }
         break;
     }
   }
 
   // DOM event handler
   handleEvent(aEvent) {
-    debug("handleEvent: aEvent.type=" + aEvent.type);
+    debug `handleEvent: ${aEvent.type}`;
 
     switch (aEvent.type) {
       case "MozDOMFullscreen:Entered":
         if (this.browser == aEvent.target) {
           // Remote browser; dispatch to content process.
           this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenEntered");
         }
         break;
       case "MozDOMFullscreen:Exited":
         this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited");
         break;
     }
   }
 
   // Message manager event handler.
   receiveMessage(aMsg) {
-    debug("receiveMessage " + aMsg.name);
+    debug `receiveMessage: ${aMsg.name}`;
 
     switch (aMsg.name) {
       case "GeckoView:DOMFullscreenExit":
         this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                    .getInterface(Ci.nsIDOMWindowUtils)
                    .remoteFrameFullscreenReverted();
         break;
       case "GeckoView:DOMFullscreenRequest":
--- a/mobile/android/modules/geckoview/GeckoViewContentModule.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContentModule.jsm
@@ -2,29 +2,34 @@
  * 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 = ["GeckoViewContentModule"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
+
+GeckoViewUtils.initLogging("GeckoView.Module.[C]", this);
 
 ChromeUtils.defineModuleGetter(this, "EventDispatcher",
   "resource://gre/modules/Messaging.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewContentModule"));
+class GeckoViewContentModule {
+  static initLogging(aModuleName) {
+    this._moduleName = aModuleName;
+    const tag = aModuleName.replace("GeckoView", "GeckoView.") + ".[C]";
+    return GeckoViewUtils.initLogging(tag, {});
+  }
 
-// function debug(aMsg) {
-//   dump(aMsg);
-// }
+  static create(aGlobal, aModuleName) {
+    return new this(aModuleName || this._moduleName, aGlobal);
+  }
 
-class GeckoViewContentModule {
   constructor(aModuleName, aMessageManager) {
     this.moduleName = aModuleName;
     this.messageManager = aMessageManager;
     this.eventDispatcher = EventDispatcher.forMessageManager(aMessageManager);
 
     this.messageManager.addMessageListener(
       "GeckoView:UpdateSettings",
       aMsg => {
--- a/mobile/android/modules/geckoview/GeckoViewModule.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewModule.jsm
@@ -2,24 +2,19 @@
  * 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 = ["GeckoViewModule"];
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewModule"));
-
-// function debug(aMsg) {
-//   dump(aMsg);
-// }
+GeckoViewUtils.initLogging("GeckoView.Module", this);
 
 class GeckoViewModule {
   constructor(aModuleName, aWindow, aBrowser, aEventDispatcher) {
     this.isRegistered = false;
     this.window = aWindow;
     this.browser = aBrowser;
     this.eventDispatcher = aEventDispatcher;
     this.moduleName = aModuleName;
@@ -131,53 +126,53 @@ class EventProxy {
     this.listener = aListener;
     this.eventDispatcher = aEventDispatcher;
     this._eventQueue = [];
     this._registeredEvents = [];
     this._enableQueuing = false;
   }
 
   registerListener(aEventList) {
-    debug("register " + aEventList);
+    debug `registerListener ${aEventList}`;
     this.eventDispatcher.registerListener(this, aEventList);
     this._registeredEvents = this._registeredEvents.concat(aEventList);
   }
 
   unregisterListener() {
-    debug("unregister");
+    debug `unregisterListener`;
     if (this._registeredEvents.length === 0) {
       return;
     }
     this.eventDispatcher.unregisterListener(this, this._registeredEvents);
     this._registeredEvents = [];
   }
 
   onEvent(aEvent, aData, aCallback) {
     if (this._enableQueuing) {
-      debug("queue " + aEvent + ", aData=" + JSON.stringify(aData));
+      debug `queue ${aEvent}, data=${aData}`;
       this._eventQueue.unshift(arguments);
     } else {
       this._dispatch(...arguments);
     }
   }
 
   enableQueuing(aEnable) {
-    debug("enableQueuing " + aEnable);
+    debug `enableQueuing ${aEnable}`;
     this._enableQueuing = aEnable;
   }
 
   _dispatch(aEvent, aData, aCallback) {
-    debug("dispatch " + aEvent + ", aData=" + JSON.stringify(aData));
+    debug `dispatch ${aEvent}, data=${aData}`;
     if (this.listener.onEvent) {
       this.listener.onEvent(...arguments);
     } else {
       this.listener(...arguments);
     }
   }
 
   dispatchQueuedEvents() {
-    debug("dispatchQueued");
+    debug `dispatchQueued`;
     while (this._eventQueue.length) {
       const args = this._eventQueue.pop();
       this._dispatch(...args);
     }
   }
 }
--- a/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewNavigation.jsm
@@ -13,24 +13,16 @@ ChromeUtils.defineModuleGetter(this, "Br
                                "resource://gre/modules/BrowserUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   EventDispatcher: "resource://gre/modules/Messaging.jsm",
   LoadURIDelegate: "resource://gre/modules/LoadURIDelegate.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewNavigation"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 // Handles navigation requests between Gecko and a GeckoView.
 // Handles GeckoView:GoBack and :GoForward requests dispatched by
 // GeckoView.goBack and .goForward.
 // Dispatches GeckoView:LocationChange to the GeckoView on location change when
 // active.
 // Implements nsIBrowserDOMWindow.
 class GeckoViewNavigation extends GeckoViewModule {
   onInitBrowser() {
@@ -49,17 +41,17 @@ class GeckoViewNavigation extends GeckoV
       "GeckoView:LoadUri",
       "GeckoView:Reload",
       "GeckoView:Stop"
     ]);
   }
 
   // Bundle event handler.
   onEvent(aEvent, aData, aCallback) {
-    debug("onEvent: aEvent=" + aEvent + ", aData=" + JSON.stringify(aData));
+    debug `onEvent: event=${aEvent}, data=${aData}`;
 
     switch (aEvent) {
       case "GeckoView:GoBack":
         this.browser.goBack();
         break;
       case "GeckoView:GoForward":
         this.browser.goForward();
         break;
@@ -96,17 +88,17 @@ class GeckoViewNavigation extends GeckoV
       case "GeckoView:Stop":
         this.browser.stop();
         break;
     }
   }
 
   // Message manager event handler.
   receiveMessage(aMsg) {
-    debug("receiveMessage " + aMsg.name);
+    debug `receiveMessage: ${aMsg.name}`;
   }
 
   waitAndSetOpener(aSessionId, aOpener) {
     if (!aSessionId) {
       return Promise.resolve(null);
     }
 
     return new Promise(resolve => {
@@ -123,19 +115,19 @@ class GeckoViewNavigation extends GeckoV
       };
 
       // This event is emitted from createBrowser() in geckoview.js
       Services.obs.addObserver(handler, "geckoview-window-created");
     });
   }
 
   handleNewSession(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal) {
-    debug("handleNewSession: aUri=" + (aUri && aUri.spec) +
-          " aWhere=" + aWhere +
-          " aFlags=" + aFlags);
+    debug `handleNewSession: uri=${ aUri && aUri.spec
+                           } where=${ aWhere
+                           } flags=${ aFlags }`;
 
     if (!this.isRegistered) {
       return null;
     }
 
     const message = {
       type: "GeckoView:OnNewSession",
       uri: aUri ? aUri.displaySpec : ""
@@ -153,19 +145,19 @@ class GeckoViewNavigation extends GeckoV
 
     // Wait indefinitely for app to respond with a browser or null
     Services.tm.spinEventLoopUntil(() => browser !== undefined);
     return browser;
   }
 
   // nsIBrowserDOMWindow.
   createContentWindow(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal) {
-    debug("createContentWindow: aUri=" + (aUri && aUri.spec) +
-          " aWhere=" + aWhere +
-          " aFlags=" + aFlags);
+    debug `createContentWindow: uri=${ aUri && aUri.spec
+                              } where=${ aWhere
+                              } flags=${ aFlags }`;
 
     if (LoadURIDelegate.load(this.eventDispatcher, aUri, aWhere, aFlags,
                              aTriggeringPrincipal)) {
       // The app has handled the load, abort open-window handling.
       Components.returnCode = Cr.NS_ERROR_ABORT;
       return null;
     }
 
@@ -177,22 +169,22 @@ class GeckoViewNavigation extends GeckoV
     }
 
     return browser.contentWindow;
   }
 
   // nsIBrowserDOMWindow.
   createContentWindowInFrame(aUri, aParams, aWhere, aFlags, aNextTabParentId,
                              aName) {
-    debug("createContentWindowInFrame: aUri=" + (aUri && aUri.spec) +
-          " aParams=" + aParams +
-          " aWhere=" + aWhere +
-          " aFlags=" + aFlags +
-          " aNextTabParentId=" + aNextTabParentId +
-          " aName=" + aName);
+    debug `createContentWindowInFrame: uri=${ aUri && aUri.spec
+                                     } params=${ aParams
+                                     } where=${ aWhere
+                                     } flags=${ aFlags
+                                     } nextTabParentId=${ aNextTabParentId
+                                     } name=${ aName }`;
 
     if (LoadURIDelegate.load(this.eventDispatcher, aUri, aWhere, aFlags, null)) {
       // The app has handled the load, abort open-window handling.
       Components.returnCode = Cr.NS_ERROR_ABORT;
       return null;
     }
 
     const browser = this.handleNewSession(aUri, null, aWhere, aFlags, null);
@@ -202,19 +194,19 @@ class GeckoViewNavigation extends GeckoV
     }
 
     browser.setAttribute("nextTabParentId", aNextTabParentId);
     return browser;
   }
 
   handleOpenUri(aUri, aOpener, aWhere, aFlags, aTriggeringPrincipal,
                 aNextTabParentId) {
-    debug("handleOpenUri: aUri=" + (aUri && aUri.spec) +
-          " aWhere=" + aWhere +
-          " aFlags=" + aFlags);
+    debug `handleOpenUri: uri=${ aUri && aUri.spec
+                        } where=${ aWhere
+                        } flags=${ aFlags }`;
 
     if (LoadURIDelegate.load(this.eventDispatcher, aUri, aWhere, aFlags,
                              aTriggeringPrincipal)) {
       return null;
     }
 
     let browser = this.browser;
 
@@ -249,59 +241,57 @@ class GeckoViewNavigation extends GeckoV
 
   // nsIBrowserDOMWindow.
   isTabContentWindow(aWindow) {
     return this.browser.contentWindow === aWindow;
   }
 
   // nsIBrowserDOMWindow.
   canClose() {
-    debug("canClose");
+    debug `canClose`;
     return true;
   }
 
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
 
     this.registerContent(
       "chrome://geckoview/content/GeckoViewNavigationContent.js");
 
     let flags = Ci.nsIWebProgress.NOTIFY_LOCATION;
     this.progressFilter =
       Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
       .createInstance(Ci.nsIWebProgress);
     this.progressFilter.addProgressListener(this, flags);
     this.browser.addProgressListener(this.progressFilter, flags);
   }
 
   onDisable() {
-    debug("onDisable");
+    debug `onDisable`;
 
     if (!this.progressFilter) {
       return;
     }
     this.progressFilter.removeProgressListener(this);
     this.browser.removeProgressListener(this.progressFilter);
   }
 
   // WebProgress event handler.
   onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
-    debug("onLocationChange");
+    debug `onLocationChange`;
 
     let fixedURI = aLocationURI;
 
     try {
       fixedURI = Services.uriFixup.createExposableURI(aLocationURI);
     } catch (ex) { }
 
     let message = {
       type: "GeckoView:LocationChange",
       uri: fixedURI.displaySpec,
       canGoBack: this.browser.canGoBack,
       canGoForward: this.browser.canGoForward,
       isTopLevel: aWebProgress.isTopLevel,
     };
 
-    debug("dispatch " + JSON.stringify(message));
-
     this.eventDispatcher.sendRequest(message);
   }
 }
--- a/mobile/android/modules/geckoview/GeckoViewProgress.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewProgress.jsm
@@ -11,24 +11,16 @@ ChromeUtils.import("resource://gre/modul
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "OverrideService",
   "@mozilla.org/security/certoverride;1", "nsICertOverrideService");
 
 XPCOMUtils.defineLazyServiceGetter(this, "IDNService",
   "@mozilla.org/network/idn-service;1", "nsIIDNService");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewProgress"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 var IdentityHandler = {
   // The definitions below should be kept in sync with those in GeckoView.ProgressListener.SecurityInformation
   // No trusted identity information. No site identity icon is shown.
   IDENTITY_MODE_UNKNOWN: 0,
 
   // Domain-Validation SSL CA-signed domain verification (DV).
   IDENTITY_MODE_IDENTIFIED: 1,
 
@@ -187,54 +179,56 @@ var IdentityHandler = {
 };
 
 class GeckoViewProgress extends GeckoViewModule {
   onInit() {
     this._hostChanged = false;
   }
 
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
 
     let flags = Ci.nsIWebProgress.NOTIFY_STATE_NETWORK |
                 Ci.nsIWebProgress.NOTIFY_SECURITY |
                 Ci.nsIWebProgress.NOTIFY_LOCATION;
     this.progressFilter =
       Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
       .createInstance(Ci.nsIWebProgress);
     this.progressFilter.addProgressListener(this, flags);
     this.browser.addProgressListener(this.progressFilter, flags);
   }
 
   onDisable() {
-    debug("onDisable");
+    debug `onDisable`;
 
     if (this.progressFilter) {
       this.progressFilter.removeProgressListener(this);
       this.browser.removeProgressListener(this.progressFilter);
     }
   }
 
   onSettingsUpdate() {
-    let settings = this.settings;
-    debug("onSettingsUpdate: " + JSON.stringify(settings));
+    const settings = this.settings;
+    debug `onSettingsUpdate: ${settings}`;
 
     IdentityHandler.setUseTrackingProtection(!!settings.useTrackingProtection);
     IdentityHandler.setUsePrivateMode(!!settings.usePrivateMode);
   }
 
   onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
-    debug(`onStateChange() isTopLevel=${aWebProgress.isTopLevel}, stateFlags=${aStateFlags}, state=${aStatus}`);
+    debug `onStateChange: isTopLevel=${ aWebProgress.isTopLevel
+                       }, flags=${ aStateFlags
+                       }, status=${ aStatus }`;
 
     if (!aWebProgress.isTopLevel) {
       return;
     }
 
     const uriSpec = aRequest.QueryInterface(Ci.nsIChannel).URI.displaySpec;
-    debug(`onStateChange() URI=${uriSpec}`);
+    debug `onStateChange: uri=${uriSpec}`;
 
     if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
       const message = {
         type: "GeckoView:PageStart",
         uri: uriSpec,
       };
 
       this.eventDispatcher.sendRequest(message);
@@ -245,17 +239,17 @@ class GeckoViewProgress extends GeckoVie
         success: !aStatus
       };
 
       this.eventDispatcher.sendRequest(message);
     }
   }
 
   onSecurityChange(aWebProgress, aRequest, aState) {
-    debug("onSecurityChange()");
+    debug `onSecurityChange`;
 
     // Don't need to do anything if the data we use to update the UI hasn't changed
     if (this._state === aState && !this._hostChanged) {
       return;
     }
 
     this._state = aState;
     this._hostChanged = false;
@@ -266,17 +260,18 @@ class GeckoViewProgress extends GeckoVie
       type: "GeckoView:SecurityChanged",
       identity: identity
     };
 
     this.eventDispatcher.sendRequest(message);
   }
 
   onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
-    debug(`onLocationChange() location=${aLocationURI.displaySpec}, flags=${aFlags}`);
+    debug `onLocationChange: location=${ aLocationURI.displaySpec
+                          }, flags=${ aFlags }`;
 
     this._hostChanged = true;
     if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
       // We apparently don't get a STATE_STOP in onStateChange(), so emit PageStop here
       this.eventDispatcher.sendRequest({
         type: "GeckoView:PageStop",
         success: false
       });
--- a/mobile/android/modules/geckoview/GeckoViewRemoteDebugger.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewRemoteDebugger.jsm
@@ -5,30 +5,22 @@
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewRemoteDebugger"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-  ChromeUtils.import("resource://gre/modules/AndroidLog.jsm", {})
-    .AndroidLog.d.bind(null, "ViewRemoteDebugger"));
-
 XPCOMUtils.defineLazyGetter(this, "DebuggerServer", () => {
   const { require } = ChromeUtils.import("resource://devtools/shared/Loader.jsm", {});
   const { DebuggerServer } = require("devtools/server/main");
   return DebuggerServer;
 });
 
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 class GeckoViewRemoteDebugger extends GeckoViewModule {
   onInit() {
     this._isEnabled = false;
     this._usbDebugger = new USBRemoteDebugger();
   }
 
   onSettingsUpdate() {
     let enabled = this.settings.useRemoteDebugger;
@@ -50,17 +42,17 @@ class GeckoViewRemoteDebugger extends Ge
     let windowId = this.window.QueryInterface(Ci.nsIInterfaceRequestor)
                               .getInterface(Ci.nsIDOMWindowUtils)
                               .outerWindowID;
     let env = Cc["@mozilla.org/process/environment;1"]
               .getService(Ci.nsIEnvironment);
     let dataDir = env.get("MOZ_ANDROID_DATA_DIR");
 
     if (!dataDir) {
-      debug("Missing env MOZ_ANDROID_DATA_DIR - aborting debugger server start");
+      warn `Missing env MOZ_ANDROID_DATA_DIR - aborting debugger server start`;
       return;
     }
 
     this._isEnabled = true;
     this._usbDebugger.stop();
 
     let portOrPath = dataDir + "/firefox-debugger-socket-" + windowId;
     this._usbDebugger.start(portOrPath);
@@ -77,32 +69,32 @@ class USBRemoteDebugger {
     try {
       let AuthenticatorType = DebuggerServer.Authenticators.get("PROMPT");
       let authenticator = new AuthenticatorType.Server();
       authenticator.allowConnection = this.allowConnection.bind(this);
       this._listener = DebuggerServer.createListener();
       this._listener.portOrPath = aPortOrPath;
       this._listener.authenticator = authenticator;
       this._listener.open();
-      debug(`USB remote debugger - listening on ${aPortOrPath}`);
+      debug `USB remote debugger - listening on ${aPortOrPath}`;
     } catch (e) {
-      debug("Unable to start USB debugger server: " + e);
+      warn `Unable to start USB debugger server: ${e}`;
     }
   }
 
   stop() {
     if (!this._listener) {
       return;
     }
 
     try {
       this._listener.close();
       this._listener = null;
     } catch (e) {
-      debug("Unable to stop USB debugger server: " + e);
+      warn `Unable to stop USB debugger server: ${e}`;
     }
   }
 
   allowConnection(aSession) {
     if (!this._listener) {
       return DebuggerServer.AuthenticationResult.DENY;
     }
 
--- a/mobile/android/modules/geckoview/GeckoViewScroll.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewScroll.jsm
@@ -4,22 +4,14 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewScroll"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewScroll"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 class GeckoViewScroll extends GeckoViewModule {
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
     this.registerContent("chrome://geckoview/content/GeckoViewScrollContent.js");
   }
 }
--- a/mobile/android/modules/geckoview/GeckoViewSelectionAction.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewSelectionAction.jsm
@@ -4,23 +4,15 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewSelectionAction"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewSelectionAction"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 // Handles inter-op between accessible carets and GeckoSession.
 class GeckoViewSelectionAction extends GeckoViewModule {
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
     this.registerContent("chrome://geckoview/content/GeckoViewSelectionActionContent.js");
   }
 }
--- a/mobile/android/modules/geckoview/GeckoViewSettings.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewSettings.jsm
@@ -24,24 +24,16 @@ XPCOMUtils.defineLazyGetter(
   this, "DESKTOP_USER_AGENT",
   function() {
     return Cc["@mozilla.org/network/protocol;1?name=http"]
            .getService(Ci.nsIHttpProtocolHandler).userAgent
            .replace(/Android \d.+?; [a-zA-Z]+/, "X11; Linux x86_64")
            .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
   });
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewSettings"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 // Handles GeckoView settings including:
 // * multiprocess
 // * user agent override
 class GeckoViewSettings extends GeckoViewModule {
   onInitBrowser() {
     if (this.settings.useMultiprocess) {
       this.browser.setAttribute("remote", "true");
     }
@@ -51,38 +43,39 @@ class GeckoViewSettings extends GeckoVie
     this._useTrackingProtection = false;
     this._useDesktopMode = false;
 
     this.registerContent(
         "chrome://geckoview/content/GeckoViewContentSettings.js");
   }
 
   onSettingsUpdate() {
-    debug("onSettingsUpdate: " + JSON.stringify(this.settings));
+    const settings = this.settings;
+    debug `onSettingsUpdate: ${settings}`;
 
-    this.displayMode = this.settings.displayMode;
-    this.useTrackingProtection = !!this.settings.useTrackingProtection;
-    this.useDesktopMode = !!this.settings.useDesktopMode;
+    this.displayMode = settings.displayMode;
+    this.useTrackingProtection = !!settings.useTrackingProtection;
+    this.useDesktopMode = !!settings.useDesktopMode;
   }
 
   get useMultiprocess() {
     return this.browser.isRemoteBrowser;
   }
 
   get useTrackingProtection() {
     return this._useTrackingProtection;
   }
 
   set useTrackingProtection(aUse) {
     aUse && SafeBrowsing;
     this._useTrackingProtection = aUse;
   }
 
   onUserAgentRequest(aSubject, aTopic, aData) {
-    debug("onUserAgentRequest");
+    debug `onUserAgentRequest`;
 
     let channel = aSubject.QueryInterface(Ci.nsIHttpChannel);
 
     if (this.browser.outerWindowID !== channel.topLevelOuterContentWindowId) {
       return;
     }
 
     if (this.useDesktopMode) {
--- a/mobile/android/modules/geckoview/GeckoViewTab.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewTab.jsm
@@ -4,24 +4,16 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewTab"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-                       {}).AndroidLog.d.bind(null, "ViewTab"));
-
-// function debug(aMsg) {
-//   dump(aMsg);
-// }
-
 // Stub BrowserApp implementation for WebExtensions support.
 class GeckoViewTab extends GeckoViewModule {
   onInit() {
     this.browser.tab = { id: 0, browser: this.browser };
 
     this.window.gBrowser = this.window.BrowserApp = {
       selectedBrowser: this.browser,
       tabs: [this.browser.tab],
--- a/mobile/android/modules/geckoview/GeckoViewTrackingProtection.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewTrackingProtection.jsm
@@ -4,38 +4,30 @@
 
 "use strict";
 
 var EXPORTED_SYMBOLS = ["GeckoViewTrackingProtection"];
 
 ChromeUtils.import("resource://gre/modules/GeckoViewModule.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "dump", () =>
-    ChromeUtils.import("resource://gre/modules/AndroidLog.jsm",
-              {}).AndroidLog.d.bind(null, "ViewTrackingProtection"));
-
-function debug(aMsg) {
-  // dump(aMsg);
-}
-
 class GeckoViewTrackingProtection extends GeckoViewModule {
   onEnable() {
-    debug("onEnable");
+    debug `onEnable`;
 
     const flags = Ci.nsIWebProgress.NOTIFY_SECURITY;
     this.progressFilter =
       Cc["@mozilla.org/appshell/component/browser-status-filter;1"]
       .createInstance(Ci.nsIWebProgress);
     this.progressFilter.addProgressListener(this, flags);
     this.browser.addProgressListener(this.progressFilter, flags);
   }
 
   onSecurityChange(aWebProgress, aRequest, aState) {
-    debug("onSecurityChange");
+    debug `onSecurityChange`;
 
     if (!(aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) ||
         !aRequest || !(aRequest instanceof Ci.nsIClassifiedChannel)) {
       return;
     }
 
     let channel = aRequest.QueryInterface(Ci.nsIChannel);
     let uri = channel.URI && channel.URI.spec;
--- a/mobile/android/modules/geckoview/GeckoViewUtils.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewUtils.jsm
@@ -1,18 +1,19 @@
 /* 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";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+  EventDispatcher: "resource://gre/modules/Messaging.jsm",
+  Log: "resource://gre/modules/Log.jsm",
   Services: "resource://gre/modules/Services.jsm",
-  EventDispatcher: "resource://gre/modules/Messaging.jsm",
 });
 
 var EXPORTED_SYMBOLS = ["GeckoViewUtils"];
 
 var GeckoViewUtils = {
   /**
    * Define a lazy getter that loads an object from external code, and
    * optionally handles observer and/or message manager notifications for the
@@ -247,12 +248,96 @@ var GeckoViewUtils = {
       dispatcher = this.getDispatcherForWindow(
           iter.getNext().QueryInterface(Ci.nsIDOMWindow));
       if (dispatcher) {
         return dispatcher;
       }
     }
     return null;
   },
+
+  /**
+   * Add logging functions to the specified scope that forward to the given
+   * Log.jsm 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 tag Name of the Log.jsm logger to forward logs to.
+   * @param scope Scope to add the logging functions to.
+   */
+  initLogging: function(tag, scope) {
+    // 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;
+      });
+
+      scope[level] = new Proxy(log, {
+        set: (obj, prop, value) => obj([prop + " = ", ""], value) || true,
+      });
+    }
+    return scope;
+  },
+
+  get rootLogger() {
+    if (!this._rootLogger) {
+      this._rootLogger = Log.repository.getLogger("GeckoView");
+      this._rootLogger.addAppender(new Log.AndroidAppender());
+    }
+    return this._rootLogger;
+  },
+
+  _log: function(logger, level, strings, exprs) {
+    if (!Array.isArray(strings)) {
+      const [, file, line] =
+          (new Error()).stack.match(/.*\n.*\n.*@(.*):(\d+):/);
+      throw Error(`Expecting template literal: ${level} \`foo \${bar}\``,
+                  file, +line);
+    }
+
+    // Do some GeckoView-specific formatting:
+    // 1) Heuristically format flags as hex.
+    // 2) Heuristically format nsresult as string name or hex.
+    for (let i = 0; i < exprs.length; i++) {
+      const expr = exprs[i];
+      switch (typeof expr) {
+        case "number":
+          if (expr > 0 && /\ba?[fF]lags?[\s=:]+$/.test(strings[i])) {
+            // Likely a flag; display in hex.
+            exprs[i] = `0x${expr.toString(0x10)}`;
+          } else if (expr >= 0 && /\b(a?[sS]tatus|rv)[\s=:]+$/.test(strings[i])) {
+            // Likely an nsresult; display in name or hex.
+            exprs[i] = `0x${expr.toString(0x10)}`;
+            for (const name in Cr) {
+              if (expr === Cr[name]) {
+                exprs[i] = name;
+                break;
+              }
+            }
+          }
+          break;
+      }
+    }
+
+    return logger[level](strings, ...exprs);
+  },
 };
 
 XPCOMUtils.defineLazyGetter(GeckoViewUtils, "IS_PARENT_PROCESS", _ =>
     Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT);
--- a/mobile/android/tests/browser/chrome/test_android_log.html
+++ b/mobile/android/tests/browser/chrome/test_android_log.html
@@ -65,21 +65,21 @@ Migrated from Robocop: https://bugzilla.
   is(DEBUG_BYTES, Log.d(DEBUG_MESSAGE), "debug bytes correct after bind");
   is(INFO_BYTES, Log.i(INFO_MESSAGE), "info bytes correct after bind");
   is(WARNING_BYTES, Log.w(WARNING_MESSAGE), "warning bytes correct after bind");
   is(ERROR_BYTES, Log.e(ERROR_MESSAGE), "error bytes correct after bind");
 
   // Ensure the functions work when the tag length is greater than the maximum
   // tag length.
   let tag = "X".repeat(AndroidLog.MAX_TAG_LENGTH + 1);
-  is(AndroidLog.MAX_TAG_LENGTH + 54, AndroidLog.v(tag, "This is a verbose message with a too-long tag."), "verbose message with too-long tag");
-  is(AndroidLog.MAX_TAG_LENGTH + 52, AndroidLog.d(tag, "This is a debug message with a too-long tag."), "debug message with too-long tag");
-  is(AndroidLog.MAX_TAG_LENGTH + 52, AndroidLog.i(tag, "This is an info message with a too-long tag."), "info message with too-long tag");
-  is(AndroidLog.MAX_TAG_LENGTH + 54, AndroidLog.w(tag, "This is a warning message with a too-long tag."), "warning message with too-long tag");
-  is(AndroidLog.MAX_TAG_LENGTH + 53, AndroidLog.e(tag, "This is an error message with a too-long tag."), "error message with too-long tag");
+  is(AndroidLog.MAX_TAG_LENGTH + 55, AndroidLog.v(tag, "This is a verbose message with a too-long tag."), "verbose message with too-long tag");
+  is(AndroidLog.MAX_TAG_LENGTH + 53, AndroidLog.d(tag, "This is a debug message with a too-long tag."), "debug message with too-long tag");
+  is(AndroidLog.MAX_TAG_LENGTH + 53, AndroidLog.i(tag, "This is an info message with a too-long tag."), "info message with too-long tag");
+  is(AndroidLog.MAX_TAG_LENGTH + 55, AndroidLog.w(tag, "This is a warning message with a too-long tag."), "warning message with too-long tag");
+  is(AndroidLog.MAX_TAG_LENGTH + 54, AndroidLog.e(tag, "This is an error message with a too-long tag."), "error message with too-long tag");
 
   // We should also ensure that the module is accessible from a ChromeWorker,
   // but there doesn't seem to be a way to load a ChromeWorker from this test.
 
   </script>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1004825">Mozilla Bug 1004825</a>
--- a/toolkit/modules/Log.jsm
+++ b/toolkit/modules/Log.jsm
@@ -9,22 +9,22 @@ var EXPORTED_SYMBOLS = ["Log"];
 const ONE_BYTE = 1;
 const ONE_KILOBYTE = 1024 * ONE_BYTE;
 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
 
 const STREAM_SEGMENT_SIZE = 4096;
 const PR_UINT32_MAX = 0xffffffff;
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "OS",
-                               "resource://gre/modules/osfile.jsm");
-ChromeUtils.defineModuleGetter(this, "Task",
-                               "resource://gre/modules/Task.jsm");
-ChromeUtils.defineModuleGetter(this, "Services",
-                               "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AndroidLog: "resource://gre/modules/AndroidLog.jsm", // Only used on Android.
+  OS: "resource://gre/modules/osfile.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+  Task: "resource://gre/modules/Task.jsm",
+});
 const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
 
 
 /*
  * Dump a message everywhere we can if we have a failure.
  */
 function dumpError(text) {
   dump(text + "\n");
@@ -81,16 +81,17 @@ var Log = {
   BasicFormatter,
   MessageOnlyFormatter,
   StructuredFormatter,
 
   Appender,
   DumpAppender,
   ConsoleAppender,
   StorageStreamAppender,
+  AndroidAppender,
 
   FileAppender,
   BoundedFileAppender,
 
   ParameterFormatter,
   // Logging helper:
   // let logger = Log.repository.getLogger("foo");
   // logger.info(Log.enumerateInterfaces(someObject).join(","));
@@ -412,54 +413,85 @@ Logger.prototype = {
     } else {
       level = this.level;
     }
 
     params.action = action;
     this.log(level, params._message, params);
   },
 
+  _unpackTemplateLiteral(string, params) {
+    if (!Array.isArray(params)) {
+      // Regular log() call.
+      return [string, params];
+    }
+
+    if (!Array.isArray(string)) {
+      // Not using template literal. However params was packed into an array by
+      // the this.[level] call, so we need to unpack it here.
+      return [string, params[0]];
+    }
+
+    // We're using template literal format (logger.warn `foo ${bar}`). Turn the
+    // template strings into one string containing "${0}"..."${n}" tokens, and
+    // feed it to the basic formatter. The formatter will treat the numbers as
+    // indices into the params array, and convert the tokens to the params.
+
+    if (!params.length) {
+      // No params; we need to set params to undefined, so the formatter
+      // doesn't try to output the params array.
+      return [string[0], undefined];
+    }
+
+    let concat = string[0];
+    for (let i = 0; i < params.length; i++) {
+      concat += `\${${i}}${string[i + 1]}`;
+    }
+    return [concat, params];
+  },
+
   log(level, string, params) {
     if (this.level > level)
       return;
 
     // Hold off on creating the message object until we actually have
     // an appender that's responsible.
     let message;
     let appenders = this.appenders;
     for (let appender of appenders) {
       if (appender.level > level) {
         continue;
       }
       if (!message) {
+        [string, params] = this._unpackTemplateLiteral(string, params);
         message = new LogMessage(this._name, level, string, params);
       }
       appender.append(message);
     }
   },
 
-  fatal(string, params) {
+  fatal(string, ...params) {
     this.log(Log.Level.Fatal, string, params);
   },
-  error(string, params) {
+  error(string, ...params) {
     this.log(Log.Level.Error, string, params);
   },
-  warn(string, params) {
+  warn(string, ...params) {
     this.log(Log.Level.Warn, string, params);
   },
-  info(string, params) {
+  info(string, ...params) {
     this.log(Log.Level.Info, string, params);
   },
-  config(string, params) {
+  config(string, ...params) {
     this.log(Log.Level.Config, string, params);
   },
-  debug(string, params) {
+  debug(string, ...params) {
     this.log(Log.Level.Debug, string, params);
   },
-  trace(string, params) {
+  trace(string, ...params) {
     this.log(Log.Level.Trace, string, params);
   }
 };
 
 /*
  * LoggerRepository
  * Implements a hierarchy of Loggers
  */
@@ -542,17 +574,26 @@ LoggerRepository.prototype = {
    *        (string) The Logger to retrieve.
    * @param prefix
    *        (string) The string to prefix each logged message with.
    */
   getLoggerWithMessagePrefix(name, prefix) {
     let log = this.getLogger(name);
 
     let proxy = Object.create(log);
-    proxy.log = (level, string, params) => log.log(level, prefix + string, params);
+    proxy.log = (level, string, params) => {
+      if (Array.isArray(string) && Array.isArray(params)) {
+        // Template literal.
+        // We cannot change the original array, so create a new one.
+        string = [prefix + string[0]].concat(string.slice(1));
+      } else {
+        string = prefix + string; // Regular string.
+      }
+      return log.log(level, string, params);
+    };
     return proxy;
   },
 };
 
 /*
  * Formatters
  * These massage a LogMessage into whatever output is desired.
  * BasicFormatter and StructuredFormatter are implemented here.
@@ -591,17 +632,17 @@ BasicFormatter.prototype = {
     // We could add a special case for NSRESULT values here...
     let pIsObject = (typeof(params) == "object" || typeof(params) == "function");
 
     // if we have params, try and find substitutions.
     if (this.parameterFormatter) {
       // have we successfully substituted any parameters into the message?
       // in the log message
       let subDone = false;
-      let regex = /\$\{(\S*)\}/g;
+      let regex = /\$\{(\S*?)\}/g;
       let textParts = [];
       if (message.message) {
         textParts.push(message.message.replace(regex, (_, sub) => {
           // ${foo} means use the params['foo']
           if (sub) {
             if (pIsObject && sub in message.params) {
               subDone = true;
               return this.parameterFormatter.format(message.params[sub]);
@@ -672,16 +713,31 @@ StructuredFormatter.prototype = {
       output._message = logMessage.message;
     }
 
     return JSON.stringify(output);
   }
 };
 
 /**
+ * A formatter that does not prepend time/name/level information to messages,
+ * because those fields are logged separately when using the Android logger.
+ */
+function AndroidFormatter() {
+  BasicFormatter.call(this);
+}
+AndroidFormatter.prototype = Object.freeze({
+  __proto__: BasicFormatter.prototype,
+
+  format(message) {
+    return this.formatText(message);
+  },
+});
+
+/**
  * Test an object to see if it is a Mozilla JS Error.
  */
 function isError(aObj) {
   return (aObj && typeof(aObj) == "object" && "name" in aObj && "message" in aObj &&
           "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj);
 }
 
 /*
@@ -1002,8 +1058,43 @@ BoundedFileAppender.prototype = {
 
     return fileClosePromise.then(_ => {
       this._size = 0;
       this._file = null;
       return OS.File.remove(this._path);
     });
   }
 };
+
+/*
+ * AndroidAppender
+ * Logs to Android logcat using AndroidLog.jsm
+ */
+function AndroidAppender(aFormatter) {
+  Appender.call(this, aFormatter || new AndroidFormatter());
+  this._name = "AndroidAppender";
+}
+AndroidAppender.prototype = {
+  __proto__: Appender.prototype,
+
+  // Map log level to AndroidLog.foo method.
+  _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);
+    AndroidLog[this._mapping[aMessage.level]](tag, msg);
+  },
+};
--- a/toolkit/modules/tests/xpcshell/test_Log.js
+++ b/toolkit/modules/tests/xpcshell/test_Log.js
@@ -69,21 +69,23 @@ add_test(function test_LoggerWithMessage
   let appender = new MockAppender(new Log.MessageOnlyFormatter());
   log.addAppender(appender);
 
   let prefixed = Log.repository.getLoggerWithMessagePrefix(
     "test.logger.prefix", "prefix: ");
 
   log.warn("no prefix");
   prefixed.warn("with prefix");
+  prefixed.warn `with prefix`;
 
-  Assert.equal(appender.messages.length, 2, "2 messages were logged.");
+  Assert.equal(appender.messages.length, 3, "3 messages were logged.");
   Assert.deepEqual(appender.messages, [
     "no prefix",
     "prefix: with prefix",
+    "prefix: with prefix",
   ], "Prefix logger works.");
 
   run_next_test();
 });
 
 /*
  * A utility method for checking object equivalence.
  * Fields with a reqular expression value in expected will be tested
@@ -410,16 +412,20 @@ add_task(async function log_message_with
   Assert.equal(formatMessage("Null ${n} undefined ${u}", {n: null, u: undefined}),
                "Null null undefined undefined");
 
   // Format params with number, bool, and String type.
   Assert.equal(formatMessage("number ${n} boolean ${b} boxed Boolean ${bx} String ${s}",
                              {n: 45, b: false, bx: Boolean(true), s: String("whatevs")}),
                "number 45 boolean false boxed Boolean true String whatevs");
 
+  // Format params with consecutive tokens.
+  Assert.equal(formatMessage("${a}${b}${c}", {a: "foo", b: "bar", c: "baz"}),
+               "foobarbaz");
+
   /*
    * Check that errors get special formatting if they're formatted directly as
    * a named param or they're the only param, but not if they're a field in a
    * larger structure.
    */
   let err = Components.Exception("test exception", Cr.NS_ERROR_FAILURE);
   let str = formatMessage("Exception is ${}", err);
   Assert.ok(str.includes('Exception is [Exception... "test exception"'));
@@ -550,16 +556,37 @@ add_task(async function log_message_with
   Assert.equal(appender.messages.length, 7);
   for (let msg of appender.messages) {
     Assert.ok(msg.params === testParams);
     Assert.ok(msg.message.startsWith("Test "));
   }
 });
 
 /*
+ * Test that all the basic logger methods support tagged template literal format.
+ */
+add_task(async function log_template_literal_message() {
+  let log = Log.repository.getLogger("error.logger");
+  let appender = new MockAppender(new Log.BasicFormatter());
+  log.addAppender(appender);
+
+  log.fatal `Test ${"foo"} ${42}`;
+  log.error `Test ${"foo"} 42`;
+  log.warn `Test foo 42`;
+  log.info `Test ${"foo " + 42}`;
+  log.config `${"Test"} foo ${42}`;
+  log.debug `Test ${"f"}${"o"}${"o"} 42`;
+  log.trace `${"Test foo 42"}`;
+  Assert.equal(appender.messages.length, 7);
+  for (let msg of appender.messages) {
+    Assert.equal(msg.split("\t")[3], "Test foo 42");
+  }
+});
+
+/*
  * Check that we format JS Errors reasonably.
  * This needs to stay a generator to exercise Task.jsm's stack rewriting.
  */
 add_task(function* format_errors() {
   let pFormat = new Log.ParameterFormatter();
 
   // Test that subclasses of Error are recognized as errors.
   let err = new ReferenceError("Ref Error", "ERROR_FILE", 28);