Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 14 Apr 2016 11:43:48 +0200
changeset 293282 3c3401eaf3a24301d9f29234f72b859d28cfdfe1
parent 293281 cb6b876450fb64170ba9d4b287351401c0b06c4a (current diff)
parent 293178 91115264629dfaacf2d60d52a3eff89c18c5af0d (diff)
child 293283 bc425da2ddae413c797cc0498a90c889a775cdf6
push id18749
push usercbook@mozilla.com
push dateFri, 15 Apr 2016 12:01:19 +0000
treeherderfx-team@8f7045b63b07 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone48.0a1
Merge mozilla-central to mozilla-inbound
browser/base/content/browser.js
toolkit/components/passwordmgr/test/test_basic_form_2pw_2.html
toolkit/components/passwordmgr/test/test_basic_form_autocomplete.html
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6419,21 +6419,29 @@ var gIdentityHandler = {
    */
   _state: 0,
 
   get _isBroken() {
     return this._state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
   },
 
   get _isSecure() {
-    return this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
+    // If a <browser> is included within a chrome document, then this._state
+    // will refer to the security state for the <browser> and not the top level
+    // document. In this case, don't upgrade the security state in the UI
+    // with the secure state of the embedded <browser>.
+    return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IS_SECURE;
   },
 
   get _isEV() {
-    return this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
+    // If a <browser> is included within a chrome document, then this._state
+    // will refer to the security state for the <browser> and not the top level
+    // document. In this case, don't upgrade the security state in the UI
+    // with the EV state of the embedded <browser>.
+    return !this._isURILoadedFromFile && this._state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
   },
 
   get _isMixedActiveContentLoaded() {
     return this._state & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT;
   },
 
   get _isMixedActiveContentBlocked() {
     return this._state & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT;
@@ -6615,31 +6623,20 @@ var gIdentityHandler = {
    *        Bitmask provided by nsIWebProgressListener.onSecurityChange.
    * @param uri
    *        nsIURI for which the identity UI should be displayed, already
    *        processed by nsIURIFixup.createExposableURI.
    */
   updateIdentity(state, uri) {
     let shouldHidePopup = this._uri && (this._uri.spec != uri.spec);
     this._state = state;
-    this._uri = uri;
 
     // Firstly, populate the state properties required to display the UI. See
     // the documentation of the individual properties for details.
-
-    try {
-      this._uri.host;
-      this._uriHasHost = true;
-    } catch (ex) {
-      this._uriHasHost = false;
-    }
-
-    let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
-    this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
-
+    this.setURI(uri);
     this._sslStatus = gBrowser.securityUI
                               .QueryInterface(Ci.nsISSLStatusProvider)
                               .SSLStatus;
     if (this._sslStatus) {
       this._sslStatus.QueryInterface(Ci.nsISSLStatus);
     }
 
     // Then, update the user interface with the available data.
@@ -6969,35 +6966,46 @@ var gIdentityHandler = {
     this._identityPopupContentOwner.textContent = owner;
     this._identityPopupContentSupp.textContent = supplemental;
     this._identityPopupContentVerif.textContent = verifier;
 
     // Update per-site permissions section.
     this.updateSitePermissions();
   },
 
-  get _isURILoadedFromFile() {
+  setURI(uri) {
+    this._uri = uri;
+
+    try {
+      this._uri.host;
+      this._uriHasHost = true;
+    } catch (ex) {
+      this._uriHasHost = false;
+    }
+
+    let whitelist = /^(?:accounts|addons|cache|config|crashes|customizing|downloads|healthreport|home|license|newaddon|permissions|preferences|privatebrowsing|rights|sessionrestore|support|welcomeback)(?:[?#]|$)/i;
+    this._isSecureInternalUI = uri.schemeIs("about") && whitelist.test(uri.path);
+
     // Create a channel for the sole purpose of getting the resolved URI
     // of the request to determine if it's loaded from the file system.
+    this._isURILoadedFromFile = false;
     let chanOptions = {uri: this._uri, loadUsingSystemPrincipal: true};
     let resolvedURI;
     try {
       resolvedURI = NetUtil.newChannel(chanOptions).URI;
       if (resolvedURI.schemeIs("jar")) {
         // Given a URI "jar:<jar-file-uri>!/<jar-entry>"
         // create a new URI using <jar-file-uri>!/<jar-entry>
         resolvedURI = NetUtil.newURI(resolvedURI.path);
       }
+      // Check the URI again after resolving.
+      this._isURILoadedFromFile = resolvedURI.schemeIs("file");
     } catch (ex) {
       // NetUtil's methods will throw for malformed URIs and the like
-      return false;
-    }
-
-    // Check the URI again after resolving.
-    return resolvedURI.schemeIs("file");
+    }
   },
 
   /**
    * Click handler for the identity-box element in primary chrome.
    */
   handleIdentityButtonEvent : function(event) {
     event.stopPropagation();
 
--- a/browser/extensions/e10srollout/bootstrap.js
+++ b/browser/extensions/e10srollout/bootstrap.js
@@ -51,18 +51,17 @@ function defineCohort() {
   let updateChannel = UpdateUtils.getUpdateChannel(false);
   if (!(updateChannel in TEST_THRESHOLD)) {
     setCohort("unsupportedChannel");
     return;
   }
 
   let userOptedOut = optedOut();
   let userOptedIn = optedIn();
-  let disqualified = (Services.appinfo.multiprocessBlockPolicy != 0) ||
-                     isThereAnActiveExperiment();
+  let disqualified = (Services.appinfo.multiprocessBlockPolicy != 0);
   let testGroup = (getUserSample() < TEST_THRESHOLD[updateChannel]);
 
   if (userOptedOut) {
     setCohort("optedOut");
   } else if (userOptedIn) {
     setCohort("optedIn");
   } else if (disqualified) {
     setCohort("disqualified");
@@ -109,13 +108,8 @@ function optedIn() {
 function optedOut() {
   // Users can also opt-out by toggling back the pref to false.
   // If they reset the pref instead they might be re-enabled if
   // they are still part of the threshold.
   return Preferences.get(PREF_E10S_FORCE_DISABLED, false) ||
          (Preferences.isSet(PREF_TOGGLE_E10S) &&
           Preferences.get(PREF_TOGGLE_E10S) == false);
 }
-
-function isThereAnActiveExperiment() {
-  let { Experiments } = Cu.import("resource:///modules/experiments/Experiments.jsm", {});
-  return (Experiments.instance().getActiveExperimentID() !== null);
-}
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -399,16 +399,17 @@
 @RESPATH@/components/nsHelperAppDlg.manifest
 @RESPATH@/components/nsHelperAppDlg.js
 @RESPATH@/components/NetworkGeolocationProvider.manifest
 @RESPATH@/components/NetworkGeolocationProvider.js
 @RESPATH@/components/extensions.manifest
 @RESPATH@/components/addonManager.js
 @RESPATH@/components/amContentHandler.js
 @RESPATH@/components/amInstallTrigger.js
+@RESPATH@/components/amWebAPI.js
 @RESPATH@/components/amWebInstallListener.js
 @RESPATH@/components/nsBlocklistService.js
 @RESPATH@/components/nsBlocklistServiceContent.js
 #ifdef MOZ_UPDATER
 @RESPATH@/components/nsUpdateService.manifest
 @RESPATH@/components/nsUpdateService.js
 @RESPATH@/components/nsUpdateServiceStub.js
 #endif
--- a/devtools/.eslintrc
+++ b/devtools/.eslintrc
@@ -34,25 +34,26 @@
     "mozilla/mark-test-function-used": 1,
     "mozilla/no-aArgs": 1,
     "mozilla/no-cpows-in-tests": 1,
     // See bug 1224289.
     "mozilla/reject-importGlobalProperties": 1,
     "mozilla/var-only-at-top-level": 1,
 
     // Rules from the React plugin
-    "react/display-name": 1,
-    "react/no-danger": 1,
-    "react/no-did-mount-set-state": 1,
-    "react/no-did-update-set-state": 1,
-    "react/no-direct-mutation-state": 1,
-    "react/no-unknown-property": 1,
+    "react/display-name": 2,
+    "react/no-danger": 2,
+    "react/no-did-mount-set-state": 2,
+    "react/no-did-update-set-state": 2,
+    "react/no-direct-mutation-state": 2,
+    "react/no-unknown-property": 2,
     "react/prefer-es6-class": [1, "never"],
-    "react/prop-types": 1,
-    "react/sort-comp": [1, {
+    // Disabled temporarily until errors are fixed.
+    "react/prop-types": 0,
+    "react/sort-comp": [2, {
       order: [
         "propTypes",
         "everything-else",
         "render"
       ]
     }],
 
     // Disallow using variables outside the blocks they are defined (especially
--- a/devtools/client/aboutdebugging/components/aboutdebugging.js
+++ b/devtools/client/aboutdebugging/components/aboutdebugging.js
@@ -51,40 +51,40 @@ module.exports = createClass({
   },
 
   componentWillUnmount() {
     window.removeEventListener("hashchange", this.onHashChange);
     this.props.telemetry.toolClosed("aboutdebugging");
     this.props.telemetry.destroy();
   },
 
-  render() {
-    let { client } = this.props;
-    let { selectedTabId } = this.state;
-    let selectTab = this.selectTab;
-
-    let selectedTab = tabs.find(t => t.id == selectedTabId);
-
-    return dom.div({ className: "app" },
-      TabMenu({ tabs, selectedTabId, selectTab }),
-      dom.div({ className: "main-content" },
-        selectedTab.component({ client })
-      )
-    );
-  },
-
   onHashChange() {
     let tabId = window.location.hash.substr(1);
 
     let isValid = tabs.some(t => t.id == tabId);
     if (isValid) {
       this.setState({ selectedTabId: tabId });
     } else {
       // If the current hash matches no valid category, navigate to the default
       // tab.
       this.selectTab(defaultTabId);
     }
   },
 
   selectTab(tabId) {
     window.location.hash = "#" + tabId;
+  },
+
+  render() {
+    let { client } = this.props;
+    let { selectedTabId } = this.state;
+    let selectTab = this.selectTab;
+
+    let selectedTab = tabs.find(t => t.id == selectedTabId);
+
+    return dom.div({ className: "app" },
+      TabMenu({ tabs, selectedTabId, selectTab }),
+      dom.div({ className: "main-content" },
+        selectedTab.component({ client })
+      )
+    );
   }
 });
--- a/devtools/client/aboutdebugging/components/addon-target.js
+++ b/devtools/client/aboutdebugging/components/addon-target.js
@@ -15,16 +15,21 @@ const { createClass, DOM: dom } =
 const Services = require("Services");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "AddonTarget",
 
+  debug() {
+    let { target } = this.props;
+    BrowserToolboxProcess.init({ addonID: target.addonID });
+  },
+
   render() {
     let { target, debugDisabled } = this.props;
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
@@ -33,15 +38,10 @@ module.exports = createClass({
         dom.div({ className: "target-name" }, target.name)
       ),
       dom.button({
         className: "debug-button",
         onClick: this.debug,
         disabled: debugDisabled,
       }, Strings.GetStringFromName("debug"))
     );
-  },
-
-  debug() {
-    let { target } = this.props;
-    BrowserToolboxProcess.init({ addonID: target.addonID });
-  },
+  }
 });
--- a/devtools/client/aboutdebugging/components/addons-controls.js
+++ b/devtools/client/aboutdebugging/components/addons-controls.js
@@ -27,16 +27,45 @@ module.exports = createClass({
   displayName: "AddonsControls",
 
   getInitialState() {
     return {
       installError: null,
     };
   },
 
+  onEnableAddonDebuggingChange(event) {
+    let enabled = event.target.checked;
+    Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
+    Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
+  },
+
+  loadAddonFromFile() {
+    this.setState({ installError: null });
+    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window,
+      Strings.GetStringFromName("selectAddonFromFile2"),
+      Ci.nsIFilePicker.modeOpen);
+    let res = fp.show();
+    if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
+      return;
+    }
+    let file = fp.file;
+    // AddonManager.installTemporaryAddon accepts either
+    // addon directory or final xpi file.
+    if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
+      file = file.parent;
+    }
+
+    AddonManager.installTemporaryAddon(file)
+      .catch(e => {
+        this.setState({ installError: e.message });
+      });
+  },
+
   render() {
     let { debugDisabled } = this.props;
 
     return dom.div({ className: "addons-top" },
       dom.div({ className: "addons-controls" },
         dom.div({ className: "addons-options" },
           dom.input({
             id: "enable-addon-debugging",
@@ -55,39 +84,10 @@ module.exports = createClass({
           ")"
         ),
         dom.button({
           id: "load-addon-from-file",
           onClick: this.loadAddonFromFile,
         }, Strings.GetStringFromName("loadTemporaryAddon"))
       ),
       AddonsInstallError({ error: this.state.installError }));
-  },
-
-  onEnableAddonDebuggingChange(event) {
-    let enabled = event.target.checked;
-    Services.prefs.setBoolPref("devtools.chrome.enabled", enabled);
-    Services.prefs.setBoolPref("devtools.debugger.remote-enabled", enabled);
-  },
-
-  loadAddonFromFile() {
-    this.setState({ installError: null });
-    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-    fp.init(window,
-      Strings.GetStringFromName("selectAddonFromFile2"),
-      Ci.nsIFilePicker.modeOpen);
-    let res = fp.show();
-    if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
-      return;
-    }
-    let file = fp.file;
-    // AddonManager.installTemporaryAddon accepts either
-    // addon directory or final xpi file.
-    if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
-      file = file.parent;
-    }
-
-    AddonManager.installTemporaryAddon(file)
-      .catch(e => {
-        this.setState({ installError: e.message });
-      });
-  },
+  }
 });
--- a/devtools/client/aboutdebugging/components/addons-tab.js
+++ b/devtools/client/aboutdebugging/components/addons-tab.js
@@ -46,38 +46,16 @@ module.exports = createClass({
   componentWillUnmount() {
     AddonManager.removeAddonListener(this);
     Services.prefs.removeObserver(CHROME_ENABLED_PREF,
       this.updateDebugStatus);
     Services.prefs.removeObserver(REMOTE_ENABLED_PREF,
       this.updateDebugStatus);
   },
 
-  render() {
-    let { client } = this.props;
-    let { debugDisabled, extensions: targets } = this.state;
-    let name = Strings.GetStringFromName("extensions");
-    let targetClass = AddonTarget;
-
-    return dom.div({
-      id: "tab-addons",
-      className: "tab",
-      role: "tabpanel",
-      "aria-labelledby": "tab-addons-header-name"
-    },
-    TabHeader({
-      id: "tab-addons-header-name",
-      name: Strings.GetStringFromName("addons")
-    }),
-    AddonsControls({ debugDisabled }),
-    dom.div({ id: "addons" },
-      TargetList({ name, targets, client, debugDisabled, targetClass })
-    ));
-  },
-
   updateDebugStatus() {
     let debugDisabled =
       !Services.prefs.getBoolPref(CHROME_ENABLED_PREF) ||
       !Services.prefs.getBoolPref(REMOTE_ENABLED_PREF);
 
     this.setState({ debugDisabled });
   },
 
@@ -117,9 +95,31 @@ module.exports = createClass({
   },
 
   /**
    * Mandatory callback as AddonManager listener.
    */
   onDisabled() {
     this.updateAddonsList();
   },
+
+  render() {
+    let { client } = this.props;
+    let { debugDisabled, extensions: targets } = this.state;
+    let name = Strings.GetStringFromName("extensions");
+    let targetClass = AddonTarget;
+
+    return dom.div({
+      id: "tab-addons",
+      className: "tab",
+      role: "tabpanel",
+      "aria-labelledby": "tab-addons-header-name"
+    },
+    TabHeader({
+      id: "tab-addons-header-name",
+      name: Strings.GetStringFromName("addons")
+    }),
+    AddonsControls({ debugDisabled }),
+    dom.div({ id: "addons" },
+      TargetList({ name, targets, client, debugDisabled, targetClass })
+    ));
+  }
 });
--- a/devtools/client/aboutdebugging/components/service-worker-target.js
+++ b/devtools/client/aboutdebugging/components/service-worker-target.js
@@ -12,59 +12,16 @@ const { debugWorker } = require("../modu
 const Services = require("Services");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "ServiceWorkerTarget",
 
-  render() {
-    let { target, debugDisabled } = this.props;
-    let isRunning = this.isRunning();
-
-    return dom.div({ className: "target-container" },
-      dom.img({
-        className: "target-icon",
-        role: "presentation",
-        src: target.icon
-      }),
-      dom.div({ className: "target" },
-        dom.div({ className: "target-name" }, target.name),
-        dom.ul({ className: "target-details" },
-          dom.li({ className: "target-detail" },
-            dom.strong(null, Strings.GetStringFromName("scope")),
-            dom.span({ className: "service-worker-scope" }, target.scope),
-            dom.a({
-              onClick: this.unregister,
-              className: "unregister-link"
-            }, Strings.GetStringFromName("unregister"))
-          )
-        )
-      ),
-      (isRunning ?
-        [
-          dom.button({
-            className: "push-button",
-            onClick: this.push
-          }, Strings.GetStringFromName("push")),
-          dom.button({
-            className: "debug-button",
-            onClick: this.debug,
-            disabled: debugDisabled
-          }, Strings.GetStringFromName("debug"))
-        ] :
-        dom.button({
-          className: "start-button",
-          onClick: this.start
-        }, Strings.GetStringFromName("start"))
-      )
-    );
-  },
-
   debug() {
     if (!this.isRunning()) {
       // If the worker is not running, we can't debug it.
       return;
     }
 
     let { client, target } = this.props;
     debugWorker(client, target.workerActor);
@@ -103,9 +60,52 @@ module.exports = createClass({
       type: "unregister"
     });
   },
 
   isRunning() {
     // We know the target is running if it has a worker actor.
     return !!this.props.target.workerActor;
   },
+
+  render() {
+    let { target, debugDisabled } = this.props;
+    let isRunning = this.isRunning();
+
+    return dom.div({ className: "target-container" },
+      dom.img({
+        className: "target-icon",
+        role: "presentation",
+        src: target.icon
+      }),
+      dom.div({ className: "target" },
+        dom.div({ className: "target-name" }, target.name),
+        dom.ul({ className: "target-details" },
+          dom.li({ className: "target-detail" },
+            dom.strong(null, Strings.GetStringFromName("scope")),
+            dom.span({ className: "service-worker-scope" }, target.scope),
+            dom.a({
+              onClick: this.unregister,
+              className: "unregister-link"
+            }, Strings.GetStringFromName("unregister"))
+          )
+        )
+      ),
+      (isRunning ?
+        [
+          dom.button({
+            className: "push-button",
+            onClick: this.push
+          }, Strings.GetStringFromName("push")),
+          dom.button({
+            className: "debug-button",
+            onClick: this.debug,
+            disabled: debugDisabled
+          }, Strings.GetStringFromName("debug"))
+        ] :
+        dom.button({
+          className: "start-button",
+          onClick: this.start
+        }, Strings.GetStringFromName("start"))
+      )
+    );
+  }
 });
--- a/devtools/client/aboutdebugging/components/tab-menu-entry.js
+++ b/devtools/client/aboutdebugging/components/tab-menu-entry.js
@@ -5,27 +5,27 @@
 "use strict";
 
 const { createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 
 module.exports = createClass({
   displayName: "TabMenuEntry",
 
+  onClick() {
+    this.props.selectTab(this.props.tabId);
+  },
+
   render() {
     let { icon, name, selected } = this.props;
 
     // Here .category, .category-icon, .category-name classnames are used to
     // apply common styles defined.
     let className = "category" + (selected ? " selected" : "");
     return dom.div({
       "aria-selected": selected,
       className,
       onClick: this.onClick,
       role: "tab" },
     dom.img({ className: "category-icon", src: icon, role: "presentation" }),
     dom.div({ className: "category-name" }, name));
-  },
-
-  onClick() {
-    this.props.selectTab(this.props.tabId);
   }
 });
--- a/devtools/client/aboutdebugging/components/worker-target.js
+++ b/devtools/client/aboutdebugging/components/worker-target.js
@@ -12,16 +12,21 @@ const { debugWorker } = require("../modu
 const Services = require("Services");
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
 
 module.exports = createClass({
   displayName: "WorkerTarget",
 
+  debug() {
+    let { client, target } = this.props;
+    debugWorker(client, target.workerActor);
+  },
+
   render() {
     let { target, debugDisabled } = this.props;
 
     return dom.div({ className: "target-container" },
       dom.img({
         className: "target-icon",
         role: "presentation",
         src: target.icon
@@ -30,15 +35,10 @@ module.exports = createClass({
         dom.div({ className: "target-name" }, target.name)
       ),
       dom.button({
         className: "debug-button",
         onClick: this.debug,
         disabled: debugDisabled
       }, Strings.GetStringFromName("debug"))
     );
-  },
-
-  debug() {
-    let { client, target } = this.props;
-    debugWorker(client, target.workerActor);
   }
 });
--- a/devtools/client/aboutdebugging/components/workers-tab.js
+++ b/devtools/client/aboutdebugging/components/workers-tab.js
@@ -43,55 +43,16 @@ module.exports = createClass({
 
   componentWillUnmount() {
     let client = this.props.client;
     client.removeListener("processListChanged", this.update);
     client.removeListener("serviceWorkerRegistrationListChanged", this.update);
     client.removeListener("workerListChanged", this.update);
   },
 
-  render() {
-    let { client } = this.props;
-    let { workers } = this.state;
-
-    return dom.div({
-      id: "tab-workers",
-      className: "tab",
-      role: "tabpanel",
-      "aria-labelledby": "tab-workers-header-name"
-    },
-    TabHeader({
-      id: "tab-workers-header-name",
-      name: Strings.GetStringFromName("workers")
-    }),
-    dom.div({ id: "workers", className: "inverted-icons" },
-      TargetList({
-        client,
-        id: "service-workers",
-        name: Strings.GetStringFromName("serviceWorkers"),
-        targetClass: ServiceWorkerTarget,
-        targets: workers.service
-      }),
-      TargetList({
-        client,
-        id: "shared-workers",
-        name: Strings.GetStringFromName("sharedWorkers"),
-        targetClass: WorkerTarget,
-        targets: workers.shared
-      }),
-      TargetList({
-        client,
-        id: "other-workers",
-        name: Strings.GetStringFromName("otherWorkers"),
-        targetClass: WorkerTarget,
-        targets: workers.other
-      })
-    ));
-  },
-
   update() {
     let workers = this.getInitialState().workers;
 
     getWorkerForms(this.props.client).then(forms => {
       forms.registrations.forEach(form => {
         workers.service.push({
           icon: WorkerIcon,
           name: form.url,
@@ -131,10 +92,49 @@ module.exports = createClass({
       });
 
       // XXX: Filter out the service worker registrations for which we couldn't
       // find the scriptSpec.
       workers.service = workers.service.filter(reg => !!reg.url);
 
       this.setState({ workers });
     });
+  },
+
+  render() {
+    let { client } = this.props;
+    let { workers } = this.state;
+
+    return dom.div({
+      id: "tab-workers",
+      className: "tab",
+      role: "tabpanel",
+      "aria-labelledby": "tab-workers-header-name"
+    },
+    TabHeader({
+      id: "tab-workers-header-name",
+      name: Strings.GetStringFromName("workers")
+    }),
+    dom.div({ id: "workers", className: "inverted-icons" },
+      TargetList({
+        client,
+        id: "service-workers",
+        name: Strings.GetStringFromName("serviceWorkers"),
+        targetClass: ServiceWorkerTarget,
+        targets: workers.service
+      }),
+      TargetList({
+        client,
+        id: "shared-workers",
+        name: Strings.GetStringFromName("sharedWorkers"),
+        targetClass: WorkerTarget,
+        targets: workers.shared
+      }),
+      TargetList({
+        client,
+        id: "other-workers",
+        name: Strings.GetStringFromName("otherWorkers"),
+        targetClass: WorkerTarget,
+        targets: workers.other
+      })
+    ));
   }
 });
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -102,16 +102,21 @@ stacktrace.asyncStack=(Async: %S)
 # of the console.time() call. Parameters: %S is the name of the timer.
 timerStarted=%S: timer started
 
 # LOCALIZATION NOTE (timeEnd): this string is used to display the result of
 # the console.timeEnd() call. Parameters: %1$S is the name of the timer, %2$S
 # is the number of milliseconds.
 timeEnd=%1$S: %2$Sms
 
+# LOCALIZATION NOTE (consoleCleared): this string is displayed when receiving a
+# call to console.clear() to let the user know the previous messages of the
+# console have been removed programmatically.
+consoleCleared=Console was cleared.
+
 # LOCALIZATION NOTE (noCounterLabel): this string is used to display
 # count-messages with no label provided.
 noCounterLabel=<no label>
 
 # LOCALIZATION NOTE (Autocomplete.blank): this string is used when inputnode
 # string containing anchor doesn't matches to any property in the content.
 Autocomplete.blank=  <- no result
 
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -16,26 +16,25 @@ const {
   rotateViewport
 } = require("./actions/viewports");
 const { takeScreenshot } = require("./actions/screenshot");
 const Types = require("./types");
 const Viewports = createFactory(require("./components/viewports"));
 const GlobalToolbar = createFactory(require("./components/global-toolbar"));
 
 let App = createClass({
-
-  displayName: "App",
-
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
+  displayName: "App",
+
   onBrowserMounted() {
     window.postMessage({ type: "browser-mounted" }, "*");
   },
 
   onChangeViewportDevice(id, device) {
     this.props.dispatch(changeDevice(id, device));
   },
 
--- a/devtools/client/responsive.html/components/browser.js
+++ b/devtools/client/responsive.html/components/browser.js
@@ -11,33 +11,32 @@ const DevToolsUtils = require("devtools/
 const { getToplevelWindow } = require("sdk/window/utils");
 const { DOM: dom, createClass, addons, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 const { waitForMessage } = require("../utils/e10s");
 
 module.exports = createClass({
-
-  displayName: "Browser",
-
-  mixins: [ addons.PureRenderMixin ],
-
   /**
    * This component is not allowed to depend directly on frequently changing
    * data (width, height) due to the use of `dangerouslySetInnerHTML` below.
    * Any changes in props will cause the <iframe> to be removed and added again,
    * throwing away the current state of the page.
    */
   propTypes: {
     location: Types.location.isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
   },
 
+  displayName: "Browser",
+
+  mixins: [ addons.PureRenderMixin ],
+
   /**
    * Once the browser element has mounted, load the frame script and enable
    * various features, like floating scrollbars.
    */
   componentDidMount: Task.async(function* () {
     let { onContentResize } = this;
     let browser = this.refs.browserContainer.querySelector("iframe.browser");
     let mm = browser.frameLoader.messageManager;
--- a/devtools/client/responsive.html/components/device-selector.js
+++ b/devtools/client/responsive.html/components/device-selector.js
@@ -6,26 +6,25 @@
 
 const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 
 module.exports = createClass({
-
-  displayName: "DeviceSelector",
-
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     selectedDevice: PropTypes.string.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
   },
 
+  displayName: "DeviceSelector",
+
   mixins: [ addons.PureRenderMixin ],
 
   onSelectChange({ target }) {
     let {
       devices,
       onChangeViewportDevice,
       onResizeViewport,
     } = this.props;
--- a/devtools/client/responsive.html/components/global-toolbar.js
+++ b/devtools/client/responsive.html/components/global-toolbar.js
@@ -5,25 +5,24 @@
 "use strict";
 
 const { getStr } = require("../utils/l10n");
 const { DOM: dom, createClass, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 const Types = require("../types");
 
 module.exports = createClass({
-
-  displayName: "GlobalToolbar",
-
   propTypes: {
     onExit: PropTypes.func.isRequired,
     onScreenshot: PropTypes.func.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
   },
 
+  displayName: "GlobalToolbar",
+
   mixins: [ addons.PureRenderMixin ],
 
   render() {
     let {
       onExit,
       onScreenshot,
       screenshot,
     } = this.props;
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -13,31 +13,30 @@ const Constants = require("../constants"
 const Types = require("../types");
 const Browser = createFactory(require("./browser"));
 const ViewportToolbar = createFactory(require("./viewport-toolbar"));
 
 const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
 const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
 
 module.exports = createClass({
-
-  displayName: "ResizableViewport",
-
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
+  displayName: "ResizableViewport",
+
   getInitialState() {
     return {
       isResizing: false,
       lastClientX: 0,
       lastClientY: 0,
       ignoreX: false,
       ignoreY: false,
     };
--- a/devtools/client/responsive.html/components/viewport-dimension.js
+++ b/devtools/client/responsive.html/components/viewport-dimension.js
@@ -6,25 +6,24 @@
 
 const { DOM: dom, createClass, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Constants = require("../constants");
 const Types = require("../types");
 
 module.exports = createClass({
-
-  displayName: "ViewportDimension",
-
   propTypes: {
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
   },
 
+  displayName: "ViewportDimension",
+
   getInitialState() {
     let { width, height } = this.props.viewport;
 
     return {
       width,
       height,
       isEditing: false,
       isInvalid: false,
--- a/devtools/client/responsive.html/components/viewport-toolbar.js
+++ b/devtools/client/responsive.html/components/viewport-toolbar.js
@@ -6,29 +6,28 @@
 
 const { DOM: dom, createClass, createFactory, PropTypes, addons } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 const DeviceSelector = createFactory(require("./device-selector"));
 
 module.exports = createClass({
-
-  displayName: "ViewportToolbar",
-
-  mixins: [ addons.PureRenderMixin ],
-
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     selectedDevice: PropTypes.string.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
+  displayName: "ViewportToolbar",
+
+  mixins: [ addons.PureRenderMixin ],
+
   render() {
     let {
       devices,
       selectedDevice,
       onChangeViewportDevice,
       onResizeViewport,
       onRotateViewport,
     } = this.props;
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -7,31 +7,30 @@
 const { DOM: dom, createClass, createFactory, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 const ResizableViewport = createFactory(require("./resizable-viewport"));
 const ViewportDimension = createFactory(require("./viewport-dimension"));
 
 module.exports = createClass({
-
-  displayName: "Viewport",
-
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
+  displayName: "Viewport",
+
   onChangeViewportDevice(device) {
     let {
       viewport,
       onChangeViewportDevice,
     } = this.props;
 
     onChangeViewportDevice(viewport.id, device);
   },
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -6,31 +6,30 @@
 
 const { DOM: dom, createClass, createFactory, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 const Viewport = createFactory(require("./viewport"));
 
 module.exports = createClass({
-
-  displayName: "Viewports",
-
   propTypes: {
     devices: PropTypes.shape(Types.devices).isRequired,
     location: Types.location.isRequired,
     screenshot: PropTypes.shape(Types.screenshot).isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
     onBrowserMounted: PropTypes.func.isRequired,
     onChangeViewportDevice: PropTypes.func.isRequired,
     onContentResize: PropTypes.func.isRequired,
     onResizeViewport: PropTypes.func.isRequired,
     onRotateViewport: PropTypes.func.isRequired,
   },
 
+  displayName: "Viewports",
+
   render() {
     let {
       devices,
       location,
       screenshot,
       viewports,
       onBrowserMounted,
       onChangeViewportDevice,
--- a/devtools/client/webconsole/console-output.js
+++ b/devtools/client/webconsole/console-output.js
@@ -88,16 +88,17 @@ const COMPAT = {
 // A map from the console API call levels to the Web Console severities.
 const CONSOLE_API_LEVELS_TO_SEVERITIES = {
   error: "error",
   exception: "error",
   assert: "error",
   warn: "warning",
   info: "info",
   log: "log",
+  clear: "log",
   trace: "log",
   table: "log",
   debug: "log",
   dir: "log",
   dirxml: "log",
   group: "log",
   groupCollapsed: "log",
   groupEnd: "log",
--- a/devtools/client/webconsole/test/browser.ini
+++ b/devtools/client/webconsole/test/browser.ini
@@ -64,16 +64,17 @@ support-files =
   test-bug-782653-css-errors.html
   test-bug-837351-security-errors.html
   test-bug-859170-longstring-hang.html
   test-bug-869003-iframe.html
   test-bug-869003-top-window.html
   test-closure-optimized-out.html
   test-closures.html
   test-console-assert.html
+  test-console-clear.html
   test-console-count.html
   test-console-count-external-file.js
   test-console-extras.html
   test-console-replaced-api.html
   test-console-server-logging.sjs
   test-console-server-logging-array.sjs
   test-console.html
   test-console-workers.html
@@ -152,16 +153,17 @@ skip-if = (e10s && debug) || (e10s && os
 skip-if = (e10s && (os == 'win' || os == 'mac')) # Bug 1243976
 [browser_bug_865288_repeat_different_objects.js]
 [browser_bug_865871_variables_view_close_on_esc_key.js]
 [browser_bug_869003_inspect_cross_domain_object.js]
 [browser_bug_871156_ctrlw_close_tab.js]
 [browser_cached_messages.js]
 [browser_console.js]
 [browser_console_addonsdk_loader_exception.js]
+[browser_console_clear_method.js]
 [browser_console_clear_on_reload.js]
 [browser_console_click_focus.js]
 [browser_console_consolejsm_output.js]
 [browser_console_copy_command.js]
 [browser_console_dead_objects.js]
 skip-if = e10s # Bug 1042253 - webconsole tests disabled with e10s
 [browser_console_copy_entire_message_context_menu.js]
 [browser_console_error_source_click.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/browser_console_clear_method.js
@@ -0,0 +1,131 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that calls to console.clear from a script delete the messages
+// previously logged.
+
+"use strict";
+
+add_task(function* () {
+  const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
+                   "test/test-console-clear.html";
+
+  yield loadTab(TEST_URI);
+  let hud = yield openConsole();
+  ok(hud, "Web Console opened");
+
+  info("Check the console.clear() done on page load has been processed.");
+  yield waitForLog("Console was cleared", hud);
+  ok(hud.outputNode.textContent.includes("Console was cleared"),
+    "console.clear() message is displayed");
+  ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed");
+  ok(!hud.outputNode.textContent.includes("log2"), "log2 not displayed");
+
+  info("Logging two messages log3, log4");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    content.wrappedJSObject.console.log("log3");
+    content.wrappedJSObject.console.log("log4");
+  });
+
+  yield waitForLog("log3", hud);
+  yield waitForLog("log4", hud);
+
+  ok(hud.outputNode.textContent.includes("Console was cleared"),
+    "console.clear() message is still displayed");
+  ok(hud.outputNode.textContent.includes("log3"), "log3 is displayed");
+  ok(hud.outputNode.textContent.includes("log4"), "log4 is displayed");
+
+  info("Open the variables view sidebar for 'objFromPage'");
+  yield openSidebar("objFromPage", { a: 1 }, hud);
+  let sidebarClosed = hud.jsterm.once("sidebar-closed");
+
+  info("Call console.clear from the page");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    content.wrappedJSObject.console.clear();
+  });
+
+  // Cannot wait for "Console was cleared" here because such a message is
+  // already present and would yield immediately.
+  info("Wait for variables view sidebar to be closed after console.clear()");
+  yield sidebarClosed;
+
+  ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed");
+  ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed");
+  ok(hud.outputNode.textContent.includes("Console was cleared"),
+    "console.clear() message is still displayed");
+  is(hud.outputNode.textContent.split("Console was cleared").length, 2,
+    "console.clear() message is only displayed once");
+
+  info("Logging one messages log5");
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function* () {
+    content.wrappedJSObject.console.log("log5");
+  });
+  yield waitForLog("log5", hud);
+
+  info("Close and reopen the webconsole.");
+  yield closeConsole(gBrowser.selectedTab);
+  hud = yield openConsole();
+  yield waitForLog("Console was cleared", hud);
+
+  ok(hud.outputNode.textContent.includes("Console was cleared"),
+    "console.clear() message is still displayed");
+  ok(!hud.outputNode.textContent.includes("log1"), "log1 not displayed");
+  ok(!hud.outputNode.textContent.includes("log2"), "log1 not displayed");
+  ok(!hud.outputNode.textContent.includes("log3"), "log3 not displayed");
+  ok(!hud.outputNode.textContent.includes("log4"), "log4 not displayed");
+  ok(hud.outputNode.textContent.includes("log5"), "log5 still displayed");
+});
+
+/**
+ * Wait for a single message to be logged in the provided webconsole instance
+ * with the category CATEGORY_WEBDEV and the SEVERITY_LOG severity.
+ *
+ * @param {String} message
+ *        The expected messaged.
+ * @param {WebConsole} webconsole
+ *        WebConsole instance in which the message should be logged.
+ */
+function* waitForLog(message, webconsole, options) {
+  yield waitForMessages({
+    webconsole: webconsole,
+    messages: [{
+      text: message,
+      category: CATEGORY_WEBDEV,
+      severity: SEVERITY_LOG,
+    }],
+  });
+}
+
+/**
+ * Open the variables view sidebar for the object with the provided name objName
+ * and wait for the expected object is displayed in the variables view.
+ *
+ * @param {String} objName
+ *        The name of the object to open in the sidebar.
+ * @param {Object} expectedObj
+ *        The properties that should be displayed in the variables view.
+ * @param {WebConsole} webconsole
+ *        WebConsole instance in which the message should be logged.
+ *
+ */
+function* openSidebar(objName, expectedObj, webconsole) {
+  let msg = yield webconsole.jsterm.execute(objName);
+  ok(msg, "output message found");
+
+  let anchor = msg.querySelector("a");
+  let body = msg.querySelector(".message-body");
+  ok(anchor, "object anchor");
+  ok(body, "message body");
+
+  yield EventUtils.synthesizeMouse(anchor, 2, 2, {}, webconsole.iframeWindow);
+
+  let vviewVar = yield webconsole.jsterm.once("variablesview-fetched");
+  let vview = vviewVar._variablesView;
+  ok(vview, "variables view object exists");
+
+  yield findVariableViewProperties(vviewVar, [
+    expectedObj,
+  ], { webconsole: webconsole });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/webconsole/test/test-console-clear.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html dir="ltr" xml:lang="en-US" lang="en-US"><head>
+    <meta charset="utf-8">
+    <title>Console.clear() tests</title>
+    <script type="text/javascript">
+      console.log("log1");
+      console.log("log2");
+      console.clear();
+
+      window.objFromPage = { a: 1 };
+    </script>
+  </head>
+  <body>
+    <h1 id="header">Clear Demo</h1>
+  </body>
+</html>
--- a/devtools/client/webconsole/webconsole.js
+++ b/devtools/client/webconsole/webconsole.js
@@ -137,16 +137,17 @@ const MESSAGE_PREFERENCE_KEYS = [
 // severities.
 const LEVELS = {
   error: SEVERITY_ERROR,
   exception: SEVERITY_ERROR,
   assert: SEVERITY_ERROR,
   warn: SEVERITY_WARNING,
   info: SEVERITY_INFO,
   log: SEVERITY_LOG,
+  clear: SEVERITY_LOG,
   trace: SEVERITY_LOG,
   table: SEVERITY_LOG,
   debug: SEVERITY_LOG,
   dir: SEVERITY_LOG,
   dirxml: SEVERITY_LOG,
   group: SEVERITY_LOG,
   groupCollapsed: SEVERITY_LOG,
   groupEnd: SEVERITY_LOG,
@@ -1280,16 +1281,21 @@ WebConsoleFrame.prototype = {
         node = msg.init(this.output).render().element;
         break;
       }
       case "trace": {
         let msg = new Messages.ConsoleTrace(message);
         node = msg.init(this.output).render().element;
         break;
       }
+      case "clear": {
+        body = l10n.getStr("consoleCleared");
+        clipboardText = body;
+        break;
+      }
       case "dir": {
         body = { arguments: args };
         let clipboardArray = [];
         args.forEach((value) => {
           clipboardArray.push(VariablesView.getString(value));
         });
         clipboardText = clipboardArray.join(" ");
         break;
@@ -2198,16 +2204,24 @@ WebConsoleFrame.prototype = {
     if (!node) {
       return null;
     }
 
     let isFiltered = this.filterMessageNode(node);
 
     let isRepeated = this._filterRepeatedMessage(node);
 
+    // If a clear message is processed while the webconsole is opened, the UI
+    // should be cleared.
+    if (message && message.level == "clear") {
+      // Do not clear the consoleStorage here as it has been cleared already
+      // by the clear method, only clear the UI.
+      this.jsterm.clearOutput(false);
+    }
+
     let visible = !isRepeated && !isFiltered;
     if (!isRepeated) {
       this.outputNode.appendChild(node);
       this._pruneCategoriesQueue[node.category] = true;
 
       let nodeID = node.getAttribute("id");
       Services.obs.notifyObservers(hudIdSupportsString,
                                    "web-console-message-created", nodeID);
--- a/dom/base/Console.cpp
+++ b/dom/base/Console.cpp
@@ -1037,16 +1037,17 @@ Console::WrapObject(JSContext* aCx, JS::
 
 METHOD(Log, "log")
 METHOD(Info, "info")
 METHOD(Warn, "warn")
 METHOD(Error, "error")
 METHOD(Exception, "exception")
 METHOD(Debug, "debug")
 METHOD(Table, "table")
+METHOD(Clear, "clear")
 
 void
 Console::Trace(JSContext* aCx)
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mStatus == eInitialized);
 
   const Sequence<JS::Value> data;
@@ -1532,16 +1533,21 @@ Console::ProcessCallData(JSContext* aCx,
     outerID = aData->mOuterIDString;
     innerID = aData->mInnerIDString;
   } else {
     MOZ_ASSERT(aData->mIDType == ConsoleCallData::eNumber);
     outerID.AppendInt(aData->mOuterIDNumber);
     innerID.AppendInt(aData->mInnerIDNumber);
   }
 
+  if (aData->mMethodName == MethodClear) {
+    nsresult rv = mStorage->ClearEvents(innerID);
+    NS_WARN_IF(NS_FAILED(rv));
+  }
+
   if (NS_FAILED(mStorage->RecordEvent(innerID, outerID, eventValue))) {
     NS_WARNING("Failed to record a console event.");
   }
 }
 
 bool
 Console::PopulateConsoleNotificationInTheTargetScope(JSContext* aCx,
                                                      const Sequence<JS::Value>& aArguments,
--- a/dom/base/Console.h
+++ b/dom/base/Console.h
@@ -111,16 +111,19 @@ public:
 
   void
   Assert(JSContext* aCx, bool aCondition, const Sequence<JS::Value>& aData);
 
   void
   Count(JSContext* aCx, const Sequence<JS::Value>& aData);
 
   void
+  Clear(JSContext* aCx, const Sequence<JS::Value>& aData);
+
+  void
   NoopMethod();
 
   void
   ClearStorage();
 
   void
   RetrieveConsoleEvents(JSContext* aCx, nsTArray<JS::Value>& aEvents,
                         ErrorResult& aRv);
@@ -151,17 +154,18 @@ private:
     MethodDirxml,
     MethodGroup,
     MethodGroupCollapsed,
     MethodGroupEnd,
     MethodTime,
     MethodTimeEnd,
     MethodTimeStamp,
     MethodAssert,
-    MethodCount
+    MethodCount,
+    MethodClear
   };
 
   void
   Method(JSContext* aCx, MethodName aName, const nsAString& aString,
          const Sequence<JS::Value>& aData);
 
   // This method must receive aCx and aArguments in the same JSCompartment.
   void
--- a/dom/base/nsFrameMessageManager.cpp
+++ b/dom/base/nsFrameMessageManager.cpp
@@ -1652,24 +1652,33 @@ nsMessageManagerScriptExecutor::DidCreat
     RefPtr<nsScriptCacheCleaner> scriptCacheCleaner =
       new nsScriptCacheCleaner();
     scriptCacheCleaner.forget(&sScriptCacheCleaner);
   }
 }
 
 // static
 void
-nsMessageManagerScriptExecutor::Shutdown()
+nsMessageManagerScriptExecutor::PurgeCache()
 {
   if (sCachedScripts) {
     NS_ASSERTION(sCachedScripts != nullptr, "Need cached scripts");
     for (auto iter = sCachedScripts->Iter(); !iter.Done(); iter.Next()) {
       delete iter.Data();
       iter.Remove();
     }
+  }
+}
+
+// static
+void
+nsMessageManagerScriptExecutor::Shutdown()
+{
+  if (sCachedScripts) {
+    PurgeCache();
 
     delete sCachedScripts;
     sCachedScripts = nullptr;
 
     RefPtr<nsScriptCacheCleaner> scriptCacheCleaner;
     scriptCacheCleaner.swap(sScriptCacheCleaner);
   }
 }
--- a/dom/base/nsFrameMessageManager.h
+++ b/dom/base/nsFrameMessageManager.h
@@ -372,16 +372,17 @@ struct nsMessageManagerScriptHolder
 
   JS::PersistentRooted<JSScript*> mScript;
   bool mRunInGlobalScope;
 };
 
 class nsMessageManagerScriptExecutor
 {
 public:
+  static void PurgeCache();
   static void Shutdown();
   already_AddRefed<nsIXPConnectJSObjectHolder> GetGlobal()
   {
     nsCOMPtr<nsIXPConnectJSObjectHolder> ref = mGlobal;
     return ref.forget();
   }
 
   void MarkScopesForCC();
@@ -412,22 +413,28 @@ class nsScriptCacheCleaner final : publi
 {
   ~nsScriptCacheCleaner() {}
 
   NS_DECL_ISUPPORTS
 
   nsScriptCacheCleaner()
   {
     nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
-    if (obsSvc)
+    if (obsSvc) {
+      obsSvc->AddObserver(this, "message-manager-flush-caches", false);
       obsSvc->AddObserver(this, "xpcom-shutdown", false);
+    }
   }
 
   NS_IMETHODIMP Observe(nsISupports *aSubject,
                         const char *aTopic,
                         const char16_t *aData) override
   {
-    nsMessageManagerScriptExecutor::Shutdown();
+    if (strcmp("message-manager-flush-caches", aTopic) == 0) {
+      nsMessageManagerScriptExecutor::PurgeCache();
+    } else if (strcmp("xpcom-shutdown", aTopic) == 0) {
+      nsMessageManagerScriptExecutor::Shutdown();
+    }
     return NS_OK;
   }
 };
 
 #endif
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -644,16 +644,17 @@ skip-if = toolkit == 'android' #bug 6870
 [test_bug628938.html]
 [test_bug631615.html]
 [test_bug638112.html]
 [test_bug647518.html]
 [test_bug650001.html]
 [test_bug650776.html]
 [test_bug650784.html]
 [test_bug656283.html]
+[test_bug659625.html]
 [test_bug664916.html]
 [test_bug666604.html]
 skip-if = buildapp == 'b2g' # b2g(dom.disable_open_during_load not implemented in b2g) b2g-debug(dom.disable_open_during_load not implemented in b2g) b2g-desktop(dom.disable_open_during_load not implemented in b2g)
 [test_bug675121.html]
 skip-if = buildapp == 'b2g' # b2g(bug 901378) b2g-debug(bug 901378) b2g-desktop(bug 901378)
 [test_bug675166.html]
 [test_bug682463.html]
 [test_bug682554.html]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_bug659625.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=659625
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 659625</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659625">Mozilla Bug 659625</a>
+<script type="application/javascript">
+  const { Cc, Ci } = SpecialPowers;
+  let consoleStorage = Cc["@mozilla.org/consoleAPI-storage;1"];
+  let storage = consoleStorage.getService(Ci.nsIConsoleAPIStorage);
+
+  let clearAndCheckStorage = () => {
+    console.clear();
+    ok(storage.getEvents().length === 1,
+      "Only one event remains in consoleAPIStorage");
+    ok(storage.getEvents()[0].level === "clear",
+      "Remaining event has level 'clear'");
+  }
+
+  storage.clearEvents();
+  ok(storage.getEvents().length === 0,
+    "Console is empty when test is starting");
+  clearAndCheckStorage();
+
+  console.log("log");
+  console.debug("debug");
+  console.warn("warn");
+  console.error("error");
+  console.exception("exception");
+  ok(storage.getEvents().length === 6,
+    "5 new console events have been registered for logging variants");
+  clearAndCheckStorage();
+
+  console.trace();
+  ok(storage.getEvents().length === 2,
+    "1 new console event registered for trace");
+  clearAndCheckStorage();
+
+  console.dir({});
+  ok(storage.getEvents().length === 2,
+    "1 new console event registered for dir");
+  clearAndCheckStorage();
+
+  console.count("count-label");
+  console.count("count-label");
+  ok(storage.getEvents().length === 3,
+    "2 new console events registered for 2 count calls");
+  clearAndCheckStorage();
+
+  console.group("group-label")
+  console.log("group-log");
+  ok(storage.getEvents().length === 3,
+    "2 new console events registered for group + log");
+  clearAndCheckStorage();
+
+  console.groupCollapsed("group-collapsed")
+  console.log("group-collapsed-log");
+  ok(storage.getEvents().length === 3,
+    "2 new console events registered for groupCollapsed + log");
+  clearAndCheckStorage();
+
+  console.group("closed-group-label")
+  console.log("group-log");
+  console.groupEnd()
+  ok(storage.getEvents().length === 4,
+    "3 new console events registered for group/groupEnd");
+  clearAndCheckStorage();
+
+  console.time("time-label");
+  console.timeEnd();
+  ok(storage.getEvents().length === 3,
+    "2 new console events registered for time/timeEnd");
+  clearAndCheckStorage();
+
+  console.timeStamp("timestamp-label");
+  ok(storage.getEvents().length === 2,
+    "1 new console event registered for timeStamp");
+  clearAndCheckStorage();
+
+  // Check that console.clear() clears previous clear messages
+  clearAndCheckStorage();
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/webidl/AddonManager.webidl
@@ -0,0 +1,41 @@
+/* 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/.
+ */
+
+/* We need a JSImplementation but cannot get one without a contract ID. Since
+   This object is only ever created from JS we don't need a real contract ID. */
+[ChromeOnly, JSImplementation="dummy"]
+interface Addon {
+  // The add-on's ID.
+  readonly attribute DOMString id;
+  // The add-on's version.
+  readonly attribute DOMString version;
+  // The add-on's type (extension, theme, etc.).
+  readonly attribute DOMString type;
+  // The add-on's name in the current locale.
+  readonly attribute DOMString name;
+  // The add-on's description in the current locale.
+  readonly attribute DOMString description;
+  // If the user has enabled this add-on, note that it still may not be running
+  // depending on whether enabling requires a restart or if the add-on is
+  // incompatible in some way.
+  readonly attribute boolean isEnabled;
+  // If the add-on is currently active in the browser.
+  readonly attribute boolean isActive;
+};
+
+[HeaderFile="mozilla/AddonManagerWebAPI.h",
+ Func="mozilla::AddonManagerWebAPI::IsAPIEnabled",
+ NavigatorProperty="mozAddonManager",
+ JSImplementation="@mozilla.org/addon-web-api/manager;1"]
+interface AddonManager {
+  /**
+   * Gets information about an add-on
+   *
+   * @param  id
+   *         The ID of the add-on to test for.
+   * @return A promise. It will resolve to an Addon if the add-on is installed.
+   */
+  Promise<Addon> getAddonByID(DOMString id);
+};
--- a/dom/webidl/Console.webidl
+++ b/dom/webidl/Console.webidl
@@ -18,27 +18,26 @@ interface Console {
   void dir(any... data);
   void dirxml(any... data);
   void group(any... data);
   void groupCollapsed(any... data);
   void groupEnd(any... data);
   void time(optional any time);
   void timeEnd(optional any time);
   void timeStamp(optional any data);
+  void clear(any... data);
 
   void profile(any... data);
   void profileEnd(any... data);
 
   void assert(boolean condition, any... data);
   void count(any... data);
 
   // No-op methods for compatibility with other browsers.
   [BinaryName="noopMethod"]
-  void clear();
-  [BinaryName="noopMethod"]
   void markTimeline();
   [BinaryName="noopMethod"]
   void timeline();
   [BinaryName="noopMethod"]
   void timelineEnd();
 };
 
 // This is used to propagate console events to the observers.
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -15,16 +15,17 @@ PREPROCESSED_WEBIDL_FILES = [
     'PromiseDebugging.webidl',
     'ServiceWorkerRegistration.webidl',
     'Window.webidl',
 ]
 
 WEBIDL_FILES = [
     'AbstractWorker.webidl',
     'ActivityRequestHandler.webidl',
+    'AddonManager.webidl',
     'AnalyserNode.webidl',
     'Animatable.webidl',
     'Animation.webidl',
     'AnimationEffectReadOnly.webidl',
     'AnimationEffectTiming.webidl',
     'AnimationEffectTimingReadOnly.webidl',
     'AnimationEvent.webidl',
     'AnimationTimeline.webidl',
--- a/mobile/android/app/checkstyle.xml
+++ b/mobile/android/app/checkstyle.xml
@@ -48,11 +48,16 @@
         <module name="NoLineWrap">
             <property name="tokens" value="IMPORT,PACKAGE_DEF"/>
         </module>
         <module name="OuterTypeFilename"/> <!-- `class Lol` only in Lol.java -->
         <module name="WhitespaceAfter">
             <!-- TODO: (bug 1263059) Remove specific tokens to enable CAST check. -->
             <property name="tokens" value="COMMA, SEMI"/>
         </module>
+        <module name="WhitespaceAround">
+            <property name="allowEmptyConstructors" value="true"/>
+            <property name="allowEmptyMethods" value="true"/>
+            <property name="allowEmptyTypes" value="true"/>
+        </module>
     </module>
 
 </module>
--- a/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ANRReporter.java
@@ -314,17 +314,17 @@ public final class ANRReporter extends B
             "\"simpleMeasurements\":{" +
                 "\"uptime\":" + String.valueOf(getUptimeMins()) +
             "}," +
             "\"info\":{" +
                 "\"reason\":\"android-anr-report\"," +
                 "\"OS\":" + JSONObject.quote(SysInfo.getName()) + "," +
                 "\"version\":\"" + String.valueOf(SysInfo.getVersion()) + "\"," +
                 "\"appID\":" + JSONObject.quote(AppConstants.MOZ_APP_ID) + "," +
-                "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION)+ "," +
+                "\"appVersion\":" + JSONObject.quote(AppConstants.MOZ_APP_VERSION) + "," +
                 "\"appName\":" + JSONObject.quote(AppConstants.MOZ_APP_BASENAME) + "," +
                 "\"appBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
                 "\"appUpdateChannel\":" + JSONObject.quote(AppConstants.MOZ_UPDATE_CHANNEL) + "," +
                 // Technically the platform build ID may be different, but we'll never know
                 "\"platformBuildID\":" + JSONObject.quote(AppConstants.MOZ_APP_BUILDID) + "," +
                 "\"locale\":" + JSONObject.quote(Locales.getLanguageTag(Locale.getDefault())) + "," +
                 "\"cpucount\":" + String.valueOf(SysInfo.getCPUCount()) + "," +
                 "\"memsize\":" + String.valueOf(SysInfo.getMemSize()) + "," +
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -147,17 +147,17 @@ class ActionBarTextSelection extends Lay
 
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
                 try {
                     if (event.equals("TextSelection:ShowHandles")) {
                         selectionID = message.getString("selectionID");
                         final JSONArray handles = message.getJSONArray("handles");
-                        for (int i=0; i < handles.length(); i++) {
+                        for (int i = 0; i < handles.length(); i++) {
                             String handle = handles.getString(i);
                             getHandle(handle).setVisibility(View.VISIBLE);
                         }
 
                         mViewLeft = 0.0f;
                         mViewTop = 0.0f;
                         mViewZoom = 0.0f;
 
@@ -188,17 +188,17 @@ class ActionBarTextSelection extends Lay
                         mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS);
 
                         anchorHandle.setVisibility(View.GONE);
                         caretHandle.setVisibility(View.GONE);
                         focusHandle.setVisibility(View.GONE);
 
                     } else if (event.equals("TextSelection:PositionHandles")) {
                         final JSONArray positions = message.getJSONArray("positions");
-                        for (int i=0; i < positions.length(); i++) {
+                        for (int i = 0; i < positions.length(); i++) {
                             JSONObject position = positions.getJSONObject(i);
                             final int left = position.getInt("left");
                             final int top = position.getInt("top");
                             final boolean rtl = position.getBoolean("rtl");
 
                             TextSelectionHandle handle = getHandle(position.getString("handle"));
                             handle.setVisibility(position.getBoolean("hidden") ? View.GONE : View.VISIBLE);
                             handle.positionFromGecko(left, top, rtl);
@@ -341,17 +341,17 @@ class ActionBarTextSelection extends Lay
                     BitmapUtils.getDrawable(anchorHandle.getContext(), iconString, new BitmapLoader() {
                         @Override
                         public void onBitmapFound(Drawable d) {
                             if (d != null) {
                                 menuitem.setIcon(d);
                             }
                         }
                     });
-                } catch(Exception ex) {
+                } catch (Exception ex) {
                     Log.i(LOGTAG, "Exception building menu", ex);
                 }
             }
             return true;
         }
 
         @Override
         public boolean onCreateActionMode(ActionModeCompat mode, Menu menu) {
@@ -360,17 +360,17 @@ class ActionBarTextSelection extends Lay
         }
 
         @Override
         public boolean onActionItemClicked(ActionModeCompat mode, MenuItem item) {
             try {
                 final JSONObject obj = mItems.getJSONObject(item.getItemId());
                 GeckoAppShell.notifyObservers("TextSelection:Action", obj.optString("id"));
                 return true;
-            } catch(Exception ex) {
+            } catch (Exception ex) {
                 Log.i(LOGTAG, "Exception calling action", ex);
             }
             return false;
         }
 
         // Called when the user exits the action mode
         @Override
         public void onDestroyActionMode(ActionModeCompat mode) {
--- a/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionModeCompat.java
@@ -116,12 +116,12 @@ class ActionModeCompat implements GeckoP
         int[] location = new int[2];
         final View view = item.getActionView();
         view.getLocationOnScreen(location);
 
         int xOffset = location[0] - view.getWidth();
         int yOffset = location[1] + view.getHeight() / 2;
 
         Toast toast = Toast.makeText(view.getContext(), item.getTitle(), Toast.LENGTH_SHORT);
-        toast.setGravity(Gravity.TOP|Gravity.LEFT, xOffset, yOffset);
+        toast.setGravity(Gravity.TOP | Gravity.LEFT, xOffset, yOffset);
         toast.show();
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/AlarmReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/AlarmReceiver.java
@@ -29,14 +29,14 @@ public class AlarmReceiver extends Broad
         TimerTask releaseLockTask = new TimerTask() {
             @Override
             public void run() {
                 wakeLock.release();
             }
         };
         Timer timer = new Timer();
         // 5 seconds ought to be enough for anybody
-        timer.schedule(releaseLockTask, 5*1000);
+        timer.schedule(releaseLockTask, 5 * 1000);
     }
 
     @WrapForJNI
     private static native void notifyAlarmFired();
 }
--- a/mobile/android/base/java/org/mozilla/gecko/AndroidGamepadManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/AndroidGamepadManager.java
@@ -296,17 +296,17 @@ public class AndroidGamepadManager {
         return true;
     }
 
     private static void scanForGamepads() {
         int[] deviceIds = InputDevice.getDeviceIds();
         if (deviceIds == null) {
             return;
         }
-        for (int i=0; i < deviceIds.length; i++) {
+        for (int i = 0; i < deviceIds.length; i++) {
             InputDevice device = InputDevice.getDevice(deviceIds[i]);
             if (device == null) {
                 continue;
             }
             if ((device.getSources() & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD) {
                 continue;
             }
             addGamepad(device);
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -19,16 +19,18 @@ import org.mozilla.gecko.DynamicToolbar.
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.distribution.Distribution.DistributionDescriptor;
+import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.dlc.catalog.DownloadContent;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.action.CheckForUpdatesAction;
 import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
@@ -319,17 +321,17 @@ public class BrowserApp extends GeckoApp
             // isn't tied to a specific tab.
             if (msg != Tabs.TabEvents.RESTORED) {
                 throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
             }
             return;
         }
 
         Log.d(LOGTAG, "BrowserApp.onTabChanged: " + tab.getId() + ": " + msg);
-        switch(msg) {
+        switch (msg) {
             case SELECTED:
                 if (Tabs.getInstance().isSelectedTab(tab) && mDynamicToolbar.isEnabled()) {
                     mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                 }
                 // fall through
             case LOCATION_CHANGE:
                 if (mZoomedView != null) {
                     mZoomedView.stopZoomDisplay(false);
@@ -683,17 +685,19 @@ public class BrowserApp extends GeckoApp
             "Sanitize:ClearHistory",
             "Sanitize:ClearSyncedTabs",
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch");
 
         // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
         // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
-        Distribution distribution = Distribution.init(this);
+        final Distribution distribution = Distribution.init(this);
+        distribution.addOnDistributionReadyCallback(new DistributionStoreCallback(this, getProfile().getName()));
+
         searchEngineManager = new SearchEngineManager(this, distribution);
 
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
         final BrowserDB db = getProfile().getDB();
         db.setSuggestedSites(suggestedSites);
 
         JavaAddonManager.getInstance().init(appContext);
@@ -910,17 +914,17 @@ public class BrowserApp extends GeckoApp
             StrictMode.setThreadPolicy(savedPolicy);
         }
     }
 
     private Class<?> getMediaPlayerManager() {
         if (AppConstants.MOZ_MEDIA_PLAYER) {
             try {
                 return Class.forName("org.mozilla.gecko.MediaPlayerManager");
-            } catch(Exception ex) {
+            } catch (Exception ex) {
                 // Ignore failures
                 Log.e(LOGTAG, "No native casting support", ex);
             }
         }
 
         return null;
     }
 
@@ -1177,17 +1181,17 @@ public class BrowserApp extends GeckoApp
         final Tab tab = Tabs.getInstance().getSelectedTab();
 
         final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
             @Override
             public void onPromptFinished(String result) {
                 int itemId = -1;
                 try {
                   itemId = new JSONObject(result).getInt("button");
-                } catch(JSONException ex) {
+                } catch (JSONException ex) {
                     Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
                 }
 
                 if (tab == null) {
                     return;
                 }
 
                 if (itemId == 0) {
@@ -3617,17 +3621,17 @@ public class BrowserApp extends GeckoApp
                             GeckoProfile.leaveGuestSession(BrowserApp.this);
 
                             // Now's a good time to make sure we're not displaying the Guest Browsing notification.
                             GuestSession.hideNotification(BrowserApp.this);
                         }
 
                         doRestart(args);
                     }
-                } catch(JSONException ex) {
+                } catch (JSONException ex) {
                     Log.e(LOGTAG, "Exception reading guest mode prompt result", ex);
                 }
             }
         });
 
         Resources res = getResources();
         ps.setButtons(new String[] {
             res.getString(R.string.guest_session_dialog_continue),
--- a/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ChromeCast.java
@@ -299,17 +299,17 @@ class ChromeCast implements GeckoMediaPl
                     if (!status.isSuccess()) {
                         debug("Unable to play: " + status.getStatusCode());
                         sendError(callback, status.toString());
                     } else {
                         sendSuccess(callback, null);
                     }
                 }
             });
-        } catch(IllegalStateException ex) {
+        } catch (IllegalStateException ex) {
             // The media player may throw if the session has been killed. For now, we're just catching this here.
             sendError(callback, "Error playing");
         }
     }
 
     @Override
     public void pause(final EventCallback callback) {
         if (!verifySession(callback)) {
@@ -324,17 +324,17 @@ class ChromeCast implements GeckoMediaPl
                     if (!status.isSuccess()) {
                         debug("Unable to pause: " + status.getStatusCode());
                         sendError(callback, status.toString());
                     } else {
                         sendSuccess(callback, null);
                     }
                 }
             });
-        } catch(IllegalStateException ex) {
+        } catch (IllegalStateException ex) {
             // The media player may throw if the session has been killed. For now, we're just catching this here.
             sendError(callback, "Error pausing");
         }
     }
 
     @Override
     public void end(final EventCallback callback) {
         if (!verifySession(callback)) {
@@ -353,27 +353,27 @@ class ChromeCast implements GeckoMediaPl
                             apiClient.disconnect();
                             apiClient = null;
 
                             if (callback != null) {
                                 sendSuccess(callback, null);
                             }
 
                             return;
-                        } catch(Exception ex) {
+                        } catch (Exception ex) {
                             debug("Error ending", ex);
                         }
                     }
 
                     if (callback != null) {
                         sendError(callback, result.getStatus().toString());
                     }
                 }
             });
-        } catch(IllegalStateException ex) {
+        } catch (IllegalStateException ex) {
             // The media player may throw if the session has been killed. For now, we're just catching this here.
             sendError(callback, "Error stopping");
         }
     }
 
     class MirrorChannel implements MessageReceivedCallback {
         /**
          * @return custom namespace
--- a/mobile/android/base/java/org/mozilla/gecko/ContactService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ContactService.java
@@ -652,17 +652,17 @@ public class ContactService implements G
             contact.put("properties", contactProperties);
         } catch (JSONException e) {
             throw new IllegalArgumentException(e);
         }
 
         if (DEBUG) {
             try {
                 Log.d(LOGTAG, "Got contact: " + contact.toString(3));
-            } catch (JSONException e) {}
+            } catch (JSONException e) { }
         }
 
         return contact;
     }
 
     private boolean bool(int integer) {
         return integer != 0;
     }
@@ -941,17 +941,17 @@ public class ContactService implements G
             Log.i(LOGTAG, "Removing contact with ID: " + rawContactId);
         } catch (JSONException e) {
             // We can't continue without a raw contact ID
             sendCallbackToJavascript("Android:Contact:Remove:Return:KO", requestID, null, null);
             return;
         }
 
         String returnStatus = "KO";
-        if(deleteContact(rawContactId)) {
+        if (deleteContact(rawContactId)) {
             returnStatus = "OK";
         }
 
         sendCallbackToJavascript("Android:Contact:Remove:Return:" + returnStatus, requestID,
                                  new String[] {"contactID"}, new Object[] {rawContactId});
     }
 
     private void saveContact(final JSONObject contactOptions, final String requestID) {
@@ -1725,17 +1725,17 @@ public class ContactService implements G
 
     private long[] getAllRawContactIds() {
         Cursor cursor = getAllRawContactIdsCursor();
 
         // Put the ids into an array
         long[] ids = new long[cursor.getCount()];
         int index = 0;
         cursor.moveToPosition(-1);
-        while(cursor.moveToNext()) {
+        while (cursor.moveToNext()) {
             ids[index] = cursor.getLong(cursor.getColumnIndex(RawContacts._ID));
             index++;
         }
         cursor.close();
 
         return ids;
     }
 
@@ -1816,17 +1816,17 @@ public class ContactService implements G
         for (int value : values) {
             if (value > max) {
                 max = value;
             }
         }
         return max;
     }
 
-    private static void putPossibleNullValueInJSONObject(final String key, final Object value, JSONObject jsonObject) throws JSONException{
+    private static void putPossibleNullValueInJSONObject(final String key, final Object value, JSONObject jsonObject) throws JSONException {
         if (value != null) {
             jsonObject.put(key, value);
         } else {
             jsonObject.put(key, JSONObject.NULL);
         }
     }
 
     private static String getKeyFromMapValue(final HashMap<String, Integer> map, int value) {
--- a/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/DoorHangerPopup.java
@@ -143,17 +143,17 @@ public class DoorHangerPopup extends Anc
         }
 
         return config;
     }
 
     // This callback is automatically executed on the UI thread.
     @Override
     public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
-        switch(msg) {
+        switch (msg) {
             case CLOSED:
                 // Remove any doorhangers for a tab when it's closed (make
                 // a temporary set to avoid a ConcurrentModificationException)
                 removeTabDoorHangers(tab.getId(), true);
                 break;
 
             case LOCATION_CHANGE:
                 // Only remove doorhangers if the popup is hidden or if we're navigating to a new URL
--- a/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java
+++ b/mobile/android/base/java/org/mozilla/gecko/DownloadsIntegration.java
@@ -10,16 +10,17 @@ import org.mozilla.gecko.AppConstants.Ve
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.EventCallback;
 
 import java.io.File;
 import java.lang.IllegalArgumentException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import android.app.DownloadManager;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.media.MediaScannerConnection;
 import android.media.MediaScannerConnection.MediaScannerConnectionClient;
@@ -27,22 +28,24 @@ import android.net.Uri;
 import android.os.Environment;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class DownloadsIntegration implements NativeEventListener
 {
     private static final String LOGTAG = "GeckoDownloadsIntegration";
 
-    @SuppressWarnings("serial")
-    private static final List<String> UNKNOWN_MIME_TYPES = new ArrayList<String>(3) {{
-        add("unknown/unknown"); // This will be used as a default mime type for unknown files
-        add("application/unknown");
-        add("application/octet-stream"); // Github uses this for APK files
-    }};
+    private static final List<String> UNKNOWN_MIME_TYPES;
+    static {
+        final ArrayList<String> tempTypes = new ArrayList<>(3);
+        tempTypes.add("unknown/unknown"); // This will be used as a default mime type for unknown files
+        tempTypes.add("application/unknown");
+        tempTypes.add("application/octet-stream"); // Github uses this for APK files
+        UNKNOWN_MIME_TYPES = Collections.unmodifiableList(tempTypes);
+    }
 
     private static final String DOWNLOAD_REMOVE = "Download:Remove";
 
     private DownloadsIntegration() {
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener)this, DOWNLOAD_REMOVE);
     }
 
     private static DownloadsIntegration sInstance;
@@ -137,17 +140,17 @@ public class DownloadsIntegration implem
             // one from the file extension below.
             mimeType = "";
         }
 
         // If the platform didn't give us a mimetype, try to guess one from the filename
         if (TextUtils.isEmpty(mimeType)) {
             final int extPosition = aFile.lastIndexOf(".");
             if (extPosition > 0 && extPosition < aFile.length() - 1) {
-                mimeType = GeckoAppShell.getMimeTypeFromExtension(aFile.substring(extPosition+1));
+                mimeType = GeckoAppShell.getMimeTypeFromExtension(aFile.substring(extPosition + 1));
             }
         }
 
         // addCompletedDownload will throw if it received any null parameters. Use aMimeType or a default
         // if we still don't have one.
         if (TextUtils.isEmpty(mimeType)) {
             if (TextUtils.isEmpty(aMimeType)) {
                 mimeType = UNKNOWN_MIME_TYPES.get(0);
@@ -188,17 +191,17 @@ public class DownloadsIntegration implem
             }
 
             do {
                 final Download d = Download.fromCursor(c);
                 // Try hard as we can to verify this download is the one we think it is
                 if (download.equals(d)) {
                     dm.remove(d.id);
                 }
-            } while(c.moveToNext());
+            } while (c.moveToNext());
         } finally {
             if (c != null) {
                 c.close();
             }
         }
     }
 
     private static final class GeckoMediaScannerClient implements MediaScannerConnectionClient {
@@ -218,15 +221,15 @@ public class DownloadsIntegration implem
 
         @Override
         public void onMediaScannerConnected() {
             mScanner.scanFile(mFile, mMimeType);
         }
 
         @Override
         public void onScanCompleted(String path, Uri uri) {
-            if(path.equals(mFile)) {
+            if (path.equals(mFile)) {
                 mScanner.disconnect();
                 mScanner = null;
             }
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
+++ b/mobile/android/base/java/org/mozilla/gecko/FilePickerResultHandler.java
@@ -230,17 +230,17 @@ class FilePickerResultHandler implements
                     fos.close();
                     is.close();
                     tempFile = file.getAbsolutePath();
                     sendResult((tempFile == null) ? "" : tempFile);
 
                     if (tabId > -1 && !TextUtils.isEmpty(tempFile)) {
                         Tabs.registerOnTabsChangedListener(this);
                     }
-                } catch(IOException ex) {
+                } catch (IOException ex) {
                     Log.i(LOGTAG, "Error writing file", ex);
                 } finally {
                     if (fos != null) {
                         try {
                             fos.close();
                         } catch (IOException e) { /* not much to do here */ }
                     }
                 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -136,17 +136,17 @@ public abstract class GeckoApp
     GeckoMenu.MenuPresenter,
     LocationListener,
     NativeEventListener,
     SensorEventListener,
     Tabs.OnTabsChangedListener,
     ViewTreeObserver.OnGlobalLayoutListener {
 
     private static final String LOGTAG = "GeckoApp";
-    private static final int ONE_DAY_MS = 1000*60*60*24;
+    private static final int ONE_DAY_MS = 1000 * 60 * 60 * 24;
 
     public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
     public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
     public static final String ACTION_DEBUG                = "org.mozilla.gecko.DEBUG";
     public static final String ACTION_LAUNCH_SETTINGS      = "org.mozilla.gecko.SETTINGS";
     public static final String ACTION_LOAD                 = "org.mozilla.gecko.LOAD";
     public static final String ACTION_INIT_PW              = "org.mozilla.gecko.INIT_PW";
 
@@ -273,17 +273,17 @@ public abstract class GeckoApp
     public FormAssistPopup getFormAssistPopup() {
         return mFormAssistPopup;
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         // When a tab is closed, it is always unselected first.
         // When a tab is unselected, another tab is always selected first.
-        switch(msg) {
+        switch (msg) {
             case UNSELECTED:
                 hidePlugins(tab);
                 break;
 
             case LOCATION_CHANGE:
                 // We only care about location change for the selected tab.
                 if (!Tabs.getInstance().isSelectedTab(tab))
                     break;
@@ -461,35 +461,35 @@ public abstract class GeckoApp
             final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
             final Set<String> clearSet =
                     PrefUtils.getStringSet(prefs, ClearOnShutdownPref.PREF, new HashSet<String>());
 
             final JSONObject clearObj = new JSONObject();
             for (String clear : clearSet) {
                 try {
                     clearObj.put(clear, true);
-                } catch(JSONException ex) {
+                } catch (JSONException ex) {
                     Log.e(LOGTAG, "Error adding clear object " + clear, ex);
                 }
             }
 
             final JSONObject res = new JSONObject();
             try {
                 res.put("sanitize", clearObj);
-            } catch(JSONException ex) {
+            } catch (JSONException ex) {
                 Log.e(LOGTAG, "Error adding sanitize object", ex);
             }
 
             // If the user has opted out of session restore, and does want to clear history
             // we also want to prevent the current session info from being saved.
             if (clearObj.has("private.data.history")) {
                 final String sessionRestore = getSessionRestorePreference();
                 try {
                     res.put("dontSaveSession", "quit".equals(sessionRestore));
-                } catch(JSONException ex) {
+                } catch (JSONException ex) {
                     Log.e(LOGTAG, "Error adding session restore data", ex);
                 }
             }
 
             GeckoAppShell.notifyObservers("Browser:Quit", res.toString());
             doShutdown();
             return true;
         }
@@ -766,17 +766,17 @@ public abstract class GeckoApp
                 for (int i = 0; i < checkedItemPositions.size(); i++)
                     if (checkedItemPositions.get(i))
                         permissionsToClear.put(i);
 
                 GeckoAppShell.notifyObservers("Permissions:Clear", permissionsToClear.toString());
             }
         });
 
-        builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener(){
+        builder.setNegativeButton(R.string.site_settings_cancel, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int id) {
                 dialog.cancel();
             }
         });
 
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
@@ -931,29 +931,29 @@ public abstract class GeckoApp
     private void setImageAs(final String aSrc) {
         boolean isDataURI = aSrc.startsWith("data:");
         Bitmap image = null;
         InputStream is = null;
         ByteArrayOutputStream os = null;
         try {
             if (isDataURI) {
                 int dataStart = aSrc.indexOf(",");
-                byte[] buf = Base64.decode(aSrc.substring(dataStart+1), Base64.DEFAULT);
+                byte[] buf = Base64.decode(aSrc.substring(dataStart + 1), Base64.DEFAULT);
                 image = BitmapUtils.decodeByteArray(buf);
             } else {
                 int byteRead;
                 byte[] buf = new byte[4192];
                 os = new ByteArrayOutputStream();
                 URL url = new URL(aSrc);
                 is = url.openStream();
 
                 // Cannot read from same stream twice. Also, InputStream from
                 // URL does not support reset. So converting to byte array.
 
-                while((byteRead = is.read(buf)) != -1) {
+                while ((byteRead = is.read(buf)) != -1) {
                     os.write(buf, 0, byteRead);
                 }
                 byte[] imgBuffer = os.toByteArray();
                 image = BitmapUtils.decodeByteArray(imgBuffer);
             }
             if (image != null) {
                 // Some devices don't have a DCIM folder and the Media.insertImage call will fail.
                 File dcimDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
@@ -978,32 +978,32 @@ public abstract class GeckoApp
                     public void onActivityResult (int resultCode, Intent data) {
                         getContentResolver().delete(intent.getData(), null, null);
                     }
                 };
                 ActivityHandlerHelper.startIntentForActivity(this, chooser, handler);
             } else {
                 SnackbarHelper.showSnackbar(this, getString(R.string.set_image_fail), Snackbar.LENGTH_LONG);
             }
-        } catch(OutOfMemoryError ome) {
+        } catch (OutOfMemoryError ome) {
             Log.e(LOGTAG, "Out of Memory when converting to byte array", ome);
-        } catch(IOException ioe) {
+        } catch (IOException ioe) {
             Log.e(LOGTAG, "I/O Exception while setting wallpaper", ioe);
         } finally {
             if (is != null) {
                 try {
                     is.close();
-                } catch(IOException ioe) {
+                } catch (IOException ioe) {
                     Log.w(LOGTAG, "I/O Exception while closing stream", ioe);
                 }
             }
             if (os != null) {
                 try {
                     os.close();
-                } catch(IOException ioe) {
+                } catch (IOException ioe) {
                     Log.w(LOGTAG, "I/O Exception while closing stream", ioe);
                 }
             }
         }
     }
 
     private int getBitmapSampleSize(BitmapFactory.Options options, int idealWidth, int idealHeight) {
         int width = options.outWidth;
@@ -1127,17 +1127,17 @@ public abstract class GeckoApp
         // GeckoLoader wants to dig some environment variables out of the
         // incoming intent, so pass it in here. GeckoLoader will do its
         // business later and dispose of the reference.
         GeckoLoader.setLastIntent(intent);
 
         // Workaround for <http://code.google.com/p/android/issues/detail?id=20915>.
         try {
             Class.forName("android.os.AsyncTask");
-        } catch (ClassNotFoundException e) {}
+        } catch (ClassNotFoundException e) { }
 
         MemoryMonitor.getInstance().init(getApplicationContext());
 
         // GeckoAppShell is tightly coupled to us, rather than
         // the app context, because various parts of Fennec (e.g.,
         // GeckoScreenOrientation) use GAS to access the Activity in
         // the guise of fetching a Context.
         // When that's fixed, `this` can change to
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -509,17 +509,17 @@ public class GeckoAppShell
         AlarmManager am = (AlarmManager)
             getApplicationContext().getSystemService(Context.ALARM_SERVICE);
 
         Intent intent = new Intent(getApplicationContext(), AlarmReceiver.class);
         PendingIntent pi = PendingIntent.getBroadcast(
                 getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
 
         // AlarmManager only supports millisecond precision
-        long time = ((long)aSeconds * 1000) + ((long)aNanoSeconds/1_000_000L);
+        long time = ((long) aSeconds * 1000) + ((long) aNanoSeconds / 1_000_000L);
         am.setExact(AlarmManager.RTC_WAKEUP, time, pi);
 
         return true;
     }
 
     @WrapForJNI
     public static void disableAlarm() {
         AlarmManager am = (AlarmManager)
@@ -536,17 +536,17 @@ public class GeckoAppShell
     public static void enableSensor(int aSensortype) {
         GeckoInterface gi = getGeckoInterface();
         if (gi == null) {
             return;
         }
         SensorManager sm = (SensorManager)
             getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
 
-        switch(aSensortype) {
+        switch (aSensortype) {
         case GeckoHalDefines.SENSOR_GAME_ROTATION_VECTOR:
             if (gGameRotationVectorSensor == null) {
                 gGameRotationVectorSensor = sm.getDefaultSensor(15);
                     // sm.getDefaultSensor(
                     //     Sensor.TYPE_GAME_ROTATION_VECTOR); // API >= 18
             }
             if (gGameRotationVectorSensor != null) {
                 sm.registerListener(gi.getSensorEventListener(),
@@ -1226,17 +1226,17 @@ public class GeckoAppShell
                     android.os.Process.killProcess(pid);
                 return true;
             }
         };
 
         EnumerateGeckoProcesses(visitor);
     }
 
-    interface GeckoProcessesVisitor{
+    interface GeckoProcessesVisitor {
         boolean callback(int pid);
     }
 
     private static void EnumerateGeckoProcesses(GeckoProcessesVisitor visiter) {
         int pidColumn = -1;
         int userColumn = -1;
 
         try {
@@ -1292,17 +1292,17 @@ public class GeckoAppShell
             cmdlineReader = new BufferedReader(new FileReader(cmdlineFile));
             return cmdlineReader.readLine().trim();
         } catch (Exception ex) {
             return "";
         } finally {
             if (null != cmdlineReader) {
                 try {
                     cmdlineReader.close();
-                } catch (Exception e) {}
+                } catch (Exception e) { }
             }
         }
     }
 
     public static void listOfOpenFiles() {
         int pidColumn = -1;
         int nameColumn = -1;
 
@@ -1487,17 +1487,17 @@ public class GeckoAppShell
             }
         }
 
         ArrayList<String> directories = new ArrayList<String>();
         PackageManager pm = getApplicationContext().getPackageManager();
         List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
 
-        synchronized(mPackageInfoCache) {
+        synchronized (mPackageInfoCache) {
 
             // clear the list of existing packageInfo objects
             mPackageInfoCache.clear();
 
 
             for (ResolveInfo info : plugins) {
 
                 // retrieve the plugin's service information
@@ -1614,17 +1614,17 @@ public class GeckoAppShell
     }
 
     static String getPluginPackage(String pluginLib) {
 
         if (pluginLib == null || pluginLib.length() == 0) {
             return null;
         }
 
-        synchronized(mPackageInfoCache) {
+        synchronized (mPackageInfoCache) {
             for (PackageInfo pkgInfo : mPackageInfoCache) {
                 if (pluginLib.contains(pkgInfo.packageName)) {
                     return pkgInfo.packageName;
                 }
             }
         }
 
         return null;
@@ -1787,17 +1787,17 @@ public class GeckoAppShell
     @WrapForJNI(stubName = "InitCameraWrapper")
     static int[] initCamera(String aContentType, int aCamera, int aWidth, int aHeight) {
         ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
                     try {
                         if (getGeckoInterface() != null)
                             getGeckoInterface().enableCameraView();
-                    } catch (Exception e) {}
+                    } catch (Exception e) { }
                 }
             });
 
         // [0] = 0|1 (failure/success)
         // [1] = width
         // [2] = height
         // [3] = fps
         int[] result = new int[4];
@@ -1819,17 +1819,17 @@ public class GeckoAppShell
                 Iterator<Integer> it = params.getSupportedPreviewFrameRates().iterator();
                 while (it.hasNext()) {
                     int nFps = it.next();
                     if (Math.abs(nFps - kPreferredFPS) < fpsDelta) {
                         fpsDelta = Math.abs(nFps - kPreferredFPS);
                         params.setPreviewFrameRate(nFps);
                     }
                 }
-            } catch(Exception e) {
+            } catch (Exception e) {
                 params.setPreviewFrameRate(kPreferredFPS);
             }
 
             // set up the closest preview size available
             Iterator<android.hardware.Camera.Size> sit = params.getSupportedPreviewSizes().iterator();
             int sizeDelta = 10000000;
             int bufferSize = 0;
             while (sit.hasNext()) {
@@ -1866,32 +1866,32 @@ public class GeckoAppShell
                 }
             });
             sCamera.startPreview();
             params = sCamera.getParameters();
             result[0] = 1;
             result[1] = params.getPreviewSize().width;
             result[2] = params.getPreviewSize().height;
             result[3] = params.getPreviewFrameRate();
-        } catch(RuntimeException e) {
+        } catch (RuntimeException e) {
             Log.w(LOGTAG, "initCamera RuntimeException.", e);
             result[0] = result[1] = result[2] = result[3] = 0;
         }
         return result;
     }
 
     @WrapForJNI
     static synchronized void closeCamera() {
         ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
                     try {
                         if (getGeckoInterface() != null)
                             getGeckoInterface().disableCameraView();
-                    } catch (Exception e) {}
+                    } catch (Exception e) { }
                 }
             });
         if (sCamera != null) {
             sCamera.stopPreview();
             sCamera.release();
             sCamera = null;
             sCameraBuffer = null;
         }
@@ -2205,17 +2205,17 @@ public class GeckoAppShell
                 connect(output);
                 ThreadUtils.postToBackgroundThread(
                     new Runnable() {
                         @Override
                         public void run() {
                             try {
                                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, output);
                                 output.close();
-                            } catch (IOException ioe) {}
+                            } catch (IOException ioe) { }
                         }
                     });
                 mHaveConnected = true;
                 return super.read(buffer, byteOffset, byteCount);
             }
         }
     }
 
@@ -2237,26 +2237,26 @@ public class GeckoAppShell
                         return null;
                     }
                     final String pkg = splits[1];
                     final PackageManager pm = getApplicationContext().getPackageManager();
                     final Drawable d = pm.getApplicationIcon(pkg);
                     final Bitmap bitmap = BitmapUtils.getBitmapFromDrawable(d);
                     return new BitmapConnection(bitmap);
                 }
-            } catch(Exception ex) {
+            } catch (Exception ex) {
                 Log.e(LOGTAG, "error", ex);
             }
 
             // if the colon got stripped, put it back
             int colon = spec.indexOf(':');
             if (colon == -1 || colon > spec.indexOf('/')) {
                 spec = spec.replaceFirst("/", ":/");
             }
-        } catch(Exception ex) {
+        } catch (Exception ex) {
             return null;
         }
         return null;
     }
 
     @WrapForJNI(allowMultithread = true, narrowChars = true)
     static String connectionGetMimeType(URLConnection connection) {
         return connection.getContentType();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoEditable.java
@@ -248,17 +248,17 @@ final class GeckoEditable extends JNIObj
             if (mListener == null) {
                 // We haven't initialized or we've been destroyed.
                 return;
             }
 
             if (mActions.isEmpty()) {
                 mActionsActive.acquireUninterruptibly();
                 mActions.offer(action);
-            } else synchronized(this) {
+            } else synchronized (this) {
                 // tryAcquire here in case Gecko thread has just released it
                 mActionsActive.tryAcquire();
                 mActions.offer(action);
             }
 
             switch (action.mType) {
             case Action.TYPE_EVENT:
             case Action.TYPE_SET_SPAN:
@@ -345,17 +345,17 @@ final class GeckoEditable extends JNIObj
         void poll() {
             if (DEBUG) {
                 ThreadUtils.assertOnGeckoThread();
             }
             if (mActions.poll() == null) {
                 throw new IllegalStateException("empty actions queue");
             }
 
-            synchronized(this) {
+            synchronized (this) {
                 if (mActions.isEmpty()) {
                     mActionsActive.release();
                 }
             }
         }
 
         /**
          * Return, but don't remove, the head of the queue, or null if queue is empty.
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoEvent.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoEvent.java
@@ -317,21 +317,21 @@ public class GeckoEvent {
             // the radius is found by removing the orientation and measuring the x and y
             // radius of the resulting ellipse
             // for android orientations >= 0 and < 90, the major axis should correspond to
             // just reporting the y radius as the major one, and x as minor
             // however, for a radius < 0, we have to shift the orientation by adding 90, and
             // reverse which radius is major and minor
             if (mOrientations[index] < 0) {
                 mOrientations[index] += 90;
-                mPointRadii[index] = new Point((int)event.getToolMajor(eventIndex)/2,
-                                               (int)event.getToolMinor(eventIndex)/2);
+                mPointRadii[index] = new Point((int) event.getToolMajor(eventIndex) / 2,
+                                               (int) event.getToolMinor(eventIndex) / 2);
             } else {
-                mPointRadii[index] = new Point((int)event.getToolMinor(eventIndex)/2,
-                                               (int)event.getToolMajor(eventIndex)/2);
+                mPointRadii[index] = new Point((int) event.getToolMinor(eventIndex) / 2,
+                                               (int) event.getToolMajor(eventIndex) / 2);
             }
 
             if (!keepInViewCoordinates) {
                 // If we are converting to gecko CSS pixels, then we should adjust the
                 // radii as well
                 float zoom = GeckoAppShell.getLayerView().getViewportMetrics().zoomFactor;
                 mPointRadii[index].x /= zoom;
                 mPointRadii[index].y /= zoom;
@@ -360,17 +360,17 @@ public class GeckoEvent {
         }
         return GeckoHalDefines.SENSOR_ACCURACY_UNKNOWN;
     }
 
     public static GeckoEvent createSensorEvent(SensorEvent s) {
         int sensor_type = s.sensor.getType();
         GeckoEvent event = null;
 
-        switch(sensor_type) {
+        switch (sensor_type) {
 
         case Sensor.TYPE_ACCELEROMETER:
             event = GeckoEvent.get(NativeGeckoEvent.SENSOR_EVENT);
             event.mFlags = GeckoHalDefines.SENSOR_ACCELERATION;
             event.mMetaState = HalSensorAccuracyFor(s.accuracy);
             event.mX = s.values[0];
             event.mY = s.values[1];
             event.mZ = s.values[2];
@@ -430,17 +430,17 @@ public class GeckoEvent {
             event.mY = s.values[1];
             event.mZ = s.values[2];
             if (s.values.length >= 4) {
                 event.mW = s.values[3];
             } else {
                 // s.values[3] was optional in API <= 18, so we need to compute it
                 // The values form a unit quaternion, so we can compute the angle of
                 // rotation purely based on the given 3 values.
-                event.mW = 1 - s.values[0]*s.values[0] - s.values[1]*s.values[1] - s.values[2]*s.values[2];
+                event.mW = 1 - s.values[0] * s.values[0] - s.values[1] * s.values[1] - s.values[2] * s.values[2];
                 event.mW = (event.mW > 0.0) ? Math.sqrt(event.mW) : 0.0;
             }
             break;
         }
 
         // SensorEvent timestamp is in nanoseconds, Gecko expects microseconds.
         event.mTime = s.timestamp / 1000;
         return event;
@@ -596,17 +596,17 @@ public class GeckoEvent {
         event.mAction = added ? ACTION_GAMEPAD_ADDED : ACTION_GAMEPAD_REMOVED;
         return event;
     }
 
     private static int boolArrayToBitfield(boolean[] array) {
         int bits = 0;
         for (int i = 0; i < array.length; i++) {
             if (array[i]) {
-                bits |= 1<<i;
+                bits |= 1 << i;
             }
         }
         return bits;
     }
 
     public static GeckoEvent createGamepadButtonEvent(int id,
                                                       int which,
                                                       boolean pressed,
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoInputConnection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -285,17 +285,17 @@ class GeckoInputConnection
             // Fake a selection change, because the IME clears the composition when
             // the selection changes, even if soft-resetting. Offsets here must be
             // different from the previous selection offsets, and -1 seems to be a
             // reasonable, deterministic value
             notifySelectionChange(-1, -1);
         }
         try {
             imm.restartInput(v);
-        } catch(RuntimeException e) {
+        } catch (RuntimeException e) {
             Log.e(LOGTAG, "Error restarting input", e);
         }
     }
 
     private void resetInputConnection() {
         if (mBatchEditCount != 0) {
             Log.w(LOGTAG, "resetting with mBatchEditCount = " + mBatchEditCount);
             mBatchEditCount = 0;
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoJavaSampler.java
@@ -97,17 +97,17 @@ public class GeckoJavaSampler {
                     Thread.sleep(mInterval);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 synchronized (GeckoJavaSampler.class) {
                     if (!mPauseSampler) {
                         StackTraceElement[] bt = sMainThread.getStackTrace();
                         mSamples.get(0)[mSamplePos] = new Sample(bt);
-                        mSamplePos = (mSamplePos+1) % mSamples.get(0).length;
+                        mSamplePos = (mSamplePos + 1) % mSamples.get(0).length;
                     }
                     if (mStopSampler) {
                         break;
                     }
                 }
             }
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoNetworkManager.java
@@ -296,18 +296,17 @@ public class GeckoNetworkManager extends
         if (cm == null) {
             Log.e(LOGTAG, "Connectivity service does not exist");
             return ConnectionType.NONE;
         }
 
         NetworkInfo ni = null;
         try {
             ni = cm.getActiveNetworkInfo();
-        } catch (SecurityException se) {} // if we don't have the permission, fall through to null check
-
+        } catch (SecurityException se) { /* if we don't have the permission, fall through to null check */ }
         if (ni == null) {
             return ConnectionType.NONE;
         }
 
         switch (ni.getType()) {
         case ConnectivityManager.TYPE_BLUETOOTH:
             return ConnectionType.BLUETOOTH;
         case ConnectivityManager.TYPE_ETHERNET:
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -490,17 +490,17 @@ public final class GeckoProfile {
             final File lockFile = new File(getDir(), LOCK_FILE_NAME);
             final boolean result = lockFile.createNewFile();
             if (lockFile.exists()) {
                 mLocked = LockState.LOCKED;
             } else {
                 mLocked = LockState.UNLOCKED;
             }
             return result;
-        } catch(IOException ex) {
+        } catch (IOException ex) {
             Log.e(LOGTAG, "Error locking profile", ex);
         }
         mLocked = LockState.UNLOCKED;
         return false;
     }
 
     public boolean unlock() {
         final File profileDir;
@@ -522,17 +522,17 @@ public final class GeckoProfile {
 
             final boolean result = delete(lockFile);
             if (result) {
                 mLocked = LockState.UNLOCKED;
             } else {
                 mLocked = LockState.LOCKED;
             }
             return result;
-        } catch(IOException ex) {
+        } catch (IOException ex) {
             Log.e(LOGTAG, "Error unlocking profile", ex);
         }
 
         mLocked = LockState.LOCKED;
         return false;
     }
 
     @RobocopTarget
@@ -620,46 +620,53 @@ public final class GeckoProfile {
         }
 
         String clientIdToWrite;
         try {
             clientIdToWrite = getValidClientIdFromDisk(FHR_CLIENT_ID_FILE_PATH);
         } catch (final IOException e) {
             // Avoid log spam: don't log the full Exception w/ the stack trace.
             Log.d(LOGTAG, "Could not migrate client ID from FHR – creating a new one: " + e.getLocalizedMessage());
-            clientIdToWrite = UUID.randomUUID().toString();
+            clientIdToWrite = generateNewClientId();
         }
 
         // There is a possibility Gecko is running and the Gecko telemetry implementation decided it's time to generate
         // the client ID, writing client ID underneath us. Since it's highly unlikely (e.g. we run in onStart before
         // Gecko is started), we don't handle that possibility besides writing the ID and then reading from the file
         // again (rather than just returning the value we generated before writing).
         //
         // In the event it does happen, any discrepancy will be resolved after a restart. In the mean time, both this
         // implementation and the Gecko implementation could upload documents with inconsistent IDs.
         //
         // In any case, if we get an exception, intentionally throw - there's nothing more to do here.
         persistClientId(clientIdToWrite);
         return getValidClientIdFromDisk(CLIENT_ID_FILE_PATH);
     }
 
+    protected static String generateNewClientId() {
+        return UUID.randomUUID().toString();
+    }
+
     /**
      * @return a valid client ID
      * @throws IOException if a valid client ID could not be retrieved
      */
     @WorkerThread
     private String getValidClientIdFromDisk(final String filePath) throws IOException {
         final JSONObject obj = readJSONObjectFromFile(filePath);
         final String clientId = obj.optString(CLIENT_ID_JSON_ATTR);
         if (isClientIdValid(clientId)) {
             return clientId;
         }
         throw new IOException("Received client ID is invalid: " + clientId);
     }
 
+    /**
+     * Persists the given client ID to disk. This will overwrite any existing files.
+     */
     @WorkerThread
     private void persistClientId(final String clientId) throws IOException {
         if (!ensureParentDirs(CLIENT_ID_FILE_PATH)) {
             throw new IOException("Could not create client ID parent directories");
         }
 
         final JSONObject obj = new JSONObject();
         try {
@@ -912,26 +919,26 @@ public final class GeckoProfile {
                     continue;
                 }
 
                 if (section.getName().startsWith("Profile")) {
                     // ok, we have stupid Profile#-named things.  Rename backwards.
                     try {
                         int sectionNumber = Integer.parseInt(section.getName().substring("Profile".length()));
                         String curSection = "Profile" + sectionNumber;
-                        String nextSection = "Profile" + (sectionNumber+1);
+                        String nextSection = "Profile" + (sectionNumber + 1);
 
                         sections.remove(curSection);
 
                         while (sections.containsKey(nextSection)) {
                             parser.renameSection(nextSection, curSection);
                             sectionNumber++;
 
                             curSection = nextSection;
-                            nextSection = "Profile" + (sectionNumber+1);
+                            nextSection = "Profile" + (sectionNumber + 1);
                         }
                     } catch (NumberFormatException nex) {
                         // uhm, malformed Profile thing; we can't do much.
                         Log.e(LOGTAG, "Malformed section name in profiles.ini: " + section.getName());
                         return false;
                     }
                 } else {
                     // this really shouldn't be the case, but handle it anyway
@@ -978,16 +985,17 @@ public final class GeckoProfile {
 
     private File findProfileDir() throws NoSuchProfileException {
         if (isCustomProfile()) {
             return mProfileDir;
         }
         return GeckoProfileDirectories.findProfileDir(mMozillaDir, mName);
     }
 
+    @WorkerThread
     private File createProfileDir() throws IOException {
         if (isCustomProfile()) {
             // Custom profiles must already exist.
             return mProfileDir;
         }
 
         INIParser parser = GeckoProfileDirectories.getProfilesINI(mMozillaDir);
 
@@ -1053,16 +1061,21 @@ public final class GeckoProfile {
             } finally {
                 writer.close();
             }
         } catch (Exception e) {
             // Best-effort.
             Log.w(LOGTAG, "Couldn't write " + TIMES_PATH, e);
         }
 
+        // Create the client ID file before Gecko starts (we assume this method
+        // is called before Gecko starts). If we let Gecko start, the JS telemetry
+        // code may try to write to the file at the same time Java does.
+        persistClientId(generateNewClientId());
+
         // Initialize pref flag for displaying the start pane for a new profile.
         final SharedPreferences prefs = GeckoSharedPrefs.forProfile(mApplicationContext);
         prefs.edit().putBoolean(FirstrunAnimationContainer.PREF_FIRSTRUN_ENABLED, true).apply();
 
         return profileDir;
     }
 
     /**
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoSmsManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoSmsManager.java
@@ -74,17 +74,17 @@ class Envelope
     mId = aId;
     mMessageId = -1;
     mError = GeckoSmsManager.kNoError;
 
     int size = SubParts.values().length;
     mRemainingParts = new int[size];
     mFailing = new boolean[size];
 
-    for (int i=0; i<size; ++i) {
+    for (int i = 0; i < size; ++i) {
       mRemainingParts[i] = aParts;
       mFailing[i] = false;
     }
   }
 
   public void decreaseRemainingParts(SubParts aType) {
     --mRemainingParts[aType.ordinal()];
 
@@ -150,17 +150,17 @@ class Postman
 
   public int createEnvelope(int aParts) {
     /*
      * We are going to create the envelope in the first empty slot in the array
      * list. If there is no empty slot, we create a new one.
      */
     int size = mEnvelopes.size();
 
-    for (int i=0; i<size; ++i) {
+    for (int i = 0; i < size; ++i) {
       if (mEnvelopes.get(i) == null) {
         mEnvelopes.set(i, new Envelope(i, aParts));
         return i;
       }
     }
 
     mEnvelopes.add(new Envelope(size, aParts));
     return size;
@@ -593,17 +593,17 @@ public class GeckoSmsManager
         sentIntent.putExtras(bundle);
         deliveredIntent.putExtras(bundle);
 
         ArrayList<PendingIntent> sentPendingIntents =
           new ArrayList<PendingIntent>(parts.size());
         ArrayList<PendingIntent> deliveredPendingIntents =
           new ArrayList<PendingIntent>(parts.size());
 
-        for (int i=0; i<parts.size(); ++i) {
+        for (int i = 0; i < parts.size(); ++i) {
           sentPendingIntents.add(
             PendingIntent.getBroadcast(GeckoAppShell.getContext(),
                                        pendingIntentGuid.incrementAndGet(), sentIntent,
                                        PendingIntent.FLAG_CANCEL_CURRENT)
           );
 
           deliveredPendingIntents.add(
             PendingIntent.getBroadcast(GeckoAppShell.getContext(),
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoView.java
@@ -538,17 +538,17 @@ public class GeckoView extends LayerView
         public PromptResult(JSONObject message) {
             mMessage = message;
         }
 
         private JSONObject makeResult(int resultCode) {
             JSONObject result = new JSONObject();
             try {
                 result.put("button", resultCode);
-            } catch(JSONException ex) { }
+            } catch (JSONException ex) { }
             return result;
         }
 
         /**
         * Handle a confirmation response from the user.
         */
         public void confirm() {
             JSONObject result = makeResult(RESULT_OK);
@@ -558,17 +558,17 @@ public class GeckoView extends LayerView
         /**
         * Handle a confirmation response from the user.
         * @param value String value to return to the browser context.
         */
         public void confirmWithValue(String value) {
             JSONObject result = makeResult(RESULT_OK);
             try {
                 result.put("textbox0", value);
-            } catch(JSONException ex) { }
+            } catch (JSONException ex) { }
             EventDispatcher.sendResponse(mMessage, result);
         }
 
         /**
         * Handle a cancellation response from the user.
         */
         public void cancel() {
             JSONObject result = makeResult(RESULT_CANCEL);
--- a/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/MediaPlayerManager.java
@@ -193,17 +193,17 @@ public class MediaPlayerManager extends 
             }
         };
 
     private GeckoMediaPlayer getMediaPlayerForRoute(MediaRouter.RouteInfo route) {
         try {
             if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
                 return new ChromeCast(getActivity(), route);
             }
-        } catch(Exception ex) {
+        } catch (Exception ex) {
             debug("Error handling presentation", ex);
         }
 
         return null;
     }
 
     @Override
     public void onPause() {
--- a/mobile/android/base/java/org/mozilla/gecko/NotificationHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/NotificationHelper.java
@@ -203,17 +203,17 @@ public final class NotificationHelper im
         return pi;
     }
 
     private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
         Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
         try {
             // Action name must be in query uri, otherwise buttons pending intents
             // would be collapsed.
-            if(action.has(ACTION_ID_ATTR)) {
+            if (action.has(ACTION_ID_ATTR)) {
                 builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
             } else {
                 Log.i(LOGTAG, "button event with no name");
             }
         } catch (JSONException ex) {
             Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
         }
         final Intent notificationIntent = buildNotificationIntent(message, builder);
@@ -356,17 +356,17 @@ public final class NotificationHelper im
         for (Iterator<String> i = mClearableNotifications.keySet().iterator(); i.hasNext();) {
             final String id = i.next();
             final String json = mClearableNotifications.get(id);
             i.remove();
 
             JSONObject obj;
             try {
                 obj = new JSONObject(json);
-            } catch(JSONException ex) {
+            } catch (JSONException ex) {
                 obj = new JSONObject();
             }
 
             closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR));
         }
     }
 
     public static void destroy() {
--- a/mobile/android/base/java/org/mozilla/gecko/RemoteTabsExpandableListAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/RemoteTabsExpandableListAdapter.java
@@ -148,17 +148,17 @@ public class RemoteTabsExpandableListAda
         // indicator.
         final int deviceTypeResId;
         final int textColorResId;
         final int deviceExpandedResId;
 
         if (isExpanded && !client.tabs.isEmpty()) {
             deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile;
             textColorResId = R.color.placeholder_active_grey;
-            deviceExpandedResId = showGroupIndicator ? R.drawable.arrow_down: R.drawable.home_group_collapsed;
+            deviceExpandedResId = showGroupIndicator ? R.drawable.arrow_down : R.drawable.home_group_collapsed;
         } else {
             deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop_inactive : R.drawable.sync_mobile_inactive;
             textColorResId = R.color.tabs_tray_icon_grey;
             deviceExpandedResId = showGroupIndicator ? R.drawable.home_group_collapsed : 0;
         }
 
         // Now update the UI.
         holder.nameView.setText(client.name);
--- a/mobile/android/base/java/org/mozilla/gecko/SessionParser.java
+++ b/mobile/android/base/java/org/mozilla/gecko/SessionParser.java
@@ -53,17 +53,17 @@ public abstract class SessionParser {
     abstract public void onTabRead(SessionTab tab);
 
     /**
      * Placeholder method that must be overloaded to handle closedTabs while parsing session data.
      *
      * @param closedTabs, JSONArray of recently closed tab entries.
      * @throws JSONException
      */
-    public void onClosedTabsRead(final JSONArray closedTabs) throws JSONException{
+    public void onClosedTabsRead(final JSONArray closedTabs) throws JSONException {
     }
 
     public void parse(String... sessionStrings) {
         final LinkedList<SessionTab> sessionTabs = new LinkedList<SessionTab>();
         int totalCount = 0;
         int selectedIndex = -1;
         try {
             for (String sessionString : sessionStrings) {
@@ -95,17 +95,17 @@ public abstract class SessionParser {
 
                     String title = entry.optString("title");
                     if (title.length() == 0) {
                         title = url;
                     }
 
                     totalCount++;
                     boolean selected = false;
-                    if (optSelected == i+1) {
+                    if (optSelected == i + 1) {
                         selected = true;
                         selectedIndex = totalCount;
                     }
                     sessionTabs.add(new SessionTab(title, url, selected, tab));
                 }
             }
         } catch (JSONException e) {
             Log.e(LOGTAG, "JSON error", e);
--- a/mobile/android/base/java/org/mozilla/gecko/Tab.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tab.java
@@ -758,35 +758,35 @@ public class Tab {
         mPluginViews.remove(view);
     }
 
     public View[] getPluginViews() {
         return mPluginViews.toArray(new View[mPluginViews.size()]);
     }
 
     public void addPluginLayer(Object surfaceOrView, Layer layer) {
-        synchronized(mPluginLayers) {
+        synchronized (mPluginLayers) {
             mPluginLayers.put(surfaceOrView, layer);
         }
     }
 
     public Layer getPluginLayer(Object surfaceOrView) {
-        synchronized(mPluginLayers) {
+        synchronized (mPluginLayers) {
             return mPluginLayers.get(surfaceOrView);
         }
     }
 
     public Collection<Layer> getPluginLayers() {
-        synchronized(mPluginLayers) {
+        synchronized (mPluginLayers) {
             return new ArrayList<Layer>(mPluginLayers.values());
         }
     }
 
     public Layer removePluginLayer(Object surfaceOrView) {
-        synchronized(mPluginLayers) {
+        synchronized (mPluginLayers) {
             return mPluginLayers.remove(surfaceOrView);
         }
     }
 
     public int getBackgroundColor() {
         return mBackgroundColor;
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/Tabs.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Tabs.java
@@ -86,17 +86,17 @@ public class Tabs implements GeckoEventL
             this.db = GeckoProfile.get(context).getDB();
             this.tabs = tabsInOrder;
         }
 
         @Override
         public void run() {
             try {
                 db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
-            } catch(SQLiteException e) {
+            } catch (SQLiteException e) {
                 Log.w(LOGTAG, "Error persisting local tabs", e);
             }
         }
     };
 
     private Tabs() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Tab:Added",
--- a/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ZoomedView.java
@@ -601,17 +601,17 @@ public class ZoomedView extends FrameLay
         zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
 
         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
         refreshZoomedViewSize(metrics);
         setTextInZoomFactorButton(zoomFactor);
     }
 
     private void setTextInZoomFactorButton(float zoom) {
-        final String percentageValue = Integer.toString((int) (100*zoom));
+        final String percentageValue = Integer.toString((int) (100 * zoom));
         changeZoomFactorButton.setText("- " + getResources().getString(R.string.percent, percentageValue) + " +");
     }
 
     @Override
     public void handleMessage(final String event, final JSONObject message) {
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDatabaseHelper.java
@@ -1364,17 +1364,17 @@ public final class BrowserDatabaseHelper
     @Override
     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
         debug("Upgrading browser.db: " + db.getPath() + " from " +
                 oldVersion + " to " + newVersion);
 
         // We have to do incremental upgrades until we reach the current
         // database schema version.
         for (int v = oldVersion + 1; v <= newVersion; v++) {
-            switch(v) {
+            switch (v) {
                 case 4:
                     upgradeDatabaseFrom3to4(db);
                     break;
 
                 case 7:
                     upgradeDatabaseFrom6to7(db);
                     break;
 
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -305,17 +305,17 @@ public class BrowserProvider extends Sha
 
         final String sortOrder = BrowserContract.getFrecencySortOrder(false, true);
         final long toRemove = rows - retain;
         debug("Expiring at most " + toRemove + " rows earlier than " + keepAfter + ".");
 
         final String sql;
         if (keepAfter > 0) {
             sql = "DELETE FROM " + TABLE_HISTORY + " " +
-                  "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED +") < " + keepAfter + " " +
+                  "WHERE MAX(" + History.DATE_LAST_VISITED + ", " + History.DATE_MODIFIED + ") < " + keepAfter + " " +
                   " AND " + History._ID + " IN ( SELECT " +
                     History._ID + " FROM " + TABLE_HISTORY + " " +
                     "ORDER BY " + sortOrder + " LIMIT " + toRemove +
                   ")";
         } else {
             sql = "DELETE FROM " + TABLE_HISTORY + " WHERE " + History._ID + " " +
                   "IN ( SELECT " + History._ID + " FROM " + TABLE_HISTORY + " " +
                   "ORDER BY " + sortOrder + " LIMIT " + toRemove + ")";
--- a/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/FormHistoryProvider.java
@@ -146,22 +146,22 @@ public class FormHistoryProvider extends
 
     @Override
     public void onPreUpdate(ContentValues values, Uri uri, SQLiteBridge db) { }
 
     @Override
     public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) { }
 
     @Override
-    protected String getDBName(){
+    protected String getDBName() {
         return DB_FILENAME;
     }
 
     @Override
     protected String getTelemetryPrefix() {
         return TELEMETRY_TAG;
     }
 
     @Override
-    protected int getDBVersion(){
+    protected int getDBVersion() {
         return DB_VERSION;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/HomeProvider.java
@@ -136,27 +136,27 @@ public class HomeProvider extends SQLite
         return c;
     }
 
     /**
      * SQLiteBridgeContentProvider implementation
      */
 
     @Override
-    protected String getDBName(){
+    protected String getDBName() {
         return DB_FILENAME;
     }
 
     @Override
     protected String getTelemetryPrefix() {
         return TELEMETRY_TAG;
     }
 
     @Override
-    protected int getDBVersion(){
+    protected int getDBVersion() {
         return DB_VERSION;
     }
 
     @Override
     public String getTable(Uri uri) {
         final int match = URI_MATCHER.match(uri);
         switch (match) {
             case ITEMS: {
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalURLMetadata.java
@@ -12,17 +12,16 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.Uri;
 import android.util.Log;
@@ -32,48 +31,45 @@ import android.util.LruCache;
 public class LocalURLMetadata implements URLMetadata {
     private static final String LOGTAG = "GeckoURLMetadata";
     private final Uri uriWithProfile;
 
     public LocalURLMetadata(String mProfile) {
         uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI);
     }
 
-    // This returns a list of columns in the table. It's used to simplify some loops for reading/writing data.
-    @SuppressWarnings("serial")
-    private final Set<String> getModel() {
-        return new HashSet<String>() {{
-            add(URLMetadataTable.URL_COLUMN);
-            add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
-            add(URLMetadataTable.TILE_COLOR_COLUMN);
-            add(URLMetadataTable.TOUCH_ICON_COLUMN);
-        }};
+    // A list of columns in the table. It's used to simplify some loops for reading/writing data.
+    private static final Set<String> COLUMNS;
+    static {
+        final HashSet<String> tempModel = new HashSet<>(4);
+        tempModel.add(URLMetadataTable.URL_COLUMN);
+        tempModel.add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
+        tempModel.add(URLMetadataTable.TILE_COLOR_COLUMN);
+        tempModel.add(URLMetadataTable.TOUCH_ICON_COLUMN);
+        COLUMNS = Collections.unmodifiableSet(tempModel);
     }
 
     // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
     private static final int CACHE_SIZE = 9;
     // Note: Members of this cache are unmodifiable.
     private final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
 
     /**
      * Converts a JSON object into a unmodifiable Map of known metadata properties.
      * Will throw away any properties that aren't stored in the database.
      *
      * Incoming data can include a list like: {touchIconList:{56:"http://x.com/56.png", 76:"http://x.com/76.png"}}.
      * This will then be filtered to find the most appropriate touchIcon, i.e. the closest icon size that is larger
      * than (or equal to) the preferred homescreen launcher icon size, which is then stored in the "touchIcon" property.
-     *
-     * @see #getModel() Returns the list of properties that will be stored in the database.
      */
     @Override
     public Map<String, Object> fromJSON(JSONObject obj) {
         Map<String, Object> data = new HashMap<String, Object>();
 
-        Set<String> model = getModel();
-        for (String key : model) {
+        for (String key : COLUMNS) {
             if (obj.has(key)) {
                 data.put(key, obj.optString(key));
             }
         }
 
 
         try {
             JSONObject icons;
@@ -103,20 +99,19 @@ public class LocalURLMetadata implements
     /**
      * Converts a Cursor into a unmodifiable Map of known metadata properties.
      * Will throw away any properties that aren't stored in the database.
      * Will also not iterate through multiple rows in the cursor.
      */
     private Map<String, Object> fromCursor(Cursor c) {
         Map<String, Object> data = new HashMap<String, Object>();
 
-        Set<String> model = getModel();
         String[] columns = c.getColumnNames();
         for (String column : columns) {
-            if (model.contains(column)) {
+            if (COLUMNS.contains(column)) {
                 try {
                     data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
                 } catch (Exception ex) {
                     Log.i(LOGTAG, "Error getting data for " + column, ex);
                 }
             }
         }
 
@@ -194,17 +189,17 @@ public class LocalURLMetadata implements
             }
 
             do {
                 final Map<String, Object> metadata = fromCursor(cursor);
                 final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));
 
                 data.put(url, metadata);
                 cache.put(url, metadata);
-            } while(cursor.moveToNext());
+            } while (cursor.moveToNext());
 
         } finally {
             cursor.close();
         }
 
         return Collections.unmodifiableMap(data);
     }
 
@@ -216,18 +211,17 @@ public class LocalURLMetadata implements
     @Override
     public void save(final ContentResolver cr, final String url, final Map<String, Object> data) {
         ThreadUtils.assertNotOnUiThread();
         ThreadUtils.assertNotOnGeckoThread();
 
         try {
             ContentValues values = new ContentValues();
 
-            Set<String> model = getModel();
-            for (String key : model) {
+            for (String key : COLUMNS) {
                 if (data.containsKey(key)) {
                     values.put(key, (String) data.get(key));
                 }
             }
 
             if (values.size() == 0) {
                 return;
             }
--- a/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/PasswordsProvider.java
@@ -113,27 +113,27 @@ public class PasswordsProvider extends S
 
         if (mCrashHandler != null) {
             mCrashHandler.unregister();
             mCrashHandler = null;
         }
     }
 
     @Override
-    protected String getDBName(){
+    protected String getDBName() {
         return DB_FILENAME;
     }
 
     @Override
     protected String getTelemetryPrefix() {
         return TELEMETRY_TAG;
     }
 
     @Override
-    protected int getDBVersion(){
+    protected int getDBVersion() {
         return DB_VERSION;
     }
 
     @Override
     public String getType(Uri uri) {
         final int match = URI_MATCHER.match(uri);
 
         switch (match) {
@@ -319,31 +319,31 @@ public class PasswordsProvider extends S
     @Override
     public void onPostQuery(Cursor cursor, Uri uri, SQLiteBridge db) {
         int passwordIndex = -1;
         int usernameIndex = -1;
         String profilePath = null;
 
         try {
             passwordIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_PASSWORD);
-        } catch(Exception ex) { }
+        } catch (Exception ex) { }
         try {
             usernameIndex = cursor.getColumnIndexOrThrow(Passwords.ENCRYPTED_USERNAME);
-        } catch(Exception ex) { }
+        } catch (Exception ex) { }
 
         if (passwordIndex > -1 || usernameIndex > -1) {
             MatrixBlobCursor m = (MatrixBlobCursor)cursor;
             if (cursor.moveToFirst()) {
                 do {
                     if (passwordIndex > -1) {
                         String decrypted = doCrypto(cursor.getString(passwordIndex), uri, false);;
                         m.set(passwordIndex, decrypted);
                     }
 
                     if (usernameIndex > -1) {
                         String decrypted = doCrypto(cursor.getString(usernameIndex), uri, false);
                         m.set(usernameIndex, decrypted);
                     }
-                } while(cursor.moveToNext());
+                } while (cursor.moveToNext());
             }
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/Distribution.java
@@ -347,16 +347,44 @@ public class Distribution {
         if (!descFile.exists()) {
             Log.e(LOGTAG, "Distribution directory exists, but no file named " + name);
             return null;
         }
 
         return descFile;
     }
 
+    public DistributionDescriptor getDescriptor() {
+        File descFile = getDistributionFile("preferences.json");
+        if (descFile == null) {
+            // Logging and existence checks are handled in getDistributionFile.
+            return null;
+        }
+
+        try {
+            JSONObject all = new JSONObject(FileUtils.getFileContents(descFile));
+
+            if (!all.has("Global")) {
+                Log.e(LOGTAG, "Distribution preferences.json has no Global entry!");
+                return null;
+            }
+
+            return new DistributionDescriptor(all.getJSONObject("Global"));
+
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
+            Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
+            return null;
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "Error parsing preferences.json", e);
+            Telemetry.addToHistogram(HISTOGRAM_CODE_CATEGORY, CODE_CATEGORY_MALFORMED_DISTRIBUTION);
+            return null;
+        }
+    }
+
     /**
      * Get the Android preferences from the preferences.json file, if any exist.
      * @return The preferences in a JSONObject, or an empty JSONObject if no preferences are defined.
      */
     public JSONObject getAndroidPreferences() {
         final File descFile = getDistributionFile("preferences.json");
         if (descFile == null) {
             // Logging and existence checks are handled in getDistributionFile.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/distribution/DistributionStoreCallback.java
@@ -0,0 +1,61 @@
+/*
+ * 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/.
+ */
+
+package org.mozilla.gecko.distribution;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+import org.mozilla.gecko.GeckoSharedPrefs;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A distribution ready callback that will store the distribution ID to profile-specific shared preferences.
+ */
+public class DistributionStoreCallback implements Distribution.ReadyCallback {
+    private static final String LOGTAG = "Gecko" + DistributionStoreCallback.class.getSimpleName();
+
+    public static final String PREF_DISTRIBUTION_ID = "distribution.id";
+
+    private final WeakReference<Context> contextReference;
+    private final String profileName;
+
+    public DistributionStoreCallback(final Context context, final String profileName) {
+        this.contextReference = new WeakReference<>(context);
+        this.profileName = profileName;
+    }
+
+    public void distributionNotFound() { /* nothing to do here */ }
+
+    @Override
+    public void distributionFound(final Distribution distribution) {
+        storeDistribution(distribution);
+    }
+
+    @Override
+    public void distributionArrivedLate(final Distribution distribution) {
+        storeDistribution(distribution);
+    }
+
+    private void storeDistribution(final Distribution distribution) {
+        final Context context = contextReference.get();
+        if (context == null) {
+            Log.w(LOGTAG, "Context is no longer alive, could retrieve shared prefs to store distribution");
+            return;
+        }
+
+        // While the distribution preferences are per install and not per profile, it's okay to use the
+        // profile-specific prefs because:
+        //   1) We don't really support mulitple profiles for end-users
+        //   2) The TelemetryUploadService already accesses profile-specific shared prefs so this keeps things simple.
+        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(context, profileName);
+        final Distribution.DistributionDescriptor desc = distribution.getDescriptor();
+        if (desc != null) {
+            sharedPrefs.edit().putString(PREF_DISTRIBUTION_ID, desc.id).apply();
+        }
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
@@ -291,17 +291,17 @@ public class Favicons {
                 return dispatchResult(pageURL, targetURL, result, callback);
             }
         }
 
         // No joy using in-memory resources. Go to background thread and ask the database.
         final LoadFaviconTask task =
             new LoadFaviconTask(context, pageURL, targetURL, 0, callback, targetSize, true);
         final int taskId = task.getId();
-        synchronized(loadTasks) {
+        synchronized (loadTasks) {
             loadTasks.put(taskId, task);
         }
         task.execute();
 
         return taskId;
     }
 
     public static int getSizedFaviconForPageFromLocal(Context context, final String pageURL, final OnFaviconLoadedListener callback) {
@@ -364,17 +364,17 @@ public class Favicons {
         if (TextUtils.isEmpty(pageURL)) {
             dispatchResult(null, null, null, listener);
             return NOT_LOADING;
         }
 
         final LoadFaviconTask task =
             new LoadFaviconTask(context, pageURL, faviconURL, flags, listener, targetSize, false);
         final int taskId = task.getId();
-        synchronized(loadTasks) {
+        synchronized (loadTasks) {
             loadTasks.put(taskId, task);
         }
         task.execute();
 
         return taskId;
     }
 
     public static void putFaviconInMemCache(String pageUrl, Bitmap image) {
@@ -562,17 +562,17 @@ public class Favicons {
      * @param mimeType Mime type to check.
      * @return true if the given mime type is a container type, false otherwise.
      */
     public static boolean isContainerType(String mimeType) {
         return sDecodableMimeTypes.contains(mimeType);
     }
 
     public static void removeLoadTask(int taskId) {
-        synchronized(loadTasks) {
+        synchronized (loadTasks) {
             loadTasks.delete(taskId);
         }
     }
 
     /**
      * Method to wrap FaviconCache.isFailedFavicon for use by LoadFaviconTask.
      *
      * @param faviconURL Favicon URL to check for failure.
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/LoadFaviconTask.java
@@ -376,17 +376,17 @@ public class LoadFaviconTask {
 
         if (isCancelled()) {
             return null;
         }
 
         Bitmap image;
         // Determine if there is already an ongoing task to fetch the Favicon we desire.
         // If there is, just join the queue and wait for it to finish. If not, we carry on.
-        synchronized(loadsInFlight) {
+        synchronized (loadsInFlight) {
             // Another load of the current Favicon is already underway
             LoadFaviconTask existingTask = loadsInFlight.get(faviconURL);
             if (existingTask != null && !existingTask.isCancelled()) {
                 existingTask.chainTasks(this);
                 isChaining = true;
 
                 // If we are chaining, we want to keep the first task started to do this job as the one
                 // in the hashmap so subsequent tasks will add themselves to its chaining list.
@@ -560,17 +560,17 @@ public class LoadFaviconTask {
         }
 
         Favicons.dispatchResult(pageUrl, faviconURL, scaled, listener);
     }
 
     void onCancelled() {
         Favicons.removeLoadTask(id);
 
-        synchronized(loadsInFlight) {
+        synchronized (loadsInFlight) {
             // Only remove from the hashmap if the task there is the one that's being canceled.
             // Cancellation of a task that would have chained is not interesting to the hashmap.
             final LoadFaviconTask primary = loadsInFlight.get(faviconURL);
             if (primary == this) {
                 loadsInFlight.remove(faviconURL);
                 return;
             }
             if (primary == null) {
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/cache/FaviconCache.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/cache/FaviconCache.java
@@ -446,17 +446,17 @@ public class FaviconCache {
     /**
      * Set an existing element as the most recently used element. Intended for use from read transactions. While
      * write transactions may safely use this method, it will perform slightly worse than its unsafe counterpart below.
      *
      * @param element The element that is to become the most recently used one.
      * @return true if this element already existed in the list, false otherwise. (Useful for preventing multiple-insertion.)
      */
     private boolean setMostRecentlyUsedWithinRead(FaviconCacheElement element) {
-        synchronized(reorderingLock) {
+        synchronized (reorderingLock) {
             boolean contained = ordering.remove(element);
             ordering.offer(element);
             return contained;
         }
     }
 
     /**
      * Functionally equivalent to setMostRecentlyUsedWithinRead, but operates without taking the reordering semaphore.
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/decoders/IconDirectoryEntry.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/decoders/IconDirectoryEntry.java
@@ -79,17 +79,17 @@ public class IconDirectoryEntry implemen
 
         // Fail if the entry describes a region outside the buffer.
         if (payloadOffset < 0 || entryLength < 0 || payloadOffset + entryLength > regionOffset + regionLength) {
             return getErroneousEntry();
         }
 
         // Extract the image dimensions.
         int imageWidth = buffer[entryOffset] & 0xFF;
-        int imageHeight = buffer[entryOffset+1] & 0xFF;
+        int imageHeight = buffer[entryOffset + 1] & 0xFF;
 
         // Because Microsoft, a size value of zero represents an image size of 256.
         if (imageWidth == 0) {
             imageWidth = 256;
         }
 
         if (imageHeight == 0) {
             imageHeight = 256;
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/Axis.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/Axis.java
@@ -130,17 +130,17 @@ abstract class Axis {
             @Override public void finish() {
                 setPrefs(mPrefs);
             }
         });
     }
 
     static final float MS_PER_FRAME = 1000.0f / 60.0f;
     static final long NS_PER_FRAME = Math.round(1000000000f / 60f);
-    private static final float FRAMERATE_MULTIPLIER = (1000f/60f) / MS_PER_FRAME;
+    private static final float FRAMERATE_MULTIPLIER = (1000f / 60f) / MS_PER_FRAME;
     private static final int FLING_VELOCITY_POINTS = 8;
 
     //  The values we use for friction are based on a 16.6ms frame, adjust them to currentNsPerFrame:
     static float getFrameAdjustedFriction(float baseFriction, long currentNsPerFrame) {
         float framerateMultiplier = (float)currentNsPerFrame / NS_PER_FRAME;
         return (float)Math.pow(Math.E, (Math.log(baseFriction) / framerateMultiplier));
     }
 
@@ -260,31 +260,31 @@ abstract class Axis {
 
         return (3 * y1)
              + t * (6 * y2 - 12 * y1)
              + t * t * (9 * y1 - 9 * y2 + 3);
     }
 
     // Calculates and returns the value of the bezier curve with the given parameter t and control points p1 and p2
     float cubicBezier(float p1, float p2, float t) {
-        return (3 * t * (1-t) * (1-t) * p1)
-             + (3 * t * t * (1-t) * p2)
+        return (3 * t * (1 - t) * (1 - t) * p1)
+             + (3 * t * t * (1 - t) * p2)
              + (t * t * t);
     }
 
     // Responsible for mapping the physical velocity to a the velocity obtained after applying bezier curve (with control points (X1,Y1) and (X2,Y2))
     float flingCurve(float By) {
         int ni = FLING_CURVE_NEWTON_ITERATIONS;
         float[] guess = new float[ni];
         float y1 = FLING_CURVE_FUNCTION_Y1;
         float y2 = FLING_CURVE_FUNCTION_Y2;
         guess[0] = By;
 
         for (int i = 1; i < ni; i++) {
-            guess[i] = guess[i-1] - (cubicBezier(y1, y2, guess[i-1]) - By) / getSlope(guess[i-1]);
+            guess[i] = guess[i - 1] - (cubicBezier(y1, y2, guess[i - 1]) - By) / getSlope(guess[i - 1]);
         }
         // guess[4] is the final approximate root the cubic equation.
         float t = guess[4];
 
         float x1 = FLING_CURVE_FUNCTION_X1;
         float x2 = FLING_CURVE_FUNCTION_X2;
         return cubicBezier(x1, x2, t);
     }
@@ -404,17 +404,17 @@ abstract class Axis {
         }
         float average = 0;
         for (int i = 0; i < usablePoints; i++) {
             average += mRecentVelocities[i];
         }
         return average / usablePoints;
     }
 
-    float accelerate(float velocity, float lastFlingVelocity){
+    float accelerate(float velocity, float lastFlingVelocity) {
         return (FLING_ACCEL_BASE_MULTIPLIER * velocity + FLING_ACCEL_SUPPLEMENTAL_MULTIPLIER * lastFlingVelocity);
     }
 
     void startFling(boolean stopped) {
         mDisableSnap = mSubscroller.scrolling();
 
         if (stopped) {
             mFlingState = FlingStates.STOPPED;
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/BitmapUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/BitmapUtils.java
@@ -219,44 +219,44 @@ public final class BitmapUtils {
         return decodeUrl(uri.toString());
     }
 
     public static Bitmap decodeUrl(String urlString) {
         URL url;
 
         try {
             url = new URL(urlString);
-        } catch(MalformedURLException e) {
+        } catch (MalformedURLException e) {
             Log.w(LOGTAG, "decodeUrl: malformed URL " + urlString);
             return null;
         }
 
         return decodeUrl(url);
     }
 
     public static Bitmap decodeUrl(URL url) {
         InputStream stream = null;
 
         try {
             stream = url.openStream();
-        } catch(IOException e) {
+        } catch (IOException e) {
             Log.w(LOGTAG, "decodeUrl: IOException downloading " + url);
             return null;
         }
 
         if (stream == null) {
             Log.w(LOGTAG, "decodeUrl: stream not found downloading " + url);
             return null;
         }
 
         Bitmap bitmap = decodeStream(stream);
 
         try {
             stream.close();
-        } catch(IOException e) {
+        } catch (IOException e) {
             Log.w(LOGTAG, "decodeUrl: IOException closing stream " + url, e);
         }
 
         return bitmap;
     }
 
     public static Bitmap decodeResource(Context context, int id) {
         return decodeResource(context, id, null);
@@ -329,19 +329,19 @@ public final class BitmapUtils {
         }
       }
 
       // maxBin may never get updated if the image holds only transparent and/or black/white pixels.
       if (maxBin < 0)
         return Color.argb(255, 255, 255, 255);
 
       // Return a color with the average hue/saturation/value of the bin with the most colors.
-      hsv[0] = sumHue[maxBin]/colorBins[maxBin];
-      hsv[1] = sumSat[maxBin]/colorBins[maxBin];
-      hsv[2] = sumVal[maxBin]/colorBins[maxBin];
+      hsv[0] = sumHue[maxBin] / colorBins[maxBin];
+      hsv[1] = sumSat[maxBin] / colorBins[maxBin];
+      hsv[2] = sumVal[maxBin] / colorBins[maxBin];
       return Color.HSVToColor(hsv);
     }
 
     /**
      * Decodes a bitmap from a Base64 data URI.
      *
      * @param dataURI a Base64-encoded data URI string
      * @return        the decoded bitmap, or null if the data URI is invalid
@@ -401,34 +401,34 @@ public final class BitmapUtils {
 
         final String scheme = resourceUrl.getScheme();
         if ("drawable".equals(scheme)) {
             String resource = resourceUrl.getSchemeSpecificPart();
             resource = resource.substring(resource.lastIndexOf('/') + 1);
 
             try {
                 return Integer.parseInt(resource);
-            } catch(NumberFormatException ex) {
+            } catch (NumberFormatException ex) {
                 // This isn't a resource id, try looking for a string
             }
 
             try {
                 final Class<R.drawable> drawableClass = R.drawable.class;
                 final Field f = drawableClass.getField(resource);
                 icon = f.getInt(null);
             } catch (final NoSuchFieldException e1) {
 
                 // just means the resource doesn't exist for fennec. Check in Android resources
                 try {
                     final Class<android.R.drawable> drawableClass = android.R.drawable.class;
                     final Field f = drawableClass.getField(resource);
                     icon = f.getInt(null);
                 } catch (final NoSuchFieldException e2) {
                     // This drawable doesn't seem to exist...
-                } catch(Exception e3) {
+                } catch (Exception e3) {
                     Log.i(LOGTAG, "Exception getting drawable", e3);
                 }
 
             } catch (Exception e4) {
               Log.i(LOGTAG, "Exception getting drawable", e4);
             }
 
             resourceUrl = null;
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -490,17 +490,17 @@ class GeckoLayerClient implements LayerV
         // precision updates.
         if (!lowPrecision) {
             if (!FloatUtils.fuzzyEquals(resolution, mProgressiveUpdateDisplayPort.resolution) ||
                 !FloatUtils.fuzzyEquals(x, mProgressiveUpdateDisplayPort.getLeft()) ||
                 !FloatUtils.fuzzyEquals(y, mProgressiveUpdateDisplayPort.getTop()) ||
                 !FloatUtils.fuzzyEquals(x + width, mProgressiveUpdateDisplayPort.getRight()) ||
                 !FloatUtils.fuzzyEquals(y + height, mProgressiveUpdateDisplayPort.getBottom())) {
                 mProgressiveUpdateDisplayPort =
-                    new DisplayPortMetrics(x, y, x+width, y+height, resolution);
+                    new DisplayPortMetrics(x, y, x + width, y + height, resolution);
             }
         }
 
         // If we're not doing low precision draws and we're about to
         // checkerboard, enable low precision drawing.
         if (!lowPrecision && !mProgressiveUpdateWasInDanger) {
             if (DisplayPortCalculator.aboutToCheckerboard(viewportMetrics,
                   mPanZoomController.getVelocityVector(), mProgressiveUpdateDisplayPort)) {
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/ImmutableViewportMetrics.java
@@ -114,17 +114,17 @@ public class ImmutableViewportMetrics {
     public RectF getViewport() {
         return new RectF(viewportRectLeft,
                          viewportRectTop,
                          viewportRectRight(),
                          viewportRectBottom());
     }
 
     public RectF getCssViewport() {
-        return RectUtils.scale(getViewport(), 1/zoomFactor);
+        return RectUtils.scale(getViewport(), 1 / zoomFactor);
     }
 
     public RectF getPageRect() {
         return new RectF(pageRectLeft, pageRectTop, pageRectRight, pageRectBottom);
     }
 
     public float getPageWidth() {
         return pageRectRight - pageRectLeft;
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/JavaPanZoomController.java
@@ -52,17 +52,17 @@ class JavaPanZoomController
 
     // Angle from axis within which we stay axis-locked
     private static final double AXIS_LOCK_ANGLE = Math.PI / 6.0; // 30 degrees
 
     // Axis-lock breakout angle
     private static final double AXIS_BREAKOUT_ANGLE = Math.PI / 8.0;
 
     // The distance the user has to pan before we consider breaking out of a locked axis
-    public static final float AXIS_BREAKOUT_THRESHOLD = 1/32f * GeckoAppShell.getDpi();
+    public static final float AXIS_BREAKOUT_THRESHOLD = 1 / 32f * GeckoAppShell.getDpi();
 
     // The maximum amount we allow you to zoom into a page
     private static final float MAX_ZOOM = 4.0f;
 
     // The maximum amount we would like to scroll with the mouse
     private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
 
     // The maximum zoom factor adjustment per frame of the AUTONAV animation
@@ -205,18 +205,18 @@ class JavaPanZoomController
             MESSAGE_TOUCH_LISTENER);
         mSubscroller.destroy();
         mTouchEventHandler.destroy();
     }
 
     private final static float easeOut(float t) {
         // ease-out approx.
         // -(t-1)^2+1
-        t = t-1;
-        return -t*t+1;
+        t = t - 1;
+        return -t * t + 1;
     }
 
     private void setState(PanZoomState state) {
         if (state != mState) {
             GeckoAppShell.notifyObservers("PanZoom:StateChange", state.toString());
             mState = state;
 
             // Let the target know we've finished with it (for now)
@@ -261,19 +261,19 @@ class JavaPanZoomController
                 RectF cssPageRect = metrics.getCssPageRect();
 
                 RectF viewableRect = metrics.getCssViewport();
                 float y = viewableRect.top;
                 // attempt to keep zoom keep focused on the center of the viewport
                 float newHeight = viewableRect.height() * cssPageRect.width() / viewableRect.width();
                 float dh = viewableRect.height() - newHeight; // increase in the height
                 final RectF r = new RectF(0.0f,
-                                    y + dh/2,
+                                    y + dh / 2,
                                     cssPageRect.width(),
-                                    y + dh/2 + newHeight);
+                                    y + dh / 2 + newHeight);
                 if (message.optBoolean("animate", true)) {
                     mTarget.post(new Runnable() {
                         @Override
                         public void run() {
                             animatedZoomTo(r);
                         }
                     });
                 } else {
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomController.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/PanZoomController.java
@@ -11,20 +11,20 @@ import org.mozilla.gecko.EventDispatcher
 import android.graphics.PointF;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
 
 public interface PanZoomController {
     // The distance the user has to pan before we recognize it as such (e.g. to avoid 1-pixel pans
     // between the touch-down and touch-up of a click). In units of density-independent pixels.
-    public static final float PAN_THRESHOLD = 1/16f * GeckoAppShell.getDpi();
+    public static final float PAN_THRESHOLD = 1 / 16f * GeckoAppShell.getDpi();
 
     // Threshold for sending touch move events to content
-    public static final float CLICK_THRESHOLD = 1/50f * GeckoAppShell.getDpi();
+    public static final float CLICK_THRESHOLD = 1 / 50f * GeckoAppShell.getDpi();
 
     static class Factory {
         static PanZoomController create(PanZoomTarget target, View view, EventDispatcher dispatcher) {
             if (org.mozilla.gecko.AppConstants.MOZ_ANDROID_APZ) {
                 return new NativePanZoomController(target, view);
             } else {
                 return new JavaPanZoomController(target, view, dispatcher);
             }
--- a/mobile/android/base/java/org/mozilla/gecko/gfx/PluginLayer.java
+++ b/mobile/android/base/java/org/mozilla/gecko/gfx/PluginLayer.java
@@ -130,20 +130,20 @@ public class PluginLayer extends Layer {
 
             mMaxDimension = maxDimension;
             reset(rect);
         }
 
         private void clampToMaxSize() {
             if (width > mMaxDimension || height > mMaxDimension) {
                 if (width > height) {
-                    height = Math.round(((float)height/ width) * mMaxDimension);
+                    height = Math.round(((float) height / width) * mMaxDimension);
                     width = mMaxDimension;
                 } else {
-                    width = Math.round(((float)width/ height) * mMaxDimension);
+                    width = Math.round(((float) width / height) * mMaxDimension);
                     height = mMaxDimension;
                 }
             }
         }
 
         public void reset(RectF rect) {
             mRect = rect;
         }
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListAdapter.java
@@ -191,17 +191,17 @@ class BookmarksListAdapter extends Multi
 
     private boolean isCurrentFolder(FolderInfo folderInfo) {
         return (mParentStack.size() > 0 &&
                 mParentStack.peek().id == folderInfo.id);
     }
 
     public void swapCursor(Cursor c, FolderInfo folderInfo, RefreshType refreshType) {
         updateOpenFolderType(folderInfo);
-        switch(refreshType) {
+        switch (refreshType) {
             case PARENT:
                 if (!isCurrentFolder(folderInfo)) {
                     mParentStack.removeFirst();
                 }
                 break;
 
             case CHILD:
                 if (!isCurrentFolder(folderInfo)) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BookmarksListView.java
@@ -24,17 +24,17 @@ import android.widget.AdapterView;
 import android.widget.HeaderViewListAdapter;
 import android.widget.ListAdapter;
 import org.mozilla.gecko.util.NetworkUtils;
 
 /**
  * A ListView of bookmarks.
  */
 public class BookmarksListView extends HomeListView
-                               implements AdapterView.OnItemClickListener{
+                               implements AdapterView.OnItemClickListener {
     public static final String LOGTAG = "GeckoBookmarksListView";
 
     public BookmarksListView(Context context) {
         this(context, null);
     }
 
     public BookmarksListView(Context context, AttributeSet attrs) {
         this(context, attrs, R.attr.bookmarksListViewStyle);
--- a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
@@ -985,17 +985,17 @@ public class BrowserSearch extends HomeF
             String actualQuery = BrowserContract.SearchHistory.QUERY + " LIKE ?";
             String[] queryArgs = new String[] { '%' + mSearchTerm + '%' };
 
             // For deduplication, the worst case is that all the first NETWORK_SUGGESTION_MAX history suggestions are duplicates
             // of search engine suggestions, and the there is a duplicate for the search term itself. A duplicate of the
             // search term  can occur if the user has previously searched for the same thing.
             final int maxSavedSuggestions = NETWORK_SUGGESTION_MAX + 1 + getContext().getResources().getInteger(R.integer.max_saved_suggestions);
 
-            final String sortOrderAndLimit = BrowserContract.SearchHistory.DATE +" DESC LIMIT " + maxSavedSuggestions;
+            final String sortOrderAndLimit = BrowserContract.SearchHistory.DATE + " DESC LIMIT " + maxSavedSuggestions;
             final Cursor result =  cr.query(BrowserContract.SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit);
 
             if (result == null) {
                 return new ArrayList<>();
             }
 
             final ArrayList<String> savedSuggestions = new ArrayList<>();
             try {
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -213,17 +213,17 @@ public class CombinedHistoryAdapter exte
      * @param position position in the adapter
      * @return position of the item in the data structure
      */
     private int transformAdapterPositionForDataStructure(ItemType type, int position) {
         if (type == ItemType.CLIENT) {
             return position;
         } else if (type == ItemType.SECTION_HEADER) {
             return position - remoteClients.size();
-        } else if (type == ItemType.HISTORY){
+        } else if (type == ItemType.HISTORY) {
             return position - remoteClients.size() - getHeadersBefore(position);
         } else {
             return position;
         }
     }
 
     public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
         final ItemType itemType = getItemTypeForPosition(position);
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
@@ -81,17 +81,17 @@ public class CombinedHistoryRecyclerView
         mDialogBuilder = builder;
     }
 
     @Override
     public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         final int viewType = getAdapter().getItemViewType(position);
         final CombinedHistoryAdapter.ItemType itemType = CombinedHistoryAdapter.ItemType.viewTypeToItemType(viewType);
 
-        switch(itemType) {
+        switch (itemType) {
             case CLIENT:
                 mOnPanelLevelChangeListener.onPanelLevelChange(PanelLevel.CHILD);
                 ((CombinedHistoryAdapter) getAdapter()).showChildView(position);
                 break;
             case HIDDEN_DEVICES:
                 if (mDialogBuilder != null) {
                     mDialogBuilder.createAndShowDialog(((CombinedHistoryAdapter) getAdapter()).getHiddenClients());
                 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/DynamicPanel.java
@@ -186,17 +186,17 @@ public class DynamicPanel extends HomeFr
     private void createPanelLayout() {
         final ContextMenuRegistry contextMenuRegistry = new ContextMenuRegistry() {
             @Override
             public void register(View view) {
                 registerForContextMenu(view);
             }
         };
 
-        switch(mPanelConfig.getLayoutType()) {
+        switch (mPanelConfig.getLayoutType()) {
             case FRAME:
                 final PanelDatasetHandler datasetHandler = new PanelDatasetHandler();
                 mPanelLayout = new FramePanelLayout(getActivity(), mPanelConfig, datasetHandler,
                         mUrlOpenListener, contextMenuRegistry);
                 break;
 
             default:
                 throw new IllegalStateException("Unrecognized layout type in DynamicPanel");
@@ -210,17 +210,17 @@ public class DynamicPanel extends HomeFr
      * Lazily creates layout for authentication UI.
      */
     private void createPanelAuthLayout() {
         mPanelAuthLayout = new PanelAuthLayout(getActivity(), mPanelConfig);
         mView.addView(mPanelAuthLayout, 0);
     }
 
     private void setUIMode(UIMode mode) {
-        switch(mode) {
+        switch (mode) {
             case PANEL:
                 if (mPanelAuthLayout != null) {
                     mPanelAuthLayout.setVisibility(View.GONE);
                 }
                 if (mPanelLayout == null) {
                     createPanelLayout();
                 }
                 mPanelLayout.setVisibility(View.VISIBLE);
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
@@ -1628,17 +1628,17 @@ public final class HomeConfig {
         mBackend.setOnReloadListener(listener);
     }
 
     public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
         return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
     }
 
     public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) {
-        switch(panelType) {
+        switch (panelType) {
         case TOP_SITES:
             return R.string.home_top_sites_title;
 
         case BOOKMARKS:
             return R.string.bookmarks_title;
 
         case COMBINED_HISTORY:
         case HISTORY:
@@ -1654,17 +1654,17 @@ public final class HomeConfig {
             return R.string.recent_tabs_title;
 
         default:
             throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
         }
     }
 
     public static String getIdForBuiltinPanelType(PanelType panelType) {
-        switch(panelType) {
+        switch (panelType) {
         case TOP_SITES:
             return TOP_SITES_PANEL_ID;
 
         case BOOKMARKS:
             return BOOKMARKS_PANEL_ID;
 
         case HISTORY:
             return HISTORY_PANEL_ID;
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -167,21 +167,25 @@ public class HomeConfigPrefsBackend impl
             final PanelConfig panelConfig = new PanelConfig(panelObj);
             final PanelType type = panelConfig.getType();
             if (type == PanelType.HISTORY) {
                 historyIndex = i;
                 historyFlags = panelConfig.getFlags();
             } else if (type == PanelType.REMOTE_TABS) {
                 syncIndex = i;
                 syncFlags = panelConfig.getFlags();
+            } else if (type == PanelType.COMBINED_HISTORY) {
+                // Partial landing of bug 1220928 combined the History and Sync panels of users who didn't
+                // have home panel customizations (including new users), thus they don't this migration.
+                return jsonPanels;
             }
         }
 
         if (historyIndex == -1 || syncIndex == -1) {
-            throw new IllegalArgumentException("Missing default panels");
+            throw new IllegalArgumentException("Expected both History and Sync panels to be present prior to Combined History.");
         }
 
         PanelConfig newPanel;
         int replaceIndex;
         int removeIndex;
         if (historyFlags.contains(PanelConfig.Flags.DISABLED_PANEL) && !syncFlags.contains(PanelConfig.Flags.DISABLED_PANEL)) {
             // Replace the Sync panel if it's visible and the History panel is disabled.
             replaceIndex = syncIndex;
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeFragment.java
@@ -50,17 +50,17 @@ import android.view.View;
 /**
  * HomeFragment is an empty fragment that can be added to the HomePager.
  * Subclasses can add their own views.
  * <p>
  * The containing activity <b>must</b> implement {@link OnUrlOpenListener}.
  */
 public abstract class HomeFragment extends Fragment {
     // Log Tag.
-    private static final String LOGTAG="GeckoHomeFragment";
+    private static final String LOGTAG = "GeckoHomeFragment";
 
     // Share MIME type.
     protected static final String SHARE_MIME_TYPE = "text/plain";
 
     // Default value for "can load" hint
     static final boolean DEFAULT_CAN_LOAD_HINT = false;
 
     // Whether the fragment can load its content or not
@@ -373,17 +373,17 @@ public abstract class HomeFragment exten
 
             if (mPosition > -1) {
                 mDB.unpinSite(cr, mPosition);
                 if (mDB.hideSuggestedSite(mUrl)) {
                     cr.notifyChange(SuggestedSites.CONTENT_URI, null);
                 }
             }
 
-            switch(mType) {
+            switch (mType) {
                 case BOOKMARKS:
                     Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.CONTEXT_MENU, "bookmark");
                     mDB.removeBookmarksWithURL(cr, mUrl);
 
                     SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(mContext);
                     if (rch.isURLCached(mUrl)) {
                         ReadingListHelper.removeCachedReaderItem(mUrl, mContext);
                     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomePanelsManager.java
@@ -321,27 +321,27 @@ public class HomePanelsManager implement
 
         final Object panelRequestLock = new Object();
         final List<PanelInfo> latestPanelInfos = new ArrayList<PanelInfo>();
 
         final PanelInfoManager pm = new PanelInfoManager();
         pm.requestPanelsById(ids, new RequestCallback() {
             @Override
             public void onComplete(List<PanelInfo> panelInfos) {
-                synchronized(panelRequestLock) {
+                synchronized (panelRequestLock) {
                     latestPanelInfos.addAll(panelInfos);
                     Log.d(LOGTAG, "executeRefresh: fetched panel infos: " + panelInfos.size());
 
                     panelRequestLock.notifyAll();
                 }
             }
         });
 
         try {
-            synchronized(panelRequestLock) {
+            synchronized (panelRequestLock) {
                 panelRequestLock.wait(PANEL_INFO_TIMEOUT_MSEC);
 
                 Log.d(LOGTAG, "executeRefresh: done fetching panel infos");
                 refreshFromPanelInfos(editor, latestPanelInfos);
             }
         } catch (InterruptedException e) {
             Log.e(LOGTAG, "Failed to fetch panels from gecko", e);
         }
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelInfoManager.java
@@ -70,17 +70,17 @@ public class PanelInfoManager implements
      *
      * @param ids list of panel ids to be fetched. A null value will fetch all
      *        available panels.
      * @param callback onComplete will be called on the UI thread.
      */
     public void requestPanelsById(Set<String> ids, RequestCallback callback) {
         final int requestId = sRequestId.getAndIncrement();
 
-        synchronized(sCallbacks) {
+        synchronized (sCallbacks) {
             // If there are no pending callbacks, register the event listener.
             if (sCallbacks.size() == 0) {
                 EventDispatcher.getInstance().registerGeckoThreadListener(this,
                     "HomePanels:Data");
             }
             sCallbacks.put(requestId, callback);
         }
 
@@ -126,17 +126,17 @@ public class PanelInfoManager implements
             for (int i = 0; i < count; i++) {
                 final PanelInfo panelInfo = getPanelInfoFromJSON(panels.getJSONObject(i));
                 panelInfos.add(panelInfo);
             }
 
             final RequestCallback callback;
             final int requestId = message.getInt("requestId");
 
-            synchronized(sCallbacks) {
+            synchronized (sCallbacks) {
                 callback = sCallbacks.get(requestId);
                 sCallbacks.delete(requestId);
 
                 // Unregister the event listener if there are no more pending callbacks.
                 if (sCallbacks.size() == 0) {
                     EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
                         "HomePanels:Data");
                 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelItemView.java
@@ -114,17 +114,17 @@ class PanelItemView extends LinearLayout
 
     private static class IconItemView extends PanelItemView {
         private IconItemView(Context context) {
             super(context, R.layout.panel_icon_item);
         }
     }
 
     public static PanelItemView create(Context context, ItemType itemType) {
-        switch(itemType) {
+        switch (itemType) {
             case ARTICLE:
                 return new ArticleItemView(context);
 
             case IMAGE:
                 return new ImageItemView(context);
 
             case ICON:
                 return new IconItemView(context);
--- a/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/PanelLayout.java
@@ -359,17 +359,17 @@ abstract class PanelLayout extends Frame
         ViewState viewState = mViewStates.get(viewConfig.getIndex());
         if (viewState == null) {
             viewState = new ViewState(viewConfig);
             mViewStates.put(viewConfig.getIndex(), viewState);
         }
 
         View view = viewState.getView();
         if (view == null) {
-            switch(viewConfig.getType()) {
+            switch (viewConfig.getType()) {
                 case LIST:
                     view = new PanelListView(getContext(), viewConfig);
                     break;
 
                 case GRID:
                     view = new PanelRecyclerView(getContext(), viewConfig);
                     break;
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
@@ -177,20 +177,20 @@ class SearchEngineRow extends AnimatedHe
         if (TextUtils.isEmpty(pattern)) {
             return;
         }
 
         final int patternLength = pattern.length();
 
         int indexOfMatch = 0;
         int lastIndexOfMatch = 0;
-        while(indexOfMatch != -1) {
+        while (indexOfMatch != -1) {
             indexOfMatch = string.indexOf(pattern, lastIndexOfMatch);
             lastIndexOfMatch = indexOfMatch + patternLength;
-            if(indexOfMatch != -1) {
+            if (indexOfMatch != -1) {
                 mOccurrences.add(indexOfMatch);
             }
         }
     }
 
     /**
      * Sets the content for the suggestion view.
      *
@@ -262,17 +262,17 @@ class SearchEngineRow extends AnimatedHe
     public void setOnSearchListener(OnSearchListener listener) {
         mSearchListener = listener;
     }
 
     public void setOnEditSuggestionListener(OnEditSuggestionListener listener) {
         mEditSuggestionListener = listener;
     }
 
-    private void bindSuggestionView(String suggestion, boolean animate, int recycledSuggestionCount, Integer previousSuggestionChildIndex, boolean isUserSavedSearch, String telemetryTag){
+    private void bindSuggestionView(String suggestion, boolean animate, int recycledSuggestionCount, Integer previousSuggestionChildIndex, boolean isUserSavedSearch, String telemetryTag) {
         final View suggestionItem;
 
         // Reuse suggestion views from recycled view, if possible.
         if (previousSuggestionChildIndex + 1 < recycledSuggestionCount) {
             suggestionItem = mSuggestionView.getChildAt(previousSuggestionChildIndex + 1);
             suggestionItem.setVisibility(View.VISIBLE);
         } else {
             suggestionItem = mInflater.inflate(R.layout.suggestion_item, null);
--- a/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TopSitesPanel.java
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
 import static org.mozilla.gecko.db.URLMetadataTable.TILE_COLOR_COLUMN;
 import static org.mozilla.gecko.db.URLMetadataTable.TILE_IMAGE_URL_COLUMN;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
@@ -777,26 +778,28 @@ public class TopSitesPanel extends HomeF
 
             return new ThumbnailInfo(imageUrl, bgColor);
         }
     }
 
     /**
      * An AsyncTaskLoader to load the thumbnails from a cursor.
      */
-    @SuppressWarnings("serial")
     static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, ThumbnailInfo>> {
         private final BrowserDB mDB;
         private Map<String, ThumbnailInfo> mThumbnailInfos;
         private final ArrayList<String> mUrls;
 
-        private static final ArrayList<String> COLUMNS = new ArrayList<String>() {{
-            add(TILE_IMAGE_URL_COLUMN);
-            add(TILE_COLOR_COLUMN);
-        }};
+        private static final List<String> COLUMNS;
+        static {
+            final ArrayList<String> tempColumns = new ArrayList<>(2);
+            tempColumns.add(TILE_IMAGE_URL_COLUMN);
+            tempColumns.add(TILE_COLOR_COLUMN);
+            COLUMNS = Collections.unmodifiableList(tempColumns);
+        }
 
         public ThumbnailsLoader(Context context, ArrayList<String> urls) {
             super(context);
             mUrls = urls;
             mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
--- a/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
+++ b/mobile/android/base/java/org/mozilla/gecko/javaaddons/JavaAddonManagerV1.java
@@ -34,17 +34,17 @@ public class JavaAddonManagerV1 implemen
 
     private static JavaAddonManagerV1 sInstance;
 
     // Protected by static synchronized.
     private Context mApplicationContext;
 
     private final org.mozilla.gecko.EventDispatcher mDispatcher;
 
-    // Protected by synchronized(this).
+    // Protected by synchronized (this).
     private final Map<String, EventDispatcherImpl> mGUIDToDispatcherMap = new HashMap<>();
 
     public static synchronized JavaAddonManagerV1 getInstance() {
         if (sInstance == null) {
             sInstance = new JavaAddonManagerV1();
         }
         return sInstance;
     }
@@ -148,17 +148,17 @@ public class JavaAddonManagerV1 implemen
      * likely hold indirect instances through its wrapping map, since the instance will probably
      * register event listeners that hold a reference to itself.  When these listeners are
      * unregistered, any link will be broken, allowing the instances to be garbage collected.
      */
     private class EventDispatcherImpl implements JavaAddonInterfaceV1.EventDispatcher {
         private final String guid;
         private final String dexFileName;
 
-        // Protected by synchronized(this).
+        // Protected by synchronized (this).
         private final Map<JavaAddonInterfaceV1.EventListener, Pair<NativeEventListener, String[]>> mListenerToWrapperMap = new IdentityHashMap<>();
 
         public EventDispatcherImpl(String guid, String dexFileName) {
             this.guid = guid;
             this.dexFileName = dexFileName;
         }
 
         protected class ListenerWrapper implements NativeEventListener {
--- a/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
+++ b/mobile/android/base/java/org/mozilla/gecko/lwt/LightweightTheme.java
@@ -138,17 +138,17 @@ public class LightweightTheme implements
                 stream = mApplication.getAssets().open(url.substring(ASSETS_PREFIX.length()));
                 return BitmapFactory.decodeStream(stream);
             } catch (IOException e) {
                 return null;
             } finally {
                 if (stream != null) {
                     try {
                         stream.close();
-                    } catch (IOException e) {}
+                    } catch (IOException e) { }
                 }
             }
         }
 
         private void onBitmapLoaded(final Bitmap bitmap) {
             ThreadUtils.postToUiThread(new Runnable() {
                 @Override
                 public void run() {
@@ -244,17 +244,17 @@ public class LightweightTheme implements
             // Malformed or missing color.
             // Default to TRANSPARENT.
             mColor = Color.TRANSPARENT;
         }
 
         // Calculate the luminance to determine if it's a light or a dark theme.
         double luminance = (0.2125 * ((mColor & 0x00FF0000) >> 16)) +
                            (0.7154 * ((mColor & 0x0000FF00) >> 8)) +
-                           (0.0721 * (mColor &0x000000FF));
+                           (0.0721 * (mColor & 0x000000FF));
         mIsLight = luminance > 110;
 
         // The bitmap image might be smaller than the device's width.
         // If it's smaller, fill the extra space on the left with the dominant color.
         if (bitmapWidth >= maxWidth) {
             mBitmap = Bitmap.createBitmap(bitmap, bitmapWidth - maxWidth, 0, maxWidth, bitmapHeight);
         } else {
             Paint paint = new Paint();
@@ -366,17 +366,17 @@ public class LightweightTheme implements
             }
 
             parent = curView.getParent();
 
             if (parent instanceof View) {
                 curView = (View) parent;
             }
 
-        } while(parent instanceof View);
+        } while (parent instanceof View);
 
         // Adjust the coordinates for the offset.
         left -= offsetX;
         right -= offsetX;
         top -= offsetY;
         bottom -= offsetY;
 
         // The either the required height may be less than the available image height or more than it.
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
@@ -67,12 +67,12 @@ public class MenuPopup extends PopupWind
     public void showAsDropDown(View anchor) {
         // Set a height, so that the popup will not be displayed below the bottom of the screen.
         // We use the exact height of the internal content, which is the technique described in
         // http://stackoverflow.com/a/7698709
         setHeight(mPanel.getHeight());
 
         // Attempt to align the center of the popup with the center of the anchor. If the anchor is
         // near the edge of the screen, the popup will just align with the edge of the screen.
-        final int xOffset = anchor.getWidth()/2 - mPopupWidth/2;
+        final int xOffset = anchor.getWidth() / 2 - mPopupWidth / 2;
         showAsDropDown(anchor, xOffset, -mYOffset);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/mozglue/GeckoLoader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/mozglue/GeckoLoader.java
@@ -66,17 +66,17 @@ public final class GeckoLoader {
                 return;
             }
 
             StringBuilder pluginSearchPath = new StringBuilder();
             for (int i = 0; i < pluginDirs.length; i++) {
                 pluginSearchPath.append(pluginDirs[i]);
                 pluginSearchPath.append(":");
             }
-            putenv("MOZ_PLUGIN_PATH="+pluginSearchPath);
+            putenv("MOZ_PLUGIN_PATH=" + pluginSearchPath);
 
             File pluginDataDir = context.getDir("plugins", 0);
             putenv("ANDROID_PLUGIN_DATADIR=" + pluginDataDir.getPath());
 
             File pluginPrivateDataDir = context.getDir("plugins_private", 0);
             putenv("ANDROID_PLUGIN_DATADIR_PRIVATE=" + pluginPrivateDataDir.getPath());
 
         } catch (Exception ex) {
--- a/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java
+++ b/mobile/android/base/java/org/mozilla/gecko/notifications/WhatsNewReceiver.java
@@ -35,17 +35,17 @@ public class WhatsNewReceiver extends Br
     @Override
     public void onReceive(Context context, Intent intent) {
         if (ACTION_NOTIFICATION_CANCELLED.equals(intent.getAction())) {
             Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.NOTIFICATION, EXTRA_WHATSNEW_NOTIFICATION);
             return;
         }
 
         final String dataString = intent.getDataString();
-        if (TextUtils.isEmpty(dataString) || !dataString.contains(AppConstants.ANDROID_PACKAGE_NAME)){
+        if (TextUtils.isEmpty(dataString) || !dataString.contains(AppConstants.ANDROID_PACKAGE_NAME)) {
             return;
         }
 
         if (!SwitchBoard.isInExperiment(context, Experiments.WHATSNEW_NOTIFICATION)) {
             return;
         }
 
         if (!isPreferenceEnabled(context)) {
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -87,16 +87,17 @@ import android.widget.AdapterView;
 import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.ListAdapter;
 import android.widget.ListView;
 
 import org.json.JSONObject;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 
 public class GeckoPreferences
 extends AppCompatPreferenceActivity
@@ -176,16 +177,24 @@ OnSharedPreferenceChangeListener
 
     // Result code used when a locale preference changes.
     // Callers can recognize this code to refresh themselves to
     // accommodate a locale change.
     public static final int RESULT_CODE_LOCALE_DID_CHANGE = 7;
 
     private static final int REQUEST_CODE_TAB_QUEUE = 8;
 
+    private final Map<String, PrefHandler> HANDLERS;
+    {
+        final HashMap<String, PrefHandler> tempHandlers = new HashMap<>(2);
+        tempHandlers.put(ClearOnShutdownPref.PREF, new ClearOnShutdownPref());
+        tempHandlers.put(AndroidImportPreference.PREF_KEY, new AndroidImportPreference.Handler());
+        HANDLERS = Collections.unmodifiableMap(tempHandlers);
+    }
+
     private SwitchPreference tabQueuePreference;
 
     /**
      * Track the last locale so we know whether to redisplay.
      */
     private Locale lastLocale = Locale.getDefault();
     private boolean localeSwitchingIsEnabled;
 
@@ -685,18 +694,18 @@ OnSharedPreferenceChangeListener
                 } else if (PREFS_SCREEN_ADVANCED.equals(key) &&
                         !Restrictions.isAllowed(this, Restrictable.ADVANCED_SETTINGS)) {
                     preferences.removePreference(pref);
                     i--;
                     continue;
                 }
                 setupPreferences((PreferenceGroup) pref, prefs);
             } else {
-                if (handlers.containsKey(key)) {
-                    PrefHandler handler = handlers.get(key);
+                if (HANDLERS.containsKey(key)) {
+                    PrefHandler handler = HANDLERS.get(key);
                     if (!handler.setupPref(this, pref)) {
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
                 }
 
                 pref.setOnPreferenceChangeListener(this);
@@ -1142,22 +1151,16 @@ OnSharedPreferenceChangeListener
 
     public interface PrefHandler {
         // Allows the pref to do any initialization it needs. Return false to have the pref removed
         // from the prefs screen entirely.
         public boolean setupPref(Context context, Preference pref);
         public void onChange(Context context, Preference pref, Object newValue);
     }
 
-    @SuppressWarnings("serial")
-    private final Map<String, PrefHandler> handlers = new HashMap<String, PrefHandler>() {{
-        put(ClearOnShutdownPref.PREF, new ClearOnShutdownPref());
-        put(AndroidImportPreference.PREF_KEY, new AndroidImportPreference.Handler());
-    }};
-
     private void recordSettingChangeTelemetry(String prefName, Object newValue) {
         final String value;
         if (newValue instanceof Boolean) {
             value = (Boolean) newValue ? "1" : "0";
         } else if (prefName.equals(PREFS_HOMEPAGE)) {
             // Don't record the user's homepage preference.
             value = "*";
         } else {
@@ -1219,18 +1222,18 @@ OnSharedPreferenceChangeListener
         } else if (PREFS_TAB_QUEUE.equals(prefName)) {
             if ((Boolean) newValue && !TabQueueHelper.canDrawOverlays(this)) {
                 Intent promptIntent = new Intent(this, TabQueuePrompt.class);
                 startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE);
                 return false;
             }
         } else if (PREFS_NOTIFICATIONS_CONTENT.equals(prefName)) {
             FeedService.setup(this);
-        } else if (handlers.containsKey(prefName)) {
-            PrefHandler handler = handlers.get(prefName);
+        } else if (HANDLERS.containsKey(prefName)) {
+            PrefHandler handler = HANDLERS.get(prefName);
             handler.onChange(this, preference, newValue);
         }
 
         // Send Gecko-side pref changes to Gecko
         if (isGeckoPref(prefName)) {
             PrefsHelper.setPref(prefName, newValue, true /* flush */);
         }
 
@@ -1338,17 +1341,17 @@ OnSharedPreferenceChangeListener
     }
 
     @Override
     protected Dialog onCreateDialog(int id) {
         AlertDialog.Builder builder = new AlertDialog.Builder(this);
         LinearLayout linearLayout = new LinearLayout(this);
         linearLayout.setOrientation(LinearLayout.VERTICAL);
         AlertDialog dialog;
-        switch(id) {
+        switch (id) {
             case DIALOG_CREATE_MASTER_PASSWORD:
                 final TextInputLayout inputLayout1 = getTextBox(R.string.masterpassword_password);
                 final TextInputLayout inputLayout2 = getTextBox(R.string.masterpassword_confirm);
                 linearLayout.addView(inputLayout1);
                 linearLayout.addView(inputLayout2);
 
                 final EditText input1 = inputLayout1.getEditText();
                 final EditText input2 = inputLayout2.getEditText();
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/LocaleListPreference.java
@@ -49,17 +49,17 @@ public class LocaleListPreference extend
 
         private final Paint paint = new Paint();
         private final byte[] missingCharacter;
 
         public CharacterValidator(String missing) {
             this.missingCharacter = getPixels(drawBitmap(missing));
         }
 
-        private Bitmap drawBitmap(String text){
+        private Bitmap drawBitmap(String text) {
             Bitmap b = Bitmap.createBitmap(BITMAP_WIDTH, BITMAP_HEIGHT, Bitmap.Config.ALPHA_8);
             Canvas c = new Canvas(b);
             c.drawText(text, 0, BITMAP_HEIGHT / 2, this.paint);
             return b;
         }
 
         private static byte[] getPixels(final Bitmap b) {
             final int byteCount;
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/MultiPrefMultiChoicePreference.java
@@ -87,17 +87,17 @@ class MultiPrefMultiChoicePreference ext
                         // Save the pref and remove the old preference.
                         setValue(i, val);
                         edit.remove(key);
                     }
 
                     persist(edit);
                     edit.putBoolean(getKey() + IMPORT_SUFFIX, true);
                     edit.apply();
-                } catch(Exception ex) {
+                } catch (Exception ex) {
                     Log.i(LOGTAG, "Err", ex);
                 }
             }
         });
     }
 
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/PanelsPreference.java
@@ -120,17 +120,17 @@ public class PanelsPreference extends Cu
             }
         } else {
             setSummary("");
         }
     }
 
     @Override
     protected void onDialogIndexClicked(int index) {
-        switch(index) {
+        switch (index) {
             case INDEX_SET_DEFAULT_BUTTON:
                 mParentCategory.setDefault(this);
                 break;
 
             case INDEX_DISPLAY_BUTTON:
                 // Handle display options for the panel.
                 if (mIsRemovable) {
                     // For removable panels, the button displays text for removing the panel.
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/Prompt.java
@@ -85,17 +85,17 @@ public class Prompt implements OnClickLi
         mButtons = getStringArray(message, "buttons");
 
         JSONArray inputs = getSafeArray(message, "inputs");
         mInputs = new PromptInput[inputs.length()];
         for (int i = 0; i < mInputs.length; i++) {
             try {
                 mInputs[i] = PromptInput.getInput(inputs.getJSONObject(i));
                 mInputs[i].setListener(this);
-            } catch(Exception ex) { }
+            } catch (Exception ex) { }
         }
 
         PromptListItem[] menuitems = PromptListItem.getArray(message.optJSONArray("listitems"));
         String selected = message.optString("choiceMode");
 
         int choiceMode = ListView.CHOICE_MODE_NONE;
         if ("single".equals(selected)) {
             choiceMode = ListView.CHOICE_MODE_SINGLE;
@@ -110,17 +110,17 @@ public class Prompt implements OnClickLi
         show(title, text, menuitems, choiceMode);
     }
 
      public void show(String title, String text, PromptListItem[] listItems, int choiceMode) {
         ThreadUtils.assertOnUiThread();
 
         try {
             create(title, text, listItems, choiceMode);
-        } catch(IllegalStateException ex) {
+        } catch (IllegalStateException ex) {
             Log.i(LOGTAG, "Error building dialog", ex);
             return;
         }
 
         if (mTabId != Tabs.INVALID_TAB_ID) {
             Tabs.registerOnTabsChangedListener(this);
 
             final Tab tab = Tabs.getInstance().getTab(mTabId);
@@ -133,17 +133,17 @@ public class Prompt implements OnClickLi
     }
 
     @Override
     public void onTabChanged(final Tab tab, final Tabs.TabEvents msg, final Object data) {
         if (tab != Tabs.getInstance().getTab(mTabId)) {
             return;
         }
 
-        switch(msg) {
+        switch (msg) {
             case SELECTED:
                 Log.i(LOGTAG, "Selected");
                 mDialog.show();
                 break;
             case UNSELECTED:
                 Log.i(LOGTAG, "Unselected");
                 mDialog.hide();
                 break;
@@ -227,48 +227,48 @@ public class Prompt implements OnClickLi
                 if (!selectedItems.contains(which)) {
                     selected.put(which);
                 }
 
                 result.put("button", which);
             }
 
             result.put("list", selected);
-        } catch(JSONException ex) { }
+        } catch (JSONException ex) { }
     }
 
     /* Adds to a result value from the inputs that can be shown in dialogs.
      * Each input will set its own value in the result.
      */
     private void addInputValues(final JSONObject result) {
         try {
             if (mInputs != null) {
                 for (int i = 0; i < mInputs.length; i++) {
                     if (mInputs[i] != null) {
                         result.put(mInputs[i].getId(), mInputs[i].getValue());
                     }
                 }
             }
-        } catch(JSONException ex) { }
+        } catch (JSONException ex) { }
     }
 
     /* Adds the selected button to a result. This should only be called if there
      * are no lists shown on the dialog, since they also write their results to the button
      * attribute.
      */
     private void addButtonResult(final JSONObject result, int which) {
         int button = -1;
-        switch(which) {
+        switch (which) {
             case DialogInterface.BUTTON_POSITIVE : button = 0; break;
             case DialogInterface.BUTTON_NEUTRAL  : button = 1; break;
             case DialogInterface.BUTTON_NEGATIVE : button = 2; break;
         }
         try {
             result.put("button", button);
-        } catch(JSONException ex) { }
+        } catch (JSONException ex) { }
     }
 
     @Override
     public void onClick(DialogInterface dialog, int which) {
         ThreadUtils.assertOnUiThread();
         closeDialog(which);
     }
 
@@ -278,17 +278,17 @@ public class Prompt implements OnClickLi
      * @param builder
      *        The alert builder currently building this dialog.
      * @param listItems
      *        The items to add.
      * @param choiceMode
      *        One of the ListView.CHOICE_MODE constants to designate whether this list shows checkmarks, radios buttons, or nothing.
     */
     private void addListItems(AlertDialog.Builder builder, PromptListItem[] listItems, int choiceMode) {
-        switch(choiceMode) {
+        switch (choiceMode) {
             case ListView.CHOICE_MODE_MULTIPLE_MODAL:
             case ListView.CHOICE_MODE_MULTIPLE:
                 addMultiSelectList(builder, listItems);
                 break;
             case ListView.CHOICE_MODE_SINGLE:
                 addSingleSelectList(builder, listItems);
                 break;
             case ListView.CHOICE_MODE_NONE:
@@ -403,17 +403,17 @@ public class Prompt implements OnClickLi
                 // If we're showing some sort of scrollable list, force an inverse background.
                 builder.setInverseBackgroundForced(true);
                 builder.setView(root);
             } else {
                 ScrollView view = new ScrollView(mContext);
                 view.addView(root);
                 builder.setView(view);
             }
-        } catch(Exception ex) {
+        } catch (Exception ex) {
             Log.e(LOGTAG, "Error showing prompt inputs", ex);
             // We cannot display these input widgets with this sdk version,
             // do not display any dialog and finish the prompt now.
             cancelDialog();
             return false;
         }
 
         return true;
@@ -457,17 +457,17 @@ public class Prompt implements OnClickLi
 
     /* Called in situations where we want to cancel the dialog . This can happen if the user hits back,
      *  or if the dialog can't be created because of invalid JSON.
      */
     private void cancelDialog() {
         JSONObject ret = new JSONObject();
         try {
             ret.put("button", -1);
-        } catch(Exception ex) { }
+        } catch (Exception ex) { }
         addInputValues(ret);
         notifyClosing(ret);
     }
 
     /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
      * is closing.
      */
     private void closeDialog(int which) {
@@ -482,17 +482,17 @@ public class Prompt implements OnClickLi
     }
 
     /* Called any time we're closing the dialog to cleanup and notify listeners that the dialog
      * is closing.
      */
     private void notifyClosing(JSONObject aReturn) {
         try {
             aReturn.put("guid", mGuid);
-        } catch(JSONException ex) { }
+        } catch (JSONException ex) { }
 
         if (mTabId != Tabs.INVALID_TAB_ID) {
             Tabs.unregisterOnTabsChangedListener(this);
         }
 
         // poke the Gecko thread in case it's waiting for new events
         GeckoAppShell.sendEventToGecko(GeckoEvent.createNoOpEvent());
 
@@ -520,32 +520,32 @@ public class Prompt implements OnClickLi
 
     public static String[] getStringArray(JSONObject aObject, String aName) {
         JSONArray items = getSafeArray(aObject, aName);
         int length = items.length();
         String[] list = new String[length];
         for (int i = 0; i < length; i++) {
             try {
                 list[i] = items.getString(i);
-            } catch(Exception ex) { }
+            } catch (Exception ex) { }
         }
         return list;
     }
 
     private static boolean[] getBooleanArray(JSONObject aObject, String aName) {
         JSONArray items = new JSONArray();
         try {
             items = aObject.getJSONArray(aName);
-        } catch(Exception ex) { return null; }
+        } catch (Exception ex) { return null; }
         int length = items.length();
         boolean[] list = new boolean[length];
         for (int i = 0; i < length; i++) {
             try {
                 list[i] = items.getBoolean(i);
-            } catch(Exception ex) { }
+            } catch (Exception ex) { }
         }
         return list;
     }
 
     public interface PromptCallback {
 
         /**
          * Called when the Prompt has been completed (i.e. when the user has selected an item or action in the Prompt).
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListAdapter.java
@@ -136,36 +136,36 @@ public class PromptListAdapter extends A
         if (viewHolder.textView instanceof CheckedTextView) {
             // Apparently just using ct.setChecked(true) doesn't work, so this
             // is stolen from the android source code as a way to set the checked
             // state of these items
             list.setItemChecked(position, item.getSelected());
         }
     }
 
-    boolean isSelected(int position){
+    boolean isSelected(int position) {
         return getItem(position).getSelected();
     }
 
     ArrayList<Integer> getSelected() {
         int length = getCount();
 
         ArrayList<Integer> selected = new ArrayList<Integer>();
-        for (int i = 0; i< length; i++) {
+        for (int i = 0; i < length; i++) {
             if (isSelected(i)) {
                 selected.add(i);
             }
         }
 
         return selected;
     }
 
     int getSelectedIndex() {
         int length = getCount();
-        for (int i = 0; i< length; i++) {
+        for (int i = 0; i < length; i++) {
             if (isSelected(i)) {
                 return i;
             }
         }
         return -1;
     }
 
     private View getActionView(PromptListItem item, final ListView list, final int position) {
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptListItem.java
@@ -106,14 +106,14 @@ public class PromptListItem {
         }
 
         int length = items.length();
         List<PromptListItem> list = new ArrayList<>(length);
         for (int i = 0; i < length; i++) {
             try {
                 PromptListItem item = new PromptListItem(items.getJSONObject(i));
                 list.add(item);
-            } catch(Exception ex) { }
+            } catch (Exception ex) { }
         }
 
         return list.toArray(new PromptListItem[length]);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/PromptService.java
@@ -54,17 +54,17 @@ public class PromptService implements Ge
             @Override
             public void run() {
                 Prompt p;
                 p = new Prompt(mContext, new Prompt.PromptCallback() {
                     @Override
                     public void onPromptFinished(String jsonResult) {
                         try {
                             EventDispatcher.sendResponse(message, new JSONObject(jsonResult));
-                        } catch(JSONException ex) {
+                        } catch (JSONException ex) {
                             Log.i(LOGTAG, "Error building json response", ex);
                         }
                     }
                 });
                 p.show(message);
             }
         });
     }
--- a/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
+++ b/mobile/android/base/java/org/mozilla/gecko/prompts/TabInput.java
@@ -39,17 +39,17 @@ public class TabInput extends PromptInpu
         try {
             JSONArray tabs = obj.getJSONArray("items");
             for (int i = 0; i < tabs.length(); i++) {
                 JSONObject tab = tabs.getJSONObject(i);
                 String title = tab.getString("label");
                 JSONArray items = tab.getJSONArray("items");
                 mTabs.put(title, PromptListItem.getArray(items));
             }
-        } catch(JSONException ex) {
+        } catch (JSONException ex) {
             Log.e(LOGTAG, "Exception", ex);
         }
     }
 
     @Override
     public View getView(final Context context) throws UnsupportedOperationException {
         final LayoutInflater inflater = LayoutInflater.from(context);
         mHost = (TabHost) inflater.inflate(R.layout.tab_prompt_input, null);
@@ -77,17 +77,17 @@ public class TabInput extends PromptInpu
     }
 
     @Override
     public Object getValue() {
         JSONObject obj = new JSONObject();
         try {
             obj.put("tab", mHost.getCurrentTab());
             obj.put("item", mPosition);
-        } catch(JSONException ex) { }
+        } catch (JSONException ex) { }
 
         return obj;
     }
 
     @Override
     public boolean getScrollable() {
         return true;
     }
--- a/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/push/PushService.java
@@ -45,17 +45,17 @@ import java.util.Map;
 @ReflectionTarget
 public class PushService implements BundleEventListener {
     private static final String LOG_TAG = "GeckoPushService";
 
     public static final String SERVICE_WEBPUSH = "webpush";
 
     private static PushService sInstance;
 
-    private static final String[] GECKO_EVENTS = new String[]{
+    private static final String[] GECKO_EVENTS = new String[] {
             "PushServiceAndroidGCM:Configure",
             "PushServiceAndroidGCM:DumpRegistration",
             "PushServiceAndroidGCM:DumpSubscriptions",
             "PushServiceAndroidGCM:Initialized",
             "PushServiceAndroidGCM:Uninitialized",
             "PushServiceAndroidGCM:RegisterUserAgent",
             "PushServiceAndroidGCM:UnregisterUserAgent",
             "PushServiceAndroidGCM:SubscribeChannel",
--- a/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/ReadingListHelper.java
@@ -39,17 +39,17 @@ public final class ReadingListHelper imp
     public void uninit() {
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
             "Reader:FaviconRequest", "Reader:AddedToCache");
     }
 
     @Override
     public void handleMessage(final String event, final NativeJSObject message,
                               final EventCallback callback) {
-        switch(event) {
+        switch (event) {
             case "Reader:FaviconRequest": {
                 handleReaderModeFaviconRequest(callback, message.getString("url"));
                 break;
             }
             case "Reader:AddedToCache": {
                 // AddedToCache is a one way message: callback will be null, and we therefore shouldn't
                 // attempt to handle it.
                 handleAddedToCache(message.getString("url"), message.getString("path"), message.getInt("size"));
--- a/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
@@ -84,17 +84,20 @@ public class SavedReaderViewHelper {
     }
 
     /**
      * Load the reader view cache list from our JSON file.
      *
      * Must not be run on the UI thread due to file access.
      */
     public synchronized void loadItems() {
-        ThreadUtils.assertNotOnUiThread();
+        // TODO bug 1264489
+        // This is a band aid fix for Bug 1264134. We need to figure out the root cause and reenable this
+        // assertion.
+        // ThreadUtils.assertNotOnUiThread();
 
         if (mItems != null) {
             return;
         }
 
         try {
             mItems = GeckoProfile.get(mContext).readJSONObjectFromFile(FILE_PATH);
         } catch (IOException e) {
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
@@ -47,17 +47,17 @@ public class RestrictedProfileConfigurat
     private static List<Restrictable> hiddenRestrictions = new ArrayList<>();
     static {
         hiddenRestrictions.add(Restrictable.MASTER_PASSWORD);
         hiddenRestrictions.add(Restrictable.GUEST_BROWSING);
         hiddenRestrictions.add(Restrictable.DATA_CHOICES);
         hiddenRestrictions.add(Restrictable.DEFAULT_THEME);
 
         // Hold behind Nightly flag until we have an actual block list deployed.
-        if (!AppConstants.NIGHTLY_BUILD){
+        if (!AppConstants.NIGHTLY_BUILD) {
             hiddenRestrictions.add(Restrictable.BLOCK_LIST);
         }
     }
 
     /* package-private */ static boolean shouldHide(Restrictable restrictable) {
         return hiddenRestrictions.contains(restrictable);
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
+++ b/mobile/android/base/java/org/mozilla/gecko/sqlite/SQLiteBridge.java
@@ -308,17 +308,17 @@ public class SQLiteBridge {
             return;
 
         try {
           if (mTransactionSuccess) {
               execSQL("COMMIT TRANSACTION");
           } else {
               execSQL("ROLLBACK TRANSACTION");
           }
-        } catch(SQLiteBridgeException ex) {
+        } catch (SQLiteBridgeException ex) {
             Log.e(LOGTAG, "Error ending transaction", ex);
         }
         mInTransaction = false;
         mTransactionSuccess = false;
     }
 
     public void setTransactionSuccessful() throws SQLiteBridgeException {
         if (!inTransaction()) {
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueueHelper.java
@@ -172,17 +172,17 @@ public class TabQueueHelper {
 
         // Since JSONArray.remove was only added in API 19, we have to use two arrays in order to remove.
         for (int i = 0; i < jsonArray.length(); i++) {
             try {
                 url = jsonArray.getString(i);
             } catch (JSONException e) {
                 url = "";
             }
-            if(!TextUtils.isEmpty(url) && !urlToRemove.equals(url)) {
+            if (!TextUtils.isEmpty(url) && !urlToRemove.equals(url)) {
                 newArray.put(url);
             }
         }
 
         profile.writeFile(filename, newArray.toString());
 
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
         prefs.edit().putInt(PREF_TAB_QUEUE_COUNT, newArray.length()).apply();
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
@@ -24,23 +24,24 @@ public class TelemetryConstants {
 
     public static final String PREF_SERVER_URL = "telemetry-serverUrl";
     public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
 
     public static class CorePing {
         private CorePing() { /* To prevent instantiation */ }
 
         public static final String NAME = "core";
-        public static final int VERSION_VALUE = 3; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
+        public static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
         public static final String OS_VALUE = "Android";
 
         public static final String ARCHITECTURE = "arch";
         public static final String CLIENT_ID = "clientId";
         public static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
         public static final String DEVICE = "device";
+        public static final String DISTRIBUTION_ID = "distributionId";
         public static final String EXPERIMENTS = "experiments";
         public static final String LOCALE = "locale";
         public static final String OS_ATTR = "os";
         public static final String OS_VERSION = "osversion";
         public static final String PROFILE_CREATION_DATE = "profileDate";
         public static final String SEQ = "seq";
         public static final String VERSION_ATTR = "v";
     }
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
@@ -60,25 +60,26 @@ public class TelemetryPingGenerator {
      * @param docId A unique document ID for the ping associated with the upload to this server
      * @param clientId The client ID of this profile (from Gecko)
      * @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
      * @param profileCreationDateDays The profile creation date in days to the UNIX epoch, NOT MILLIS.
      * @throws IOException when client ID could not be created
      */
     public static TelemetryPing createCorePing(final Context context, final String docId, final String clientId,
             final String serverURLSchemeHostPort, final int seq, final long profileCreationDateDays,
-            @Nullable final String defaultSearchEngine) {
+            @Nullable final String distributionId, @Nullable final String defaultSearchEngine) {
         final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
         final ExtendedJSONObject payload =
-                createCorePingPayload(context, clientId, seq, profileCreationDateDays, defaultSearchEngine);
+                createCorePingPayload(context, clientId, seq, profileCreationDateDays, distributionId, defaultSearchEngine);
         return new TelemetryPing(serverURL, payload);
     }
 
     private static ExtendedJSONObject createCorePingPayload(final Context context, final String clientId,
-            final int seq, final long profileCreationDate, @Nullable final String defaultSearchEngine) {
+            final int seq, final long profileCreationDate, @Nullable final String distributionId,
+            @Nullable final String defaultSearchEngine) {
         final ExtendedJSONObject ping = new ExtendedJSONObject();
         ping.put(CorePing.VERSION_ATTR, CorePing.VERSION_VALUE);
         ping.put(CorePing.OS_ATTR, CorePing.OS_VALUE);
 
         // We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
         // manufacturer because we're less likely to have manufacturers with similar names than we are for a
         // manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
         final String deviceDescriptor =
@@ -88,14 +89,19 @@ public class TelemetryPingGenerator {
         ping.put(CorePing.CLIENT_ID, clientId);
         ping.put(CorePing.DEFAULT_SEARCH_ENGINE, TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine);
         ping.put(CorePing.DEVICE, deviceDescriptor);
         ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
         ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
         ping.put(CorePing.SEQ, seq);
         ping.putArray(CorePing.EXPERIMENTS, Experiments.getActiveExperiments(context));
 
+        // Optional.
+        if (distributionId != null) {
+            ping.put(CorePing.DISTRIBUTION_ID, distributionId);
+        }
+
         // `null` indicates failure more clearly than < 0.
         final Long finalProfileCreationDate = (profileCreationDate < 0) ? null : profileCreationDate;
         ping.put(CorePing.PROFILE_CREATION_DATE, finalProfileCreationDate);
         return ping;
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -11,16 +11,17 @@ import android.support.annotation.NonNul
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.background.BackgroundService;
+import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
@@ -178,19 +179,19 @@ public class TelemetryUploadService exte
             return;
         }
 
         // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
         final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(this, profileName);
         // TODO (bug 1241685): Sync this preference with the gecko preference.
         final String serverURLSchemeHostPort =
                 sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
-
+        final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
         final TelemetryPing corePing = TelemetryPingGenerator.createCorePing(this, docId, clientId,
-                serverURLSchemeHostPort, seq, profileCreationDate, defaultSearchEngine);
+                serverURLSchemeHostPort, seq, profileCreationDate, distributionId, defaultSearchEngine);
         final CorePingResultDelegate resultDelegate = new CorePingResultDelegate();
         uploadPing(corePing, resultDelegate);
     }
 
     private void uploadPing(final TelemetryPing ping, final ResultDelegate delegate) {
         final BaseResource resource;
         try {
             resource = new BaseResource(ping.getURL());
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BackButton.java
@@ -13,14 +13,14 @@ public class BackButton extends NavButto
         super(context, attrs);
     }
 
     @Override
     protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
         super.onSizeChanged(width, height, oldWidth, oldHeight);
 
         mPath.reset();
-        mPath.addCircle(width/2, height/2, width/2, Path.Direction.CW);
+        mPath.addCircle(width / 2, height / 2, width / 2, Path.Direction.CW);
 
         mBorderPath.reset();
-        mBorderPath.addCircle(width/2, height/2, (width/2) - (mBorderWidth/2), Path.Direction.CW);
+        mBorderPath.addCircle(width / 2, height / 2, (width / 2) - (mBorderWidth / 2), Path.Direction.CW);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SiteIdentityPopup.java
@@ -384,17 +384,17 @@ public class SiteIdentityPopup extends A
 
     private void clearSecurityStateIcon() {
         mSecurityState.setCompoundDrawablePadding(0);
         mSecurityState.setCompoundDrawables(null, null, null, null);
     }
 
     private void setSecurityStateIcon(int resource, int factor) {
         final Drawable stateIcon = ContextCompat.getDrawable(mContext, resource);
-        stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth()/factor, stateIcon.getIntrinsicHeight()/factor);
+        stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth() / factor, stateIcon.getIntrinsicHeight() / factor);
         mSecurityState.setCompoundDrawables(stateIcon, null, null, null);
         mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
     }
     private void updateIdentityInformation(final SiteIdentity siteIdentity) {
         String owner = siteIdentity.getOwner();
         if (owner == null) {
             mOwner.setVisibility(View.GONE);
             mOwnerSupplemental.setVisibility(View.GONE);
@@ -417,17 +417,17 @@ public class SiteIdentityPopup extends A
     }
 
     private void addTrackingContentNotification(boolean blocked) {
         // Remove any existing tracking content notification.
         removeTrackingContentNotification();
 
         final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mContentButtonClickListener);
 
-        final int icon = blocked ? R.drawable.shield_enabled: R.drawable.shield_disabled;
+        final int icon = blocked ? R.drawable.shield_enabled : R.drawable.shield_disabled;
 
         final JSONObject options = new JSONObject();
         final JSONObject tracking = new JSONObject();
         try {
             tracking.put("enabled", blocked);
             options.put("tracking_protection", tracking);
         } catch (JSONException e) {
             Log.e(LOGTAG, "Error adding tracking protection options", e);
--- a/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/updater/UpdateService.java
@@ -604,22 +604,22 @@ public class UpdateService extends Inten
             showDownloadFailure();
 
             Log.e(LOGTAG, "failed to download update: ", e);
             return null;
         } finally {
             try {
                 if (input != null)
                     input.close();
-            } catch (java.io.IOException e) {}
+            } catch (java.io.IOException e) { }
 
             try {
                 if (output != null)
                     output.close();
-            } catch (java.io.IOException e) {}
+            } catch (java.io.IOException e) { }
 
             mDownloading = false;
 
             if (mWifiLock.isHeld()) {
                 mWifiLock.release();
             }
         }
     }
@@ -641,17 +641,17 @@ public class UpdateService extends Inten
             }
         } catch (java.io.IOException e) {
             Log.e(LOGTAG, "Failed to verify update package: ", e);
             return false;
         } finally {
             try {
                 if (input != null)
                     input.close();
-            } catch(java.io.IOException e) {}
+            } catch (java.io.IOException e) { }
         }
 
         String hex = Hex.encodeHexString(digest.digest());
         if (!hex.equals(getLastHashValue())) {
             Log.e(LOGTAG, "Package hash does not match");
             return false;
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/util/Clipboard.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/Clipboard.java
@@ -43,17 +43,17 @@ public final class Clipboard {
         }
 
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 String text = getClipboardTextImpl();
                 try {
                     sClipboardQueue.put(text != null ? text : "");
-                } catch (InterruptedException ie) {}
+                } catch (InterruptedException ie) { }
             }
         });
 
         try {
             return sClipboardQueue.take();
         } catch (InterruptedException ie) {
             return "";
         }
--- a/mobile/android/base/java/org/mozilla/gecko/util/FileUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/FileUtils.java
@@ -9,17 +9,17 @@ import android.util.Log;
 import java.io.File;
 import java.io.IOException;
 import java.io.FilenameFilter;
 import java.util.Scanner;
 
 import org.mozilla.gecko.annotation.RobocopTarget;
 
 public class FileUtils {
-    private static final String LOGTAG= "GeckoFileUtils";
+    private static final String LOGTAG = "GeckoFileUtils";
     /*
     * A basic Filter for checking a filename and age.
     **/
     static public class NameAndAgeFilter implements FilenameFilter {
         final private String mName;
         final private double mMaxAge;
 
         public NameAndAgeFilter(String name, double age) {
@@ -68,17 +68,17 @@ public class FileUtils {
     public static boolean delete(File file, boolean recurse) {
         if (file.isDirectory() && recurse) {
             // If the quick delete failed and this is a dir, recursively delete the contents of the dir
             String files[] = file.list();
             for (String temp : files) {
                 File fileDelete = new File(file, temp);
                 try {
                     delete(fileDelete);
-                } catch(IOException ex) {
+                } catch (IOException ex) {
                     Log.i(LOGTAG, "Error deleting " + fileDelete.getPath(), ex);
                 }
             }
         }
 
         // Even if this is a dir, it should now be empty and delete should work
         return file.delete();
     }
--- a/mobile/android/base/java/org/mozilla/gecko/util/GamepadUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/GamepadUtils.java
@@ -122,17 +122,17 @@ public final class GamepadUtils {
         // The cross and circle buttons on Sony Xperia phones are swapped
         // in different regions
         // http://developer.sonymobile.com/2011/02/13/xperia-play-game-keys/
         final char DEFAULT_O_BUTTON_LABEL = 0x25CB;
 
         boolean swapped = false;
         int[] deviceIds = InputDevice.getDeviceIds();
 
-        for (int i= 0; deviceIds != null && i < deviceIds.length; i++) {
+        for (int i = 0; deviceIds != null && i < deviceIds.length; i++) {
             KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(deviceIds[i]);
             if (keyCharacterMap != null && DEFAULT_O_BUTTON_LABEL ==
                 keyCharacterMap.getDisplayLabel(KeyEvent.KEYCODE_DPAD_CENTER)) {
                 swapped = true;
                 break;
             }
         }
         return swapped;
--- a/mobile/android/base/java/org/mozilla/gecko/util/GeckoJarReader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/GeckoJarReader.java
@@ -57,17 +57,17 @@ public final class GeckoJarReader {
                 bitmap.setTargetDensity(resources.getDisplayMetrics());
             }
         } catch (IOException | URISyntaxException ex) {
             Log.e(LOGTAG, "Exception ", ex);
         } finally {
             if (inputStream != null) {
                 try {
                     inputStream.close();
-                } catch(IOException ex) {
+                } catch (IOException ex) {
                     Log.e(LOGTAG, "Error closing stream", ex);
                 }
             }
             if (zip != null) {
                 zip.close();
             }
         }
 
@@ -88,17 +88,17 @@ public final class GeckoJarReader {
                 text = reader.readLine();
             }
         } catch (IOException | URISyntaxException ex) {
             Log.e(LOGTAG, "Exception ", ex);
         } finally {
             if (reader != null) {
                 try {
                     reader.close();
-                } catch(IOException ex) {
+                } catch (IOException ex) {
                     Log.e(LOGTAG, "Error closing reader", ex);
                 }
             }
             if (zip != null) {
                 zip.close();
             }
         }
 
@@ -233,17 +233,17 @@ public final class GeckoJarReader {
     private static Stack<String> parseUrl(String url, Stack<String> results) {
         if (results == null) {
             results = new Stack<String>();
         }
 
         if (url.startsWith("jar:")) {
             int jarEnd = url.lastIndexOf("!");
             String subStr = url.substring(4, jarEnd);
-            results.push(url.substring(jarEnd+2)); // remove the !/ characters
+            results.push(url.substring(jarEnd + 2)); // remove the !/ characters
             return parseUrl(subStr, results);
         } else {
             results.push(url);
             return results;
         }
     }
 
     public static String getJarURL(Context context, String pathInsideJAR) {
--- a/mobile/android/base/java/org/mozilla/gecko/util/INIParser.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/INIParser.java
@@ -111,17 +111,17 @@ public final class INIParser extends INI
             if (line != null)
                 line = line.trim();
 
             // blank line or a comment. ignore it
             if (line == null || line.length() == 0 || line.charAt(0) == ';') {
                 debug("Ignore line: " + line);
             } else if (line.charAt(0) == '[') {
                 debug("Parse as section: " + line);
-                currentSection = new INISection(line.substring(1, line.length()-1));
+                currentSection = new INISection(line.substring(1, line.length() - 1));
                 mSections.put(currentSection.getName(), currentSection);
             } else {
                 debug("Parse as property: " + line);
 
                 String[] pieces = line.split("=");
                 if (pieces.length != 2)
                     continue;
 
--- a/mobile/android/base/java/org/mozilla/gecko/util/IOUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/IOUtils.java
@@ -110,17 +110,17 @@ public class IOUtils {
 
         return newBytes;
     }
 
     public static void safeStreamClose(Closeable stream) {
         try {
             if (stream != null)
                 stream.close();
-        } catch (IOException e) {}
+        } catch (IOException e) { }
     }
 
     public static void copy(InputStream in, OutputStream out) throws IOException {
         byte[] buffer = new byte[4096];
         int len;
 
         while ((len = in.read(buffer)) != -1) {
             out.write(buffer, 0, len);
--- a/mobile/android/base/java/org/mozilla/gecko/util/JSONUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/JSONUtils.java
@@ -53,17 +53,17 @@ public final class JSONUtils {
 
     // Handles conversions between a JSONArray and a Set<String>
     public static Set<String> parseStringSet(JSONArray json) {
         final Set<String> ret = new HashSet<String>();
 
         for (int i = 0; i < json.length(); i++) {
             try {
                 ret.add(json.getString(i));
-            } catch(JSONException ex) {
+            } catch (JSONException ex) {
                 Log.i(LOGTAG, "Error parsing json", ex);
             }
         }
 
         return ret;
     }
 
 }
--- a/mobile/android/base/java/org/mozilla/gecko/util/PrefUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/PrefUtils.java
@@ -25,30 +25,30 @@ public class PrefUtils {
                                            final Set<String> defaultVal) {
         if (!prefs.contains(key)) {
             return defaultVal;
         }
 
         // If this is Android version >= 11, try to use a Set<String>.
         try {
             return prefs.getStringSet(key, new HashSet<String>());
-        } catch(ClassCastException ex) {
+        } catch (ClassCastException ex) {
             // A ClassCastException means we've upgraded from a pre-v11 Android to a new one
             final Set<String> val = getFromJSON(prefs, key);
             SharedPreferences.Editor edit = prefs.edit();
             putStringSet(edit, key, val).apply();
             return val;
         }
     }
 
     private static Set<String> getFromJSON(SharedPreferences prefs, String key) {
         try {
             final String val = prefs.getString(key, "[]");
             return JSONUtils.parseStringSet(new JSONArray(val));
-        } catch(JSONException ex) {
+        } catch (JSONException ex) {
             Log.i(LOGTAG, "Unable to parse JSON", ex);
         }
 
         return new HashSet<String>();
     }
 
     /**
      * Cross version compatible way to save a set of strings.
--- a/mobile/android/base/java/org/mozilla/gecko/util/WebActivityMapper.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/WebActivityMapper.java
@@ -208,14 +208,14 @@ public final class WebActivityMapper {
 
             try {
                 File dest = File.createTempFile(
                     "capture", /* prefix */
                     ext,       /* suffix */
                     destDir    /* directory */
                 );
                 intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(dest));
-            } catch(Exception e) {
+            } catch (Exception e) {
                 Log.w(LOGTAG, "Failed to add extra for " + action + " : " + e);
             }
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ContentSecurityDoorHanger.java
@@ -87,17 +87,17 @@ public class ContentSecurityDoorHanger e
                     mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.affirmative_green));
                 } else {
                     mMessage.setText(R.string.doorhanger_tracking_message_disabled);
                     mSecurityState.setText(R.string.doorhanger_tracking_state_disabled);
                     mSecurityState.setTextColor(ContextCompat.getColor(getContext(), R.color.rejection_red));
                 }
                 mMessage.setVisibility(VISIBLE);
                 mSecurityState.setVisibility(VISIBLE);
-            } catch (JSONException e) {}
+            } catch (JSONException e) { }
         }
     }
 
     @Override
     protected OnClickListener makeOnButtonClickListener(final int id, final String telemetryExtra) {
         return new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DateTimePicker.java
@@ -140,54 +140,54 @@ public class DateTimePicker extends Fram
                     setTempDate(Calendar.MINUTE, oldVal, newVal, 0, 59);
                 } else if (picker == mAMPMSpinner && mHourEnabled) {
                     mTempDate.set(Calendar.AM_PM, newVal);
                 } else {
                     throw new IllegalArgumentException();
                 }
             } else {
                 if (DEBUG) Log.d(LOGTAG, "Sdk version < 10, using old behavior");
-                if (picker == mDaySpinner && mDayEnabled){
+                if (picker == mDaySpinner && mDayEnabled) {
                     mTempDate.set(Calendar.DAY_OF_MONTH, newVal);
-                } else if (picker == mMonthSpinner && mMonthEnabled){
+                } else if (picker == mMonthSpinner && mMonthEnabled) {
                     mTempDate.set(Calendar.MONTH, newVal);
-                    if (mTempDate.get(Calendar.MONTH) == newVal+1){
+                    if (mTempDate.get(Calendar.MONTH) == newVal + 1) {
                         mTempDate.set(Calendar.MONTH, newVal);
                         mTempDate.set(Calendar.DAY_OF_MONTH,
                         mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH));
                     }
-                } else if (picker == mWeekSpinner){
+                } else if (picker == mWeekSpinner) {
                     mTempDate.set(Calendar.WEEK_OF_YEAR, newVal);
-                } else if (picker == mYearSpinner && mYearEnabled){
+                } else if (picker == mYearSpinner && mYearEnabled) {
                     int month = mTempDate.get(Calendar.MONTH);
                     mTempDate.set(Calendar.YEAR, newVal);
                     if (month != mTempDate.get(Calendar.MONTH)) {
                         mTempDate.set(Calendar.MONTH, month);
                         mTempDate.set(Calendar.DAY_OF_MONTH,
                         mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH));
                     }
-                } else if (picker == mHourSpinner && mHourEnabled){
+                } else if (picker == mHourSpinner && mHourEnabled) {
                     if (mIs12HourMode) {
                         mTempDate.set(Calendar.HOUR, newVal);
                     } else {
                         mTempDate.set(Calendar.HOUR_OF_DAY, newVal);
                     }
-                } else if (picker == mMinuteSpinner && mMinuteEnabled){
+                } else if (picker == mMinuteSpinner && mMinuteEnabled) {
                     mTempDate.set(Calendar.MINUTE, newVal);
                 } else if (picker == mAMPMSpinner && mHourEnabled) {
                     mTempDate.set(Calendar.AM_PM, newVal);
                 } else {
                     throw new IllegalArgumentException();
                 }
             }
             setDate(mTempDate);
             if (mDayEnabled) {
                 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
             }
-            if(mWeekEnabled) {
+            if (mWeekEnabled) {
                 mWeekSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.WEEK_OF_YEAR));
             }
             updateCalendar();
             updateSpinners();
             notifyDateChanged();
         }
 
         private void setTempDate(int field, int oldVal, int newVal, int min, int max) {
@@ -418,17 +418,17 @@ public class DateTimePicker extends Fram
         NumberPicker mSpinner = (NumberPicker) findViewById(id);
         mSpinner.setMinValue(min);
         mSpinner.setMaxValue(max);
         mSpinner.setOnValueChangedListener(mOnChangeListener);
         mSpinner.setOnLongPressUpdateInterval(100);
         return mSpinner;
     }
 
-    public long getTimeInMillis(){
+    public long getTimeInMillis() {
         return mCurrentDate.getTimeInMillis();
     }
 
     private void reorderDateSpinners() {
         mDateSpinners.removeAllViews();
         char[] order = DateFormat.getDateFormatOrder(getContext());
         final int spinnerCount = order.length;
 
@@ -446,17 +446,17 @@ public class DateTimePicker extends Fram
                 default:
                     throw new IllegalArgumentException();
             }
         }
 
         mDateSpinners.addView(mWeekSpinner);
     }
 
-    void setDate(Calendar calendar){
+    void setDate(Calendar calendar) {
         mCurrentDate = mTempDate;
         if (mCurrentDate.before(mMinDate)) {
             mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
         } else if (mCurrentDate.after(mMaxDate)) {
             mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
         }
     }
 
@@ -537,17 +537,17 @@ public class DateTimePicker extends Fram
             }
         }
         if (mMinuteEnabled) {
             mMinuteSpinner.setValue(mCurrentDate.get(Calendar.MINUTE));
         }
     }
 
     void updateCalendar() {
-        if (mCalendarEnabled){
+        if (mCalendarEnabled) {
             mCalendar.setDate(mCurrentDate.getTimeInMillis(), false, false);
         }
     }
 
     void notifyDateChanged() {
         sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
     }
 
@@ -628,30 +628,30 @@ public class DateTimePicker extends Fram
         } else {
             mAMPMSpinner.setVisibility(GONE);
         }
     }
 
     private void setHourShown(boolean shown) {
         if (shown) {
             mHourSpinner.setVisibility(VISIBLE);
-            mHourEnabled= true;
+            mHourEnabled = true;
         } else {
             mHourSpinner.setVisibility(GONE);
             mAMPMSpinner.setVisibility(GONE);
             mTimeSpinners.setVisibility(GONE);
             mHourEnabled = false;
         }
     }
 
     private void setMinuteShown(boolean shown) {
         if (shown) {
             mMinuteSpinner.setVisibility(VISIBLE);
             mTimeSpinners.findViewById(R.id.mincolon).setVisibility(VISIBLE);
-            mMinuteEnabled= true;
+            mMinuteEnabled = true;
         } else {
             mMinuteSpinner.setVisibility(GONE);
             mTimeSpinners.findViewById(R.id.mincolon).setVisibility(GONE);
             mMinuteEnabled = false;
         }
     }
 
     private void setCurrentLocale(Locale locale) {
--- a/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/DefaultDoorHanger.java
@@ -111,17 +111,17 @@ public class DefaultDoorHanger extends D
                     PromptInput input = PromptInput.getInput(inputs.getJSONObject(i));
                     mInputs.add(input);
 
                     final int padding = mResources.getDimensionPixelSize(R.dimen.doorhanger_section_padding_medium);
                     View v = input.getView(getContext());
                     styleInput(input, v);
                     v.setPadding(0, 0, 0, padding);
                     group.addView(v);
-                } catch(JSONException ex) { }
+                } catch (JSONException ex) { }
             }
         }
 
         final String checkBoxText = options.optString("checkbox");
         if (!TextUtils.isEmpty(checkBoxText)) {
             mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox);
             mCheckBox.setText(checkBoxText);
             mCheckBox.setVisibility(VISIBLE);
--- a/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FaviconView.java
@@ -98,17 +98,17 @@ public class FaviconView extends ImageVi
             sStrokePaint.setStrokeWidth(sStrokeWidth);
         }
 
         mStrokeRect.left = mStrokeRect.top = sStrokeWidth;
         mBackgroundRect.left = mBackgroundRect.top = sStrokeWidth * 2.0f;
     }
 
     @Override
-    protected void onSizeChanged(int w, int h, int oldw, int oldh){
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
         super.onSizeChanged(w, h, oldw, oldh);
 
         // No point rechecking the image if there hasn't really been any change.
         if (w == mActualWidth && h == mActualHeight) {
             return;
         }
 
         mActualWidth = w;
@@ -168,17 +168,17 @@ public class FaviconView extends ImageVi
         } else {
             mDominantColor = 0;
         }
     }
 
     private void scaleBitmap() {
         // If the Favicon can be resized to fill the view exactly without an enlargment of more than
         // a factor of two, do so.
-        int doubledSize = mIconBitmap.getWidth()*2;
+        int doubledSize = mIconBitmap.getWidth() * 2;
         if (mActualWidth > doubledSize) {
             // If the view is more than twice the size of the image, just double the image size
             // and do the rest with padding.
             mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, doubledSize, doubledSize, true);
         } else {
             // Otherwise, scale the image to fill the view.
             mIconBitmap = Bitmap.createScaledBitmap(mIconBitmap, mActualWidth, mActualWidth, true);
         }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoActionProvider.java
@@ -351,15 +351,15 @@ public class GeckoActionProvider {
                     }
 
                     // Only alter the intent when we're sure everything has worked
                     intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(imageFile));
                 } finally {
                     IOUtils.safeStreamClose(is);
                 }
             }
-        } catch(IOException ex) {
+        } catch (IOException ex) {
             // If something went wrong, we'll just leave the intent un-changed
         } finally {
             IOUtils.safeStreamClose(os);
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/ThumbnailView.java
@@ -42,17 +42,17 @@ public class ThumbnailView extends Theme
 
         Drawable d = getDrawable();
         if (mLayoutChanged) {
             int w1 = d.getIntrinsicWidth();
             int h1 = d.getIntrinsicHeight();
             int w2 = getWidth();
             int h2 = getHeight();
 
-            float scale = (w2/h2 < w1/h1) ? (float)h2/h1 : (float)w2/w1;
+            float scale = ((w2 / h2) < (w1 / h1)) ? (float) h2 / h1 : (float) w2 / w1;
             mMatrix.setScale(scale, scale);
         }
 
         int saveCount = canvas.save();
         canvas.concat(mMatrix);
         d.draw(canvas);
         canvas.restoreToCount(saveCount);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/TwoWayView.java
@@ -2234,17 +2234,17 @@ public class TwoWayView extends AdapterV
             int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
 
             final int maxScrollAmount = getMaxScrollAmount();
             if (focusScroll < maxScrollAmount) {
                 // Not moving too far, safe to give next view focus
                 newFocus.requestFocus(direction);
                 mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
                 return mArrowScrollFocusResult;
-            } else if (distanceToView(newFocus) < maxScrollAmount){
+            } else if (distanceToView(newFocus) < maxScrollAmount) {
                 // Case to consider:
                 // Too far to get entire next focusable on screen, but by going
                 // max scroll amount, we are getting it at least partially in view,
                 // so give it focus and scroll the max amount.
                 newFocus.requestFocus(direction);
                 mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
                 return mArrowScrollFocusResult;
             }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -240,16 +240,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'db/Table.java',
     'db/TabsAccessor.java',
     'db/TabsProvider.java',
     'db/UrlAnnotations.java',
     'db/URLMetadata.java',
     'db/URLMetadataTable.java',
     'DevToolsAuthHelper.java',
     'distribution/Distribution.java',
+    'distribution/DistributionStoreCallback.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
     'dlc/BaseAction.java',
     'dlc/catalog/DownloadContent.java',
     'dlc/catalog/DownloadContentBootstrap.java',
     'dlc/catalog/DownloadContentBuilder.java',
     'dlc/catalog/DownloadContentCatalog.java',
     'dlc/DownloadAction.java',
--- a/netwerk/protocol/http/nsHttpHandler.cpp
+++ b/netwerk/protocol/http/nsHttpHandler.cpp
@@ -124,24 +124,34 @@ NewURI(const nsACString &aSpec,
 
     url.forget(aURI);
     return NS_OK;
 }
 
 #ifdef ANDROID
 static nsCString
 GetDeviceModelId() {
+    // Assumed to be running on the main thread
+    // We need the device property in either case
+    nsAutoCString deviceModelId;
     nsCOMPtr<nsIPropertyBag2> infoService = do_GetService("@mozilla.org/system-info;1");
     MOZ_ASSERT(infoService, "Could not find a system info service");
     nsAutoString androidDevice;
     nsresult rv = infoService->GetPropertyAsAString(NS_LITERAL_STRING("device"), androidDevice);
     if (NS_SUCCEEDED(rv)) {
-        return NS_LossyConvertUTF16toASCII(androidDevice);
+        deviceModelId = NS_LossyConvertUTF16toASCII(androidDevice);
     }
-    return EmptyCString();
+    nsAutoCString deviceString;
+    rv = Preferences::GetCString(UA_PREF("device_string"), &deviceString);
+    if (NS_SUCCEEDED(rv)) {
+        deviceString.Trim(" ", true, true);
+        deviceString.ReplaceSubstring(NS_LITERAL_CSTRING("%DEVICEID%"), deviceModelId);
+        return deviceString;
+    }
+    return deviceModelId;
 }
 #endif
 
 //-----------------------------------------------------------------------------
 // nsHttpHandler <public>
 //-----------------------------------------------------------------------------
 
 nsHttpHandler *gHttpHandler = nullptr;
--- a/toolkit/components/passwordmgr/test/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -12,25 +12,22 @@ support-files =
   subtst_notifications_11.html
   subtst_notifications_11_popup.html
   subtst_privbrowsing_1.html
   subtst_privbrowsing_2.html
   subtst_privbrowsing_3.html
   subtst_privbrowsing_4.html
   subtst_prompt_async.html
 
-[test_basic_form_2pw_2.html]
-[test_basic_form_autocomplete.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_master_password.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
+skip-if = toolkit == 'android' # Tests desktop prompts
 [test_master_password_cleanup.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
+skip-if = toolkit == 'android' # Tests desktop prompts
 [test_notifications_popup.html]
-skip-if = true || os == "linux" || toolkit == 'android' # bug 934057. Bug 1258975 on android.
+skip-if = true || os == "linux" || toolkit == 'android' # bug 934057. Tests desktop doorhangers
 [test_prompt.html]
-skip-if = os == "linux" || toolkit == 'android' # Bug 1258975 on android.
+skip-if = os == "linux" || toolkit == 'android' # Tests desktop prompts
 [test_prompt_async.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
+skip-if = toolkit == 'android' # Tests desktop prompts
 [test_xhr.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
+skip-if = toolkit == 'android' # Tests desktop prompts
 [test_xml_load.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
\ No newline at end of file
+skip-if = toolkit == 'android' # Tests desktop prompts
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -8,29 +8,31 @@ support-files =
   auth2/authenticate.sjs
 
 [test_autofill_password-only.html]
 [test_basic_form.html]
 [test_basic_form_0pw.html]
 [test_basic_form_1pw.html]
 [test_basic_form_1pw_2.html]
 [test_basic_form_2pw_1.html]
+[test_basic_form_2pw_2.html]
 [test_basic_form_3pw_1.html]
+[test_basic_form_autocomplete.html]
+skip-if = toolkit == 'android' || e10s # android:autocomplete. e10s:Requires code fix in bug 1258921.
 [test_basic_form_html5.html]
 [test_basic_form_pwevent.html]
 [test_basic_form_pwonly.html]
 [test_bug_627616.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
+skip-if = toolkit == 'android' # Tests desktop prompts
 [test_bug_776171.html]
 [test_case_differences.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_form_action_1.html]
 [test_form_action_2.html]
 [test_form_action_javascript.html]
 [test_formless_autofill.html]
 skip-if = toolkit == 'android' # Bug 1259768
 [test_input_events.html]
 [test_input_events_for_identical_values.html]
 [test_maxlength.html]
 [test_passwords_in_type_password.html]
 [test_recipe_login_fields.html]
-skip-if = (toolkit == 'android') # Bug 1258975 on android.
-[test_xhr_2.html]
\ No newline at end of file
+[test_xhr_2.html]
rename from toolkit/components/passwordmgr/test/test_basic_form_2pw_2.html
rename to toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
--- a/toolkit/components/passwordmgr/test/test_basic_form_2pw_2.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_2pw_2.html
@@ -81,49 +81,26 @@ function getFormSubmitButton(formNum) {
   // invoke the form onsubmit handler.
   var button = form.firstChild;
   while (button && button.type != "submit") { button = button.nextSibling; }
   ok(button != null, "getting form submit button");
 
   return button;
 }
 
-
-// Counts the number of logins currently stored by password manager.
-function countLogins() {
-  var logins = pwmgr.getAllLogins();
-
-  return logins.length;
-}
-
-commonInit();
+runChecksAfterCommonInit(startTest)
 
-// Get the pwmgr service
-var Cc_pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"];
-ok(Cc_pwmgr != null, "Access Cc[@mozilla.org/login-manager;1]");
-
-var Ci_pwmgr = SpecialPowers.Ci.nsILoginManager;
-ok(Ci_pwmgr != null, "Access Ci.nsILoginManager");
-
-var pwmgr = Cc_pwmgr.getService(Ci_pwmgr);
-ok(pwmgr != null, "pwmgr getService()");
-
-
-window.addEventListener("runTests", startTest);
-
-SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
 <div id="content" style="display: none">
   <form id="form1" onsubmit="return checkSubmit(1)" action="http://newuser.com">
     <input  type="text"     name="uname">
     <input  type="password" name="pword">
     <input  type="password" name="qword">
 
     <button type="submit">Submit</button>
     <button type="reset"> Reset </button>
   </form>
 
 </div>
 
 </body>
 </html>
-
rename from toolkit/components/passwordmgr/test/test_basic_form_autocomplete.html
rename to toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
--- a/toolkit/components/passwordmgr/test/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -1,115 +1,108 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
-  <title>Test basic autocomplete</title>
+  <title>Test basic login autocomplete</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="satchel_common.js"></script>
   <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 Login Manager test: multiple login autocomplete
 
 <script>
-commonInit();
-SimpleTest.waitForExplicitFinish();
+var chromeScript = runChecksAfterCommonInit();
+
+var setupScript = runInParent(function setup() {
+  const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+  Cu.import("resource://gre/modules/Services.jsm");
+
+  // Create some logins just for this form, since we'll be deleting them.
+  var nsLoginInfo = Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+                                           Ci.nsILoginInfo, "init");
+  assert.ok(nsLoginInfo != null, "nsLoginInfo constructor");
+
+  // login0 has no username, so should be filtered out from the autocomplete list.
+  var login0 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+                               "", "user0pass", "", "pword");
+
+  var login1 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+                               "tempuser1", "temppass1", "uname", "pword");
+
+  var login2 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+                               "testuser2", "testpass2", "uname", "pword");
+
+  var login3 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+                               "testuser3", "testpass3", "uname", "pword");
 
-// Get the pwmgr service
-var pwmgr = SpecialPowers.Cc["@mozilla.org/login-manager;1"]
-                         .getService(SpecialPowers.Ci.nsILoginManager);
-ok(pwmgr != null, "nsLoginManager service");
+  var login4 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete:8888", null,
+                               "zzzuser4", "zzzpass4", "uname", "pword");
+
+  // login 5 only used in the single-user forms
+  var login5 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete2", null,
+                               "singleuser5", "singlepass5", "uname", "pword");
+
+  var login6A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+                                "form7user1", "form7pass1", "uname", "pword");
+  var login6B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete3", null,
+                                "form7user2", "form7pass2", "uname", "pword");
 
-// Create some logins just for this form, since we'll be deleting them.
-var nsLoginInfo =
-SpecialPowers.wrap(SpecialPowers.Components).Constructor("@mozilla.org/login-manager/loginInfo;1",
-                          SpecialPowers.Ci.nsILoginInfo, "init");
-ok(nsLoginInfo != null, "nsLoginInfo constructor");
+  var login7  = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete4", null,
+                                "form8user", "form8pass", "uname", "pword");
+
+  var login8A = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+                                "form9userAB", "form9pass", "uname", "pword");
+  var login8B = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+                                "form9userAAB", "form9pass", "uname", "pword");
+  var login8C = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete5", null,
+                                "form9userAABzz", "form9pass", "uname", "pword");
+
+  var login9 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete6", null,
+                               "testuser9", "testpass9", "uname", "pword");
+
+  var login10 = new nsLoginInfo("http://mochi.test:8888", "http://autocomplete7", null,
+                                "testuser10", "testpass10", "uname", "pword");
 
 
-// login0 has no username, so should be filtered out from the autocomplete list.
-var login0 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete:8888", null,
-    "", "user0pass", "", "pword");
-
-var login1 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete:8888", null,
-    "tempuser1", "temppass1", "uname", "pword");
-
-var login2 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete:8888", null,
-    "testuser2", "testpass2", "uname", "pword");
-
-var login3 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete:8888", null,
-    "testuser3", "testpass3", "uname", "pword");
-
-var login4 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete:8888", null,
-    "zzzuser4", "zzzpass4", "uname", "pword");
-
-// login 5 only used in the single-user forms
-var login5 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete2", null,
-    "singleuser5", "singlepass5", "uname", "pword");
-
-var login6A = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete3", null,
-    "form7user1", "form7pass1", "uname", "pword");
-var login6B = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete3", null,
-    "form7user2", "form7pass2", "uname", "pword");
-
-var login7  = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete4", null,
-    "form8user", "form8pass", "uname", "pword");
+  // try/catch in case someone runs the tests manually, twice.
+  try {
+    Services.logins.addLogin(login0);
+    Services.logins.addLogin(login1);
+    Services.logins.addLogin(login2);
+    Services.logins.addLogin(login3);
+    Services.logins.addLogin(login4);
+    Services.logins.addLogin(login5);
+    Services.logins.addLogin(login6A);
+    Services.logins.addLogin(login6B);
+    Services.logins.addLogin(login7);
+    Services.logins.addLogin(login8A);
+    Services.logins.addLogin(login8B);
+    // login8C is added later
+    Services.logins.addLogin(login9);
+    Services.logins.addLogin(login10);
+  } catch (e) {
+    assert.ok(false, "addLogin threw: " + e);
+  }
 
-var login8A = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete5", null,
-    "form9userAB", "form9pass", "uname", "pword");
-
-var login8B = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete5", null,
-    "form9userAAB", "form9pass", "uname", "pword");
-
-// login8C is added later
-var login8C = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete5", null,
-    "form9userAABz", "form9pass", "uname", "pword");
-
-var login9 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete6", null,
-    "testuser9", "testpass9", "uname", "pword");
-
-var login10 = new nsLoginInfo(
-    "http://mochi.test:8888", "http://autocomplete7", null,
-    "testuser10", "testpass10", "uname", "pword");
-
-
-// try/catch in case someone runs the tests manually, twice.
-try {
-    pwmgr.addLogin(login0);
-    pwmgr.addLogin(login1);
-    pwmgr.addLogin(login2);
-    pwmgr.addLogin(login3);
-    pwmgr.addLogin(login4);
-    pwmgr.addLogin(login5);
-    pwmgr.addLogin(login6A);
-    pwmgr.addLogin(login6B);
-    pwmgr.addLogin(login7);
-    pwmgr.addLogin(login8A);
-    pwmgr.addLogin(login8B);
-    pwmgr.addLogin(login9);
-    pwmgr.addLogin(login10);
-} catch (e) {
-    ok(false, "addLogin threw: " + e);
-}
-
+  addMessageListener("addLogin", loginVariableName => {
+    let login = eval(loginVariableName);
+    assert.ok(!!login, "Login to add is defined: " + loginVariableName);
+    Services.logins.addLogin(login);
+  });
+  addMessageListener("removeLogin", loginVariableName => {
+    let login = eval(loginVariableName);
+    assert.ok(!!login, "Login to delete is defined: " + loginVariableName);
+    Services.logins.removeLogin(login);
+  });
+});
 </script>
 <p id="display"></p>
 
 <!-- we presumably can't hide the content for this test. -->
 <div id="content">
 
   <!-- form1 tests multiple matching logins -->
   <form id="form1" action="http://autocomplete:8888/formtest.js" onsubmit="return false;">
@@ -193,746 +186,647 @@ try {
    </div>
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Login Manager: multiple login autocomplete. **/
 
-var tester;
-
 var uname = $_(1, "uname");
 var pword = $_(1, "pword");
 const shiftModifier = SpecialPowers.Ci.nsIDOMEvent.SHIFT_MASK;
 
 // Restore the form to the default state.
 function restoreForm() {
-    uname.value = "";
-    pword.value = "";
-    uname.focus();
+  uname.value = "";
+  pword.value = "";
+  uname.focus();
 }
 
-
 // Check for expected username/password in form.
 function checkACForm(expectedUsername, expectedPassword) {
   var formID = uname.parentNode.id;
   is(uname.value, expectedUsername, "Checking " + formID + " username is: " + expectedUsername);
   is(pword.value, expectedPassword, "Checking " + formID + " password is: " + expectedPassword);
 }
 
-
 function sendFakeAutocompleteEvent(element) {
-    var acEvent = document.createEvent("HTMLEvents");
-    acEvent.initEvent("DOMAutoComplete", true, false);
-    element.dispatchEvent(acEvent);
+  var acEvent = document.createEvent("HTMLEvents");
+  acEvent.initEvent("DOMAutoComplete", true, false);
+  element.dispatchEvent(acEvent);
 }
 
-function hitEventLoop(func, times) {
-  if (times > 0) {
-    setTimeout(hitEventLoop, 0, func, times - 1);
-  } else {
-    setTimeout(func, 0);
-  }
-}
-
-function addPopupListener(eventName, func, capture) {
-  autocompletePopup.addEventListener(eventName, func, capture);
-}
-
-function removePopupListener(eventName, func, capture) {
-  autocompletePopup.removeEventListener(eventName, func, capture);
+function spinEventLoop() {
+  return Promise.resolve();
 }
 
-/*
- * Main section of test...
- *
- * This test is, to a first approximation, event driven. Each time we need to
- * wait for an event, runTest sets an event listener (or timeout for a couple
- * of rare cases) and yields. The event listener then resumes the generator by
- * calling its |next| method.
- */
-function* runTest() {
-  var testNum = 1;
-  ok(true, "Starting test #" + testNum);
-
-  function waitForPopup() {
-    addPopupListener("popupshown", function popupshown() {
-      removePopupListener("popupshown", popupshown, false);
-
-      window.setTimeout(tester.next.bind(tester), 0);
-    }, false);
-  }
-
-  function runNextTest(expectPopup) {
-    var save = testNum++;
-    if (expectPopup === "expect popup")
-      return waitForPopup();
-
-    var unexpectedPopup = function() {
-      removePopupListener("popupshown", unexpectedPopup, false);
-      ok(false, "Test " + save + " should not show a popup");
-    };
-    addPopupListener("popupshown", unexpectedPopup, false);
+add_task(function* setup() {
+  listenForUnexpectedPopupShown();
+});
 
-    hitEventLoop(function() {
-      removePopupListener("popupshown", unexpectedPopup, false);
-      tester.next();
-    }, 100);
-
-    return undefined;
-  }
-
-  // We use this function when we're trying to prove that something doesn't
-  // happen, but where if it did it would do so asynchronously. It isn't
-  // perfect, but it's better than nothing.
-  function spinEventLoop() {
-    setTimeout(function() { tester.next(); }, 0);
-  }
+add_task(function* test_form1_initial_empty() {
+  yield SimpleTest.promiseFocus(window);
 
-  function waitForCompletion() {
-    var observer = SpecialPowers.wrapCallback(function(subject, topic, data) {
-      SpecialPowers.removeObserver(observer, "passwordmgr-processed-form");
-      tester.next();
-    });
-    SpecialPowers.addObserver(observer, "passwordmgr-processed-form", false);
-  }
-
-  function getLoginRecipes() {
-    getRecipeParent().then(function (recipeParent) {
-      tester.next(recipeParent);
-    });
-  }
-
-  /* test 1 */
   // Make sure initial form is empty.
   checkACForm("", "");
+  let popupState = yield getPopupState();
+  is(popupState.open, false, "Check popup is initially closed");
+  is(popupState.selectedIndex, -1, "Check no entries are selected");
+});
+
+add_task(function* test_form1_first_entry() {
+  yield SimpleTest.promiseFocus(window);
+  // Trigger autocomplete popup
+  restoreForm();
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
+  doKey("down"); // first
+  checkACForm("", ""); // value shouldn't update just by selecting
+  doKey("return"); // not "enter"!
+  yield promiseFormsProcessed();
+  checkACForm("tempuser1", "temppass1");
+});
+
+add_task(function* test_form1_second_entry() {
+  // Trigger autocomplete popup
+  restoreForm();
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
+
+  doKey("down"); // first
+  doKey("down"); // second
+  doKey("return"); // not "enter"!
+  yield promiseFormsProcessed();
+  checkACForm("testuser2", "testpass2");
+});
+
+add_task(function* test_form1_third_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 2 */
-  // Check first entry
-  doKey("down");
-  checkACForm("", ""); // value shouldn't update
-  doKey("return"); // not "enter"!
-  yield waitForCompletion();
-  checkACForm("tempuser1", "temppass1");
+  doKey("down"); // first
+  doKey("down"); // second
+  doKey("down"); // third
+  doKey("return");
+  yield promiseFormsProcessed();
+  checkACForm("testuser3", "testpass3");
+});
 
+add_task(function* test_form1_fourth_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
-
-  /* test 3 */
-  // Check second entry
-  doKey("down");
-  doKey("down");
-  doKey("return"); // not "enter"!
-  yield waitForCompletion();
-  checkACForm("testuser2", "testpass2");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  // Trigger autocomplete popup
-  restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  doKey("down"); // first
+  doKey("down"); // second
+  doKey("down"); // third
+  doKey("down"); // fourth
+  doKey("return");
+  yield promiseFormsProcessed();
+  checkACForm("zzzuser4", "zzzpass4");
+});
 
-  /* test 4 */
-  // Check third entry
-  doKey("down");
-  doKey("down");
-  doKey("down");
-  doKey("return");
-  yield waitForCompletion();
-  checkACForm("testuser3", "testpass3");
-
+add_task(function* test_form1_wraparound_first_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  yield spinEventLoop(); // let focus happen
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 5 */
-  // Check fourth entry
-  doKey("down");
-  doKey("down");
-  doKey("down");
-  doKey("down");
+  doKey("down"); // first
+  doKey("down"); // second
+  doKey("down"); // third
+  doKey("down"); // fourth
+  doKey("down"); // deselects
+  doKey("down"); // first
   doKey("return");
-  yield waitForCompletion();
-  checkACForm("zzzuser4", "zzzpass4");
+  yield promiseFormsProcessed();
+  checkACForm("tempuser1", "temppass1");
+});
 
+add_task(function* test_form1_wraparound_up_last_entry() {
   // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 6 */
-  // Check first entry (wraparound)
-  doKey("down");
-  doKey("down");
-  doKey("down");
-  doKey("down");
-  doKey("down"); // deselects
-  doKey("down");
+  doKey("up"); // last (fourth)
   doKey("return");
-  yield waitForCompletion();
-  checkACForm("tempuser1", "temppass1");
+  yield promiseFormsProcessed();
+  checkACForm("zzzuser4", "zzzpass4");
+});
 
+add_task(function* test_form1_wraparound_down_up_up() {
   // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 7 */
-  // Check the last entry via arrow-up
-  doKey("up");
-  doKey("return");
-  yield waitForCompletion();
-  checkACForm("zzzuser4", "zzzpass4");
-
-  // Trigger autocomplete popup
-  restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
-
-  /* test 8 */
-  // Check the last entry via arrow-up
   doKey("down"); // select first entry
   doKey("up");   // selects nothing!
   doKey("up");   // select last entry
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_wraparound_up_last() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 9 */
-  // Check the last entry via arrow-up (wraparound)
   doKey("down");
   doKey("up"); // deselects
   doKey("up"); // last entry
   doKey("up");
   doKey("up");
   doKey("up"); // first entry
   doKey("up"); // deselects
   doKey("up"); // last entry
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_fill_username_without_autofill_right() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 10 */
   // Set first entry w/o triggering autocomplete
-  doKey("down");
+  doKey("down"); // first
   doKey("right");
   yield spinEventLoop();
   checkACForm("tempuser1", ""); // empty password
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_fill_username_without_autofill_left() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 11 */
   // Set first entry w/o triggering autocomplete
-  doKey("down");
+  doKey("down"); // first
   doKey("left");
   checkACForm("tempuser1", ""); // empty password
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_pageup_first() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 12 */
   // Check first entry (page up)
-  doKey("down");
-  doKey("down");
-  doKey("page_up");
+  doKey("down"); // first
+  doKey("down"); // second
+  doKey("page_up"); // first
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("tempuser1", "temppass1");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_pagedown_last() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   /* test 13 */
   // Check last entry (page down)
-  doKey("down");
-  doKey("page_down");
+  doKey("down"); // first
+  doKey("page_down"); // last
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
+});
+
+add_task(function* test_form1_untrusted_event() {
   restoreForm();
-  yield runNextTest();
+  yield spinEventLoop();
 
-  /* test 14 */
   // Send a fake (untrusted) event.
   checkACForm("", "");
   uname.value = "zzzuser4";
   sendFakeAutocompleteEvent(uname);
   yield spinEventLoop();
   checkACForm("zzzuser4", "");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_delete() {
   restoreForm();
-  doKey("down");
-  testNum = 49;
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   // XXX tried sending character "t" before/during dropdown to test
   // filtering, but had no luck. Seemed like the character was getting lost.
   // Setting uname.value didn't seem to work either. This works with a human
   // driver, so I'm not sure what's up.
 
-
-  /* test 50 */
   // Delete the first entry (of 4), "tempuser1"
   doKey("down");
   var numLogins;
-  numLogins = pwmgr.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 5, "Correct number of logins before deleting one");
 
+  var deletionPromise = promiseStorageChanged(["removeLogin"]);
   // On OS X, shift-backspace and shift-delete work, just delete does not.
   // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
   doKey("delete", shiftModifier);
+  yield deletionPromise;
 
   checkACForm("", "");
-  numLogins = pwmgr.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 4, "Correct number of logins after deleting one");
+  notifyMenuChanged(4);
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_first_after_deletion() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 51 */
   // Check the new first entry (of 3)
   doKey("down");
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_delete_second() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 52 */
   // Delete the second entry (of 3), "testuser3"
   doKey("down");
   doKey("down");
   doKey("delete", shiftModifier);
   checkACForm("", "");
-  numLogins = pwmgr.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 3, "Correct number of logins after deleting one");
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_first_after_deletion2() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 53 */
-  // Check the new second entry (of 2)
+  // Check the new first entry (of 2)
   doKey("down");
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_delete_last() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   /* test 54 */
   // Delete the last entry (of 2), "zzzuser4"
   doKey("down");
   doKey("down");
   doKey("delete", shiftModifier);
   checkACForm("", "");
-  numLogins = pwmgr.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 2, "Correct number of logins after deleting one");
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_first_after_3_deletions() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 55 */
-  // Check the new second entry (of 2)
+  // Check the only remaining entry
   doKey("down");
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form1_check_only_entry_remaining() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   /* test 56 */
   // Delete the only remaining entry, "testuser2"
   doKey("down");
   doKey("delete", shiftModifier);
   //doKey("return");
   checkACForm("", "");
-  numLogins = pwmgr.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
+  numLogins = countLogins(chromeScript, "http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 1, "Correct number of logins after deleting one");
-  pwmgr.removeLogin(login0); // remove the login that's not shown in the list.
-  testNum = 99;
-  yield runNextTest();
 
+  // remove the login that's not shown in the list.
+  setupScript.sendSyncMessage("removeLogin", "login0");
+});
 
-  /* Tests for single-user forms for ignoring autocomplete=off */
-
-  /* test 100 */
+/* Tests for single-user forms for ignoring autocomplete=off */
+add_task(function* test_form2() {
   // Turn our attention to form2
   uname = $_(2, "uname");
   pword = $_(2, "pword");
   checkACForm("singleuser5", "singlepass5");
 
-  // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 101 */
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
-  restoreForm(); // clear field, so reloading test doesn't fail
-  yield runNextTest();
+});
 
-  /* test 102 */
-  // Turn our attention to form3
+add_task(function* test_form3() {
   uname = $_(3, "uname");
   pword = $_(3, "pword");
   checkACForm("singleuser5", "singlepass5");
+  restoreForm();
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  // Trigger autocomplete popup
-  restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
-
-  /* test 103 */
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
-  yield runNextTest();
+});
 
-  /* test 104 */
-  // Turn our attention to form4
+add_task(function* test_form4() {
   uname = $_(4, "uname");
   pword = $_(4, "pword");
   checkACForm("singleuser5", "singlepass5");
+  restoreForm();
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  // Trigger autocomplete popup
-  restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
-
-  /* test 105 */
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
-  yield runNextTest();
+});
 
-  /* test 106 */
-  // Turn our attention to form5
+add_task(function* test_form5() {
   uname = $_(5, "uname");
   pword = $_(5, "pword");
   checkACForm("singleuser5", "singlepass5");
+  restoreForm();
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  // Trigger autocomplete popup
-  restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
-
-  /* test 107 */
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
-  yield runNextTest();
+});
 
-  /* test 108 */
-  // Turn our attention to form6
+add_task(function* test_form6() {
   // (this is a control, w/o autocomplete=off, to ensure the login
   // that was being suppressed would have been filled in otherwise)
   uname = $_(6, "uname");
   pword = $_(6, "pword");
   checkACForm("singleuser5", "singlepass5");
-  yield runNextTest();
+});
 
-  /* test 109 */
+add_task(function* test_form6_changeUsername() {
   // Test that the password field remains filled in after changing
   // the username.
   uname.focus();
   doKey("right");
   sendChar("X");
   // Trigger the 'blur' event on uname
   pword.focus();
   yield spinEventLoop();
   checkACForm("sXingleuser5", "singlepass5");
 
-  pwmgr.removeLogin(login5);
-  testNum = 499;
-  yield runNextTest();
+  setupScript.sendSyncMessage("removeLogin", "login5");
+});
 
-  /* test 500 */
-  // Turn our attention to form7
+add_task(function* test_form7() {
   uname = $_(7, "uname");
   pword = $_(7, "pword");
   checkACForm("", "");
 
   // Insert a new username field into the form. We'll then make sure
   // that invoking the autocomplete doesn't try to fill the form.
   var newField = document.createElement("input");
   newField.setAttribute("type", "text");
   newField.setAttribute("name", "uname2");
   pword.parentNode.insertBefore(newField, pword);
   is($_(7, "uname2").value, "", "Verifying empty uname2");
 
   // Delete login6B. It was created just to prevent filling in a login
   // automatically, removing it makes it more likely that we'll catch a
   // future regression with form filling here.
-  pwmgr.removeLogin(login6B);
+  setupScript.sendSyncMessage("removeLogin", "login6B");
+});
 
-  // Trigger autocomplete popup
+add_task(function* test_form7_2() {
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
-  /* test 501 */
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   // The form changes, so we expect the old username field to get the
   // selected autocomplete value, but neither the new username field nor
   // the password field should have any values filled in.
   yield spinEventLoop();
   checkACForm("form7user1", "");
   is($_(7, "uname2").value, "", "Verifying empty uname2");
   restoreForm(); // clear field, so reloading test doesn't fail
 
-  pwmgr.removeLogin(login6A);
-  testNum = 599;
-  yield runNextTest();
+  setupScript.sendSyncMessage("removeLogin", "login6A");
+});
 
-  /* test 600 */
-  // Turn our attention to form8
+add_task(function* test_form8() {
   uname = $_(8, "uname");
   pword = $_(8, "pword");
   checkACForm("form8user", "form8pass");
   restoreForm();
-  yield runNextTest();
+});
 
-  /* test 601 */
+add_task(function* test_form8_blur() {
   checkACForm("", "");
   // Focus the previous form to trigger a blur.
   $_(7, "uname").focus();
-  yield runNextTest();
+});
 
-  /* test 602 */
+add_task(function* test_form8_2() {
   checkACForm("", "");
   restoreForm();
-  yield runNextTest();
+});
 
-  /* test 603 */
+add_task(function* test_form8_3() {
   checkACForm("", "");
-  pwmgr.removeLogin(login7);
+  setupScript.sendSyncMessage("removeLogin", "login7");
+});
 
-  testNum = 699;
-  yield runNextTest();
-
-  /* test 700 */
+add_task(function* test_form9_filtering() {
   // Turn our attention to form9 to test the dropdown - bug 497541
   uname = $_(9, "uname");
   pword = $_(9, "pword");
   uname.focus();
+  let shownPromise = promiseACShown();
   sendString("form9userAB");
-  yield runNextTest("expect popup");
+  yield shownPromise;
 
-  /* test 701 */
   checkACForm("form9userAB", "");
   uname.focus();
   doKey("left");
+  shownPromise = promiseACShown();
   sendChar("A");
-  yield runNextTest("expect popup");
+  let results = yield shownPromise;
 
-  /* test 702 */
-  // check dropdown is updated after inserting "A"
   checkACForm("form9userAAB", "");
-  checkMenuEntries(["form9userAAB"]);
+  checkArrayValues(results, ["form9userAAB"], "Check dropdown is updated after inserting 'A'");
   doKey("down");
   doKey("return");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("form9userAAB", "form9pass");
-  yield runNextTest();
+});
 
-  /* test 703 */
+add_task(function* test_form9_autocomplete_cache() {
   // Note that this addLogin call will only be seen by the autocomplete
   // attempt for the sendChar if we do not successfully cache the
   // autocomplete results.
-  pwmgr.addLogin(login8C);
+  setupScript.sendSyncMessage("addLogin", "login8C");
   uname.focus();
+  let promise0 = notifyMenuChanged(0);
   sendChar("z");
-  yield runNextTest();
+  yield promise0;
+  let popupState = yield getPopupState();
+  is(popupState.open, false, "Check popup shouldn't open");
 
-  /* test 704 */
   // check that empty results are cached - bug 496466
-  checkMenuEntries([]);
+  promise0 = notifyMenuChanged(0);
+  sendChar("z");
+  yield promise0;
+  popupState = yield getPopupState();
+  is(popupState.open, false, "Check popup stays closed due to cached empty result");
+});
 
-  /* test 705 */
+add_task(function* test_form10_formSubmitURLScheme() {
   // Check that formSubmitURL with different schemes matches
-  // Turn our attention to form10
   uname = $_(10, "uname");
   pword = $_(10, "pword");
-
-  // Trigger autocomplete popup
   restoreForm();
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser9", "testpass9");
-  yield runNextTest();
+});
 
-  // Turn our attention to form11 to test recipes
-  var recipeParent = yield getLoginRecipes();
-  recipeParent.add({
-    "hosts": ["mochi.test:8888"],
-    "usernameSelector": "input[name='1']",
-    "passwordSelector": "input[name='2']"
+add_task(function* test_form11_recipes() {
+  yield loadRecipes({
+    siteRecipes: [{
+      "hosts": ["mochi.test:8888"],
+      "usernameSelector": "input[name='1']",
+      "passwordSelector": "input[name='2']"
+    }],
   });
   uname = $_(11, "1");
   pword = $_(11, "2");
 
   // First test DOMAutocomplete
   // Switch the password field to type=password so _fillForm marks the username
   // field for autocomplete.
   pword.type = "password";
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   restoreForm();
   checkACForm("", "");
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser10", "testpass10");
 
   // Now test recipes with blur on the username field.
   restoreForm();
   checkACForm("", "");
   uname.value = "testuser10";
   checkACForm("testuser10", "");
   doKey("tab");
-  yield waitForCompletion();
+  yield promiseFormsProcessed();
   checkACForm("testuser10", "testpass10");
+  yield resetRecipes();
+});
 
-  recipeParent.reset();
-  yield runNextTest();
-
+add_task(function* test_form12_formless() {
   // Test form-less autocomplete
-  uname = $_(12, "uname")
-  pword = $_(12, "pword")
+  uname = $_(12, "uname");
+  pword = $_(12, "pword");
   restoreForm();
   checkACForm("", "");
-  // Trigger autocomplete popup
-  doKey("down");
-  yield runNextTest("expect popup");
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
 
   // Trigger autocomplete
   doKey("down");
   checkACForm("", ""); // value shouldn't update
+  let processedPromise = promiseFormsProcessed();
   doKey("return"); // not "enter"!
-  yield waitForCompletion();
+  yield processedPromise;
   checkACForm("testuser", "testpass");
-  yield runNextTest();
-
-  SimpleTest.finish();
-  return;
-}
-
-
-function checkMenuEntries(expectedValues) {
-    var actualValues = getMenuEntries();
-    is(actualValues.length, expectedValues.length, "Checking length of expected menu");
-    for (var i = 0; i < expectedValues.length; i++)
-        is(actualValues[i], expectedValues[i], "Checking menu entry #"+i);
-}
-
-var autocompletePopup;
-function getMenuEntries() {
-    var entries = [];
-
-    // Could perhaps pull values directly from the controller, but it seems
-    // more reliable to test the values that are actually in the tree?
-    var column = autocompletePopup.tree.columns[0];
-    var numRows = autocompletePopup.tree.view.rowCount;
-    for (var i = 0; i < numRows; i++) {
-        entries.push(autocompletePopup.tree.view.getValueAt(i, column));
-    }
-    return entries;
-}
-
-function startTest() {
-    var Ci = SpecialPowers.Ci;
-    chromeWin = SpecialPowers.wrap(window)
-                    .QueryInterface(Ci.nsIInterfaceRequestor)
-                    .getInterface(Ci.nsIWebNavigation)
-                    .QueryInterface(Ci.nsIDocShellTreeItem)
-                    .rootTreeItem
-                    .QueryInterface(Ci.nsIInterfaceRequestor)
-                    .getInterface(Ci.nsIDOMWindow)
-                    .QueryInterface(Ci.nsIDOMChromeWindow);
-    // shouldn't reach into browser internals like this and
-    // shouldn't assume ID is consistent across products
-    autocompletePopup = chromeWin.document.getElementById("PopupAutoComplete");
-    ok(autocompletePopup, "Got autocomplete popup");
-    tester = runTest();
-    tester.next();
-}
-
-window.addEventListener("runTests", startTest);
+});
 </script>
 </pre>
 </body>
 </html>
-
--- a/toolkit/components/passwordmgr/test/pwmgr_common.js
+++ b/toolkit/components/passwordmgr/test/pwmgr_common.js
@@ -297,16 +297,34 @@ function resetRecipes() {
     chromeScript.addMessageListener("recipesReset", function reset() {
       chromeScript.removeMessageListener("recipesReset", reset);
       resolve();
     });
     chromeScript.sendAsyncMessage("resetRecipes");
   });
 }
 
+function promiseStorageChanged(expectedChangeTypes) {
+  return new Promise((resolve, reject) => {
+    let onStorageChanged = SpecialPowers.wrapCallback(function osc(subject, topic, data) {
+      let changeType = expectedChangeTypes.shift();
+      is(data, changeType, "Check expected passwordmgr-storage-changed type");
+      if (expectedChangeTypes.length === 0) {
+        SpecialPowers.removeObserver(onStorageChanged, "passwordmgr-storage-changed");
+        resolve(subject);
+      }
+    });
+    SpecialPowers.addObserver(onStorageChanged, "passwordmgr-storage-changed", false);
+  });
+}
+
+function countLogins(chromeScript, formOrigin, submitOrigin, httpRealm) {
+  return chromeScript.sendSyncMessage("countLogins", {formOrigin, submitOrigin, httpRealm})[0][0];
+}
+
 /**
  * Run a function synchronously in the parent process and destroy it in the test cleanup function.
  * @param {Function|String} aFunctionOrURL - either a function that will be stringified and run
  *                                           or the URL to a JS file.
  * @return {Object} - the return value of loadChromeScript providing message-related methods.
  *                    @see loadChromeScript in specialpowersAPI.js
  */
 function runInParent(aFunctionOrURL) {
@@ -336,16 +354,17 @@ function runChecksAfterCommonInit(aFunct
 // Code to run when loaded as a chrome script in tests via loadChromeScript
 if (this.addMessageListener) {
   const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
   var SpecialPowers = { Cc, Ci, Cr, Cu, };
   var ok, is;
   // Ignore ok/is in commonInit since they aren't defined in a chrome script.
   ok = is = () => {}; // eslint-disable-line no-native-reassign
 
+  Cu.import("resource://gre/modules/Services.jsm");
   Cu.import("resource://gre/modules/Task.jsm");
 
   addMessageListener("setupParent", ({selfFilling = false} = {selfFilling: false}) => {
     commonInit(selfFilling);
     sendAsyncMessage("doneSetup");
   });
 
   addMessageListener("loadRecipes", Task.async(function* loadRecipes(recipes) {
@@ -357,16 +376,20 @@ if (this.addMessageListener) {
 
   addMessageListener("resetRecipes", Task.async(function* resetRecipes() {
     let { LoginManagerParent } = Cu.import("resource://gre/modules/LoginManagerParent.jsm", {});
     let recipeParent = yield LoginManagerParent.recipeParentPromise;
     yield recipeParent.reset();
     sendAsyncMessage("recipesReset");
   }));
 
+  addMessageListener("countLogins", ({formOrigin, submitOrigin, httpRealm}) => {
+    return Services.logins.countLogins(formOrigin, submitOrigin, httpRealm);
+  });
+
   var globalMM = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
   globalMM.addMessageListener("RemoteLogins:onFormSubmit", function onFormSubmit(message) {
     sendAsyncMessage("formSubmissionProcessed", message.data, message.objects);
   });
 } else {
   // Code to only run in the mochitest pages (not in the chrome script).
   SimpleTest.registerCleanupFunction(() => {
     runInParent(function cleanupParent() {
--- a/toolkit/components/satchel/test/satchel_common.js
+++ b/toolkit/components/satchel/test/satchel_common.js
@@ -1,12 +1,13 @@
 /* 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/. */
 
+var gPopupShownExpected = false;
 var gPopupShownListener;
 var gLastAutoCompleteResults;
 var gChromeScript;
 
 /*
  * Returns the element with the specified |name| attribute.
  */
 function $_(formNum, name) {
@@ -220,23 +221,33 @@ function getPopupState(then = null) {
       if (then) {
         then(state);
       }
       resolve(state);
     });
   });
 }
 
+function listenForUnexpectedPopupShown() {
+  gChromeScript.addMessageListener("onpopupshown", function onPopupShown() {
+    if (!gPopupShownExpected) {
+      ok(false, "Unexpected autocomplete popupshown event");
+    }
+  });
+}
+
 /**
  * Resolve at the next popupshown event for the autocomplete popup
  * @return {Promise} with the results
  */
 function promiseACShown() {
+  gPopupShownExpected = true;
   return new Promise(resolve => {
     gChromeScript.addMessageListener("onpopupshown", ({ results }) => {
+      gPopupShownExpected = false;
       resolve(results);
     });
   });
 }
 
 function satchelCommonSetup() {
   var chromeURL = SimpleTest.getTestFileURL("parent_utils.js");
   gChromeScript = SpecialPowers.loadChromeScript(chromeURL);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8704,17 +8704,17 @@
   },
   "E10S_BLOCKED_FROM_RUNNING": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Whether the e10s pref was set but it was blocked from running due to blacklisted conditions"
   },
   "E10S_ADDONS_BLOCKER_RAN": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "48",
+    "expires_in_version": "49",
     "kind": "flag",
     "releaseChannelCollection": "opt-out",
     "bug_numbers": [1248796],
     "description": "Whether the code to block e10s for add-ons users was called"
   },
   "BLOCKED_ON_PLUGIN_MODULE_INIT_MS": {
     "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
     "expires_in_version": "never",
--- a/toolkit/modules/Console.jsm
+++ b/toolkit/modules/Console.jsm
@@ -278,16 +278,17 @@ function logProperty(aProp, aValue) {
   return reply;
 }
 
 const LOG_LEVELS = {
   "all": Number.MIN_VALUE,
   "debug": 2,
   "log": 3,
   "info": 3,
+  "clear": 3,
   "trace": 3,
   "timeEnd": 3,
   "time": 3,
   "group": 3,
   "groupEnd": 3,
   "dir": 3,
   "dirxml": 3,
   "warn": 4,
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -302,16 +302,37 @@ function getLocale() {
   try {
     return Services.prefs.getCharPref(PREF_SELECTED_LOCALE);
   }
   catch (e) { }
 
   return "en-US";
 }
 
+function webAPIForAddon(addon) {
+  if (!addon) {
+    return null;
+  }
+
+  let result = {};
+
+  // By default just pass through any plain property, the webidl will control
+  // access.
+  for (let prop in addon) {
+    if (typeof(addon[prop]) != "function") {
+      result[prop] = addon[prop];
+    }
+  }
+
+  // A few properties are computed for a nicer API
+  result.isEnabled = !addon.userDisabled;
+
+  return result;
+}
+
 /**
  * A helper class to repeatedly call a listener with each object in an array
  * optionally checking whether the object has a method in it.
  *
  * @param  aObjects
  *         The array of objects to iterate through
  * @param  aMethod
  *         An optional method name, if not null any objects without this method
@@ -2756,16 +2777,26 @@ var AddonManagerInternal = {
     if (aValue != gUpdateEnabled)
       Services.prefs.setBoolPref(PREF_EM_UPDATE_ENABLED, aValue);
     return aValue;
   },
 
   get hotfixID() {
     return gHotfixID;
   },
+
+  webAPI: {
+    getAddonByID(id) {
+      return new Promise(resolve => {
+        AddonManager.getAddonByID(id, (addon) => {
+          resolve(webAPIForAddon(addon));
+        });
+      });
+    }
+  },
 };
 
 /**
  * Should not be used outside of core Mozilla code. This is a private API for
  * the startup and platform integration code to use. Refer to the methods on
  * AddonManagerInternal for documentation however note that these methods are
  * subject to change at any time.
  */
@@ -3337,16 +3368,20 @@ this.AddonManager = {
   escapeAddonURI: function(aAddon, aUri, aAppVersion) {
     return AddonManagerInternal.escapeAddonURI(aAddon, aUri, aAppVersion);
   },
 
   getPreferredIconURL: function(aAddon, aSize, aWindow = undefined) {
     return AddonManagerInternal.getPreferredIconURL(aAddon, aSize, aWindow);
   },
 
+  get webAPI() {
+    return AddonManagerInternal.webAPI;
+  },
+
   get shutdown() {
     return gShutdownBarrier.client;
   },
 };
 
 // load the timestamps module into AddonManagerInternal
 Cu.import("resource://gre/modules/TelemetryTimestamps.jsm", AddonManagerInternal);
 Object.freeze(AddonManagerInternal);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "AddonManagerWebAPI.h"
+
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/NavigatorBinding.h"
+
+#include "mozilla/Preferences.h"
+#include "nsGlobalWindow.h"
+
+#include "nsIDocShell.h"
+#include "nsIScriptObjectPrincipal.h"
+
+namespace mozilla {
+using namespace mozilla::dom;
+
+// Checks if the given uri is secure and matches one of the hosts allowed to
+// access the API.
+bool
+AddonManagerWebAPI::IsValidSite(nsIURI* uri)
+{
+  if (!uri) {
+    return false;
+  }
+
+  bool isSecure;
+  nsresult rv = uri->SchemeIs("https", &isSecure);
+  if (NS_FAILED(rv) || !isSecure) {
+    return false;
+  }
+
+  nsCString host;
+  rv = uri->GetHost(host);
+  if (NS_FAILED(rv)) {
+    return false;
+  }
+
+  if (host.Equals("addons.mozilla.org") ||
+      host.Equals("services.addons.mozilla.org")) {
+    return true;
+  }
+
+  // When testing allow access to the developer sites.
+  if (Preferences::GetBool("extensions.webapi.testing", false)) {
+    if (host.Equals("addons.allizom.org") ||
+        host.Equals("services.addons.allizom.org") ||
+        host.Equals("addons-dev.allizom.org") ||
+        host.Equals("services.addons-dev.allizom.org") ||
+        host.Equals("example.com")) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool
+AddonManagerWebAPI::IsAPIEnabled(JSContext* cx, JSObject* obj)
+{
+  nsGlobalWindow* global = xpc::WindowGlobalOrNull(obj);
+  if (!global) {
+    return false;
+  }
+
+  nsCOMPtr<nsPIDOMWindowInner> win = global->AsInner();
+  if (!win) {
+    return false;
+  }
+
+  // Check that the current window and all parent frames are allowed access to
+  // the API.
+  while (win) {
+    nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(win);
+    if (!sop) {
+      return false;
+    }
+
+    nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+    if (!principal) {
+      return false;
+    }
+
+    // Reaching a window with a system principal means we have reached
+    // privileged UI of some kind so stop at this point and allow access.
+    if (principal->GetIsSystemPrincipal()) {
+      return true;
+    }
+
+    nsCOMPtr<nsIDocShell> docShell = win->GetDocShell();
+    if (!docShell) {
+      // This window has been torn down so don't allow access to the API.
+      return false;
+    }
+
+    if (!IsValidSite(win->GetDocumentURI())) {
+      return false;
+    }
+
+    // Checks whether there is a parent frame of the same type. This won't cross
+    // mozbrowser or chrome boundaries.
+    nsCOMPtr<nsIDocShellTreeItem> parent;
+    nsresult rv = docShell->GetSameTypeParent(getter_AddRefs(parent));
+    if (NS_FAILED(rv)) {
+      return false;
+    }
+
+    if (!parent) {
+      // No parent means we've hit a mozbrowser or chrome boundary so allow
+      // access to the API.
+      return true;
+    }
+
+    nsIDocument* doc = win->GetDoc();
+    if (!doc) {
+      return false;
+    }
+
+    doc = doc->GetParentDocument();
+    if (!doc) {
+      // Getting here means something has been torn down so fail safe.
+      return false;
+    }
+
+
+    win = doc->GetInnerWindow();
+  }
+
+  // Found a document with no inner window, don't grant access to the API.
+  return false;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#include "nsPIDOMWindow.h"
+
+namespace mozilla {
+
+class AddonManagerWebAPI {
+public:
+  static bool IsAPIEnabled(JSContext* cx, JSObject* obj);
+
+private:
+  static bool IsValidSite(nsIURI* uri);
+};
+
+} // namespace mozilla
--- a/toolkit/mozapps/extensions/addonManager.js
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -19,38 +19,40 @@ const USER_CANCELLED    = -210;
 const DOWNLOAD_ERROR    = -228;
 const UNSUPPORTED_TYPE  = -244;
 const SUCCESS           = 0;
 
 const MSG_INSTALL_ENABLED  = "WebInstallerIsInstallEnabled";
 const MSG_INSTALL_ADDONS   = "WebInstallerInstallAddonsFromWebpage";
 const MSG_INSTALL_CALLBACK = "WebInstallerInstallCallback";
 
+const MSG_PROMISE_REQUEST  = "WebAPIPromiseRequest";
+const MSG_PROMISE_RESULT   = "WebAPIPromiseResult";
+
 const CHILD_SCRIPT = "resource://gre/modules/addons/Content.js";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 var gSingleton = null;
 
 var gParentMM = null;
 
 
 function amManager() {
   Cu.import("resource://gre/modules/AddonManager.jsm");
   /*globals AddonManagerPrivate*/
 
-  let globalMM = Cc["@mozilla.org/globalmessagemanager;1"]
-                 .getService(Ci.nsIMessageListenerManager);
+  let globalMM = Services.mm;
   globalMM.loadFrameScript(CHILD_SCRIPT, true);
   globalMM.addMessageListener(MSG_INSTALL_ADDONS, this);
 
-  gParentMM = Cc["@mozilla.org/parentprocessmessagemanager;1"]
-                 .getService(Ci.nsIMessageListenerManager);
+  gParentMM = Services.ppmm;
   gParentMM.addMessageListener(MSG_INSTALL_ENABLED, this);
+  gParentMM.addMessageListener(MSG_PROMISE_REQUEST, this);
 
   // Needed so receiveMessage can be called directly by JS callers
   this.wrappedJSObject = this;
 }
 
 amManager.prototype = {
   observe: function(aSubject, aTopic, aData) {
     if (aTopic == "addons-startup")
@@ -169,16 +171,40 @@ amManager.prototype = {
             },
           };
         }
 
         return this.installAddonsFromWebpage(payload.mimetype,
           aMessage.target, payload.triggeringPrincipal, payload.uris,
           payload.hashes, payload.names, payload.icons, callback);
       }
+
+      case MSG_PROMISE_REQUEST: {
+        let resolve = (value) => {
+          aMessage.target.sendAsyncMessage(MSG_PROMISE_RESULT, {
+            callbackID: payload.callbackID,
+            resolve: value
+          });
+        }
+        let reject = (value) => {
+          aMessage.target.sendAsyncMessage(MSG_PROMISE_RESULT, {
+            callbackID: payload.callbackID,
+            reject: value
+          });
+        }
+
+        let API = AddonManager.webAPI;
+        if (payload.type in API) {
+          API[payload.type](...payload.args).then(resolve, reject);
+        }
+        else {
+          reject("Unknown Add-on API request.");
+        }
+        break;
+      }
     }
     return undefined;
   },
 
   classID: Components.ID("{4399533d-08d1-458c-a87a-235f74451cfa}"),
   _xpcom_factory: {
     createInstance: function(aOuter, aIid) {
       if (aOuter != null)
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -0,0 +1,108 @@
+/* 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";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+
+const MSG_PROMISE_REQUEST  = "WebAPIPromiseRequest";
+const MSG_PROMISE_RESULT   = "WebAPIPromiseResult";
+
+const APIBroker = {
+  _nextID: 0,
+
+  init() {
+    this._promises = new Map();
+
+    Services.cpmm.addMessageListener(MSG_PROMISE_RESULT, this);
+  },
+
+  receiveMessage(message) {
+    let payload = message.data;
+
+    switch (message.name) {
+      case MSG_PROMISE_RESULT: {
+        if (!this._promises.has(payload.callbackID)) {
+          return;
+        }
+
+        let { resolve, reject } = this._promises.get(payload.callbackID);
+        this._promises.delete(payload.callbackID);
+
+        if ("resolve" in payload)
+          resolve(payload.resolve);
+        else
+          reject(payload.reject);
+        break;
+      }
+    }
+  },
+
+  sendRequest: function(type, ...args) {
+    return new Promise((resolve, reject) => {
+      let callbackID = this._nextID++;
+
+      this._promises.set(callbackID, { resolve, reject });
+      Services.cpmm.sendAsyncMessage(MSG_PROMISE_REQUEST, { type, callbackID, args });
+    });
+  },
+};
+
+APIBroker.init();
+
+function Addon(properties) {
+  // We trust the webidl binding to broker access to our properties.
+  for (let key of Object.keys(properties)) {
+    this[key] = properties[key];
+  }
+}
+
+/**
+ * API methods should return promises from the page, this is a simple wrapper
+ * to make sure of that. It also automatically wraps objects when necessary.
+ */
+function WebAPITask(generator) {
+  let task = Task.async(generator);
+
+  return function(...args) {
+    let win = this.window;
+
+    let wrapForContent = (obj) => {
+      if (obj instanceof Addon) {
+        return win.Addon._create(win, obj);
+      }
+
+      return obj;
+    }
+
+    return new win.Promise((resolve, reject) => {
+      task(...args).then(wrapForContent)
+                   .then(resolve, reject);
+    });
+  }
+}
+
+function WebAPI() {
+}
+
+WebAPI.prototype = {
+  init(window) {
+    this.window = window;
+  },
+
+  getAddonByID: WebAPITask(function*(id) {
+    let addonInfo = yield APIBroker.sendRequest("getAddonByID", id);
+    return addonInfo ? new Addon(addonInfo) : null;
+  }),
+
+  classID: Components.ID("{8866d8e3-4ea5-48b7-a891-13ba0ac15235}"),
+  contractID: "@mozilla.org/addon-web-api/manager;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIDOMGlobalPropertyInitializer])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([WebAPI]);
--- a/toolkit/mozapps/extensions/extensions.manifest
+++ b/toolkit/mozapps/extensions/extensions.manifest
@@ -18,8 +18,10 @@ contract @mozilla.org/addons/web-install
 component {9df8ef2b-94da-45c9-ab9f-132eb55fddf1} amInstallTrigger.js
 contract @mozilla.org/addons/installtrigger;1 {9df8ef2b-94da-45c9-ab9f-132eb55fddf1}
 category JavaScript-global-property InstallTrigger @mozilla.org/addons/installtrigger;1
 #ifndef MOZ_WIDGET_ANDROID
 category addon-provider-module PluginProvider resource://gre/modules/addons/PluginProvider.jsm
 #endif
 category addon-provider-module GMPProvider resource://gre/modules/addons/GMPProvider.jsm
 #endif
+component {8866d8e3-4ea5-48b7-a891-13ba0ac15235} amWebAPI.js
+contract @mozilla.org/addon-web-api/manager;1 {8866d8e3-4ea5-48b7-a891-13ba0ac15235}
--- a/toolkit/mozapps/extensions/internal/Content.js
+++ b/toolkit/mozapps/extensions/internal/Content.js
@@ -11,23 +11,28 @@
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 var {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
 
 var nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                      "initWithPath");
 
 const MSG_JAR_FLUSH = "AddonJarFlush";
+const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush";
 
 
 try {
   if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
-  // Propagate JAR cache flush notifications across process boundaries.
+    // Propagate JAR cache flush notifications across process boundaries.
     addMessageListener(MSG_JAR_FLUSH, function(message) {
       let file = new nsIFile(message.data);
       Services.obs.notifyObservers(file, "flush-cache-entry", null);
     });
+    // Propagate message manager caches flush notifications across processes.
+    addMessageListener(MSG_MESSAGE_MANAGER_CACHES_FLUSH, function() {
+      Services.obs.notifyObservers(null, "message-manager-flush-caches", null);
+    });
   }
 } catch(e) {
   Cu.reportError(e);
 }
 
 })();
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -256,16 +256,17 @@ const XPI_BEFORE_UI_STARTUP = "BeforeFin
 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
 
 const COMPATIBLE_BY_DEFAULT_TYPES = {
   extension: true,
   dictionary: true
 };
 
 const MSG_JAR_FLUSH = "AddonJarFlush";
+const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush";
 
 var gGlobalScope = this;
 
 /**
  * Valid IDs fit this pattern.
  */
 var gIDTest = /^(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}|[a-z0-9-\._]*\@[a-z0-9-\._]+)$/i;
 
@@ -295,17 +296,17 @@ function loadLazyObjects() {
     DB_SCHEMA,
     AddonInternal,
     XPIProvider,
     XPIStates,
     syncLoadManifestFromFile,
     isUsableAddon,
     recordAddonTelemetry,
     applyBlocklistChanges,
-    flushStartupCache,
+    flushChromeCaches,
     canRunInSafeMode,
   }
 
   for (let key of Object.keys(shared))
     scope[key] = shared[key];
 
   Services.scriptloader.loadSubScript(uri, scope);
 
@@ -1500,23 +1501,26 @@ function buildJarURI(aJarfile, aPath) {
 /**
  * Sends local and remote notifications to flush a JAR file cache entry
  *
  * @param aJarFile
  *        The ZIP/XPI/JAR file as a nsIFile
  */
 function flushJarCache(aJarFile) {
   Services.obs.notifyObservers(aJarFile, "flush-cache-entry", null);
-  Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster)
-    .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
+  Services.mm.broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
 }
 
-function flushStartupCache() {
+function flushChromeCaches() {
   // Init this, so it will get the notification.
   Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+  // Flush message manager cached scripts
+  Services.obs.notifyObservers(null, "message-manager-flush-caches", null);
+  // Also dispatch this event to child processes
+  Services.mm.broadcastAsyncMessage(MSG_MESSAGE_MANAGER_CACHES_FLUSH, null);
 }
 
 /**
  * Creates and returns a new unique temporary file. The caller should delete
  * the file when it is no longer needed.
  *
  * @return an nsIFile that points to a randomly named, initially empty file in
  *         the OS temporary files directory
@@ -2645,17 +2649,17 @@ this.XPIProvider = {
         let addonsToUpdate = this.shouldForceUpdateCheck(aAppChanged);
         if (addonsToUpdate) {
           this.showUpgradeUI(addonsToUpdate);
           flushCaches = true;
         }
       }
 
       if (flushCaches) {
-        flushStartupCache();
+        Services.obs.notifyObservers(null, "startupcache-invalidate", null);
         // UI displayed early in startup (like the compatibility UI) may have
         // caused us to cache parts of the skin or locale in memory. These must
         // be flushed to allow extension provided skins and locales to take full
         // effect
         Services.obs.notifyObservers(null, "chrome-flush-skin-caches", null);
         Services.obs.notifyObservers(null, "chrome-flush-caches", null);
       }
 
@@ -3330,17 +3334,17 @@ this.XPIProvider = {
               let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
                                     BOOTSTRAP_REASONS.ADDON_UPGRADE :
                                     BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
               this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
                                        existingAddon, "uninstall", uninstallReason,
                                        { newVersion: newVersion });
               this.unloadBootstrapScope(existingAddonID);
-              flushStartupCache();
+              flushChromeCaches();
             }
           }
           catch (e) {
           }
         }
 
         try {
           addon._sourceBundle = location.installAddon(id, stageDirEntry,
@@ -3873,17 +3877,17 @@ this.XPIProvider = {
         if (oldAddon.active) {
           XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
                                           "shutdown", uninstallReason,
                                           { newVersion });
         }
         this.callBootstrapMethod(oldAddon, existingAddon,
                                  "uninstall", uninstallReason, { newVersion });
         this.unloadBootstrapScope(existingAddonID);
-        flushStartupCache();
+        flushChromeCaches();
       }
     }
 
     let file = addon._sourceBundle;
 
     XPIProvider._addURIMapping(addon.id, file);
     XPIProvider.callBootstrapMethod(addon, file, "install",
                                     BOOTSTRAP_REASONS.ADDON_INSTALL);
@@ -4988,17 +4992,17 @@ this.XPIProvider = {
         if (aAddon.active) {
           this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
                                    BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         }
 
         this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         this.unloadBootstrapScope(aAddon.id);
-        flushStartupCache();
+        flushChromeCaches();
       }
       aAddon._installLocation.uninstallAddon(aAddon.id);
       XPIDatabase.removeAddonMetadata(aAddon);
       XPIStates.removeAddon(aAddon.location, aAddon.id);
       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
 
       findAddonAndReveal(aAddon.id);
     }
@@ -6095,17 +6099,17 @@ AddonInstall.prototype = {
                                               "shutdown", reason,
                                               { newVersion: this.addon.version });
             }
 
             XPIProvider.callBootstrapMethod(this.existingAddon, file,
                                             "uninstall", reason,
                                             { newVersion: this.addon.version });
             XPIProvider.unloadBootstrapScope(this.existingAddon.id);
-            flushStartupCache();
+            flushChromeCaches();
           }
 
           if (!isUpgrade && this.existingAddon.active) {
             XPIDatabase.updateAddonActive(this.existingAddon, false);
           }
         }
 
         // Install the new add-on into its final location
@@ -7304,17 +7308,17 @@ AddonWrapper.prototype = {
       const isReloadable = (!XPIProvider.enableRequiresRestart(addon) &&
                             !XPIProvider.disableRequiresRestart(addon));
       if (!isReloadable) {
         throw new Error(
           "cannot reload add-on because it requires a browser restart");
       }
 
       this.userDisabled = true;
-      flushStartupCache();
+      flushChromeCaches();
       this.userDisabled = false;
       resolve();
     });
   },
 
   /**
    * Returns a URI to the selected resource or to the add-on bundle if aPath
    * is null. URIs to the bundle will always be file: URIs. URIs to resources
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -3,17 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 // These are injected from XPIProvider.jsm
 /*globals ADDON_SIGNING, SIGNED_TYPES, BOOTSTRAP_REASONS, DB_SCHEMA,
           AddonInternal, XPIProvider, XPIStates, syncLoadManifestFromFile,
           isUsableAddon, recordAddonTelemetry, applyBlocklistChanges,
-          flushStartupCache, canRunInSafeMode*/
+          flushChromeCaches, canRunInSafeMode*/
 
 var Cc = Components.classes;
 var Ci = Components.interfaces;
 var Cr = Components.results;
 var Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
@@ -2115,17 +2115,17 @@ this.XPIDatabaseReconcile = {
 
             XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
                                             "uninstall", installReason,
                                             { newVersion: currentAddon.version });
             XPIProvider.unloadBootstrapScope(previousAddon.id);
           }
 
           // Make sure to flush the cache when an old add-on has gone away
-          flushStartupCache();
+          flushChromeCaches();
 
           if (currentAddon.bootstrap) {
             // Visible bootstrapped add-ons need to have their install method called
             let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
             file.persistentDescriptor = currentAddon._sourceBundle.persistentDescriptor;
             XPIProvider.callBootstrapMethod(currentAddon, file,
                                             "install", installReason,
                                             { oldVersion: previousAddon.version });
@@ -2172,17 +2172,17 @@ this.XPIDatabaseReconcile = {
       if (previousAddon.bootstrap && exists(previousAddon)) {
         XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
                                         "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         XPIProvider.unloadBootstrapScope(previousAddon.id);
       }
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
 
       // Make sure to flush the cache when an old add-on has gone away
-      flushStartupCache();
+      flushChromeCaches();
     }
 
     // Make sure add-ons from hidden locations are marked invisible and inactive
     let locationAddonMap = currentAddons.get(hideLocation);
     if (locationAddonMap) {
       for (let addon of locationAddonMap.values()) {
         addon.visible = false;
         addon.active = false;
--- a/toolkit/mozapps/extensions/moz.build
+++ b/toolkit/mozapps/extensions/moz.build
@@ -18,16 +18,17 @@ XPIDL_SOURCES += [
 ]
 
 XPIDL_MODULE = 'extensions'
 
 EXTRA_COMPONENTS += [
     'addonManager.js',
     'amContentHandler.js',
     'amInstallTrigger.js',
+    'amWebAPI.js',
     'amWebInstallListener.js',
     'nsBlocklistService.js',
     'nsBlocklistServiceContent.js',
 ]
 
 EXTRA_PP_COMPONENTS += [
     'extensions.manifest',
 ]
@@ -38,20 +39,26 @@ EXTRA_JS_MODULES += [
     'DeferredSave.jsm',
     'LightweightThemeManager.jsm',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 EXPORTS.mozilla += [
     'AddonContentPolicy.h',
+    'AddonManagerWebAPI.h',
     'AddonPathService.h',
 ]
 
 UNIFIED_SOURCES += [
     'AddonContentPolicy.cpp',
+    'AddonManagerWebAPI.cpp',
     'AddonPathService.cpp',
 ]
 
+LOCAL_INCLUDES += [
+    '/dom/base',
+]
+
 FINAL_LIBRARY = 'xul'
 
 with Files('**'):
     BUG_COMPONENT = ('Toolkit', 'Add-ons Manager')
new file mode 100644
index 0000000000000000000000000000000000000000..aba80181d2d1e028d002737172d4ee8a6c729db4
GIT binary patch
literal 1416
zc$^FHW@Zs#U|`^2U@&a=*ytsBHI|WqA&!}Wftx{wAv3SIBrzvPuP7xgG=!6Zd3`in
zI0%<ka5FHnya1{K6MKU%`W-S5sl6Y5<zrgVtz4HIMr>*)X6w7v?sqtNY;Ei<YscRI
z?=*5Yboi@%Tl;)o@q5dcxmGttEh~kOd96KGcj5UzmCt*=B$}z0y58LQzbQhkCwKep
ze`c?;HzzvE9Peq@;aM5Ec4ciihxMwQ^WRVWvq)6x>f3ff-st>6Qw~dcgTOaeZTHK$
zdsqpVWu|m)<z?cIeRyMik5}I+7p?9I&$lqBUO$wy_Ljew>%<eGVN<mo1bsHHyYbmm
z(^??vlVN5Ud+F9^_k_fv7P~U1wk;FtD{k5|tu4WfDgU(5bngS(oXtI&x7yhsEjpsS
zPwrCciAKIWwrnZ=9Sa;;1dk}mxh*~ze!OI{-jXW2s;7TX1fE-<CtAi^SF13u;h5_0
z#LI^IM}8%zT(PR3YVr4Npx)Ep-%G-$Oxk&o>*U<1iPen;%q&hXGxbG8l&XuW-Q>P!
zKeMvB@;1Lb<<u+wDV6sF;4w7Upxq<s$pWj@K;9i7<_E@5T2W$ds%~*|QD#AjURE(U
zmVQOCg-c_`lI<DCtiujGtq=G5EAH`PmNdOxCORv3;RInH(?j!;df7brES9gI?K<7Q
z=KVuKy<Fx?`y%H)Yj_o~BjnUURwu=@w)J1DSF(gpjfs|ez)|c{@2v7z=Igm7O1ErQ
z`reqo!$#NchRFGsqTdDi4!3(<Kc;K5Hc;HZG{5_B#+zMIAOBfOF3yM)FWa;6uJ*>|
z^3y*)t^JYzO6lazsonX#b~kPPYa~*wp9XE2d#?VMILOaj3?KmVv-6>u7N8Iif%-W)
zqbNT&RWCO&FEcH*xCEb{i&Bg8ON)|I6>=+e6H`+1^Az;BPy<w@*lqP2Aa4>d+4BGm
zPs-0PDK05WEFch?wG$lum<@SczZY|5hvi;X+t4bi#lGT-aLbXooL-Mkly$Kkx!ak>
zC@Q40|L>oa+wUH%)OoN{PiA?fA{PtKMeCysUJHy5t=zlh#lxAKx{vZ~XpY=!7x7wh
z?lJAEQ!~3H9-X`<%BP)uf8xn|v#oEpzcGl3`Y5&1L1&TPQs07C-!AAnrE<&9w|9BJ
zdE2kq5)6G)v_*BamSy@RC7oY0dt%^<jmP)hIFe!9IxXOh<WDKvuF9!4dGfm-?heX6
zr#6vWec8OHZ!E*^masesn?AjnyRmEUhSL=jL|^YK<QJdLviob<*PWRQwtqTby7te{
zM*RS9MkYCCT*ZV0IHG`DhAoXC7D}PP3Mn+u0uR|h%v6nR;36P}k;Yjev4Yi5T*(I6
o&}mEzn289Vp}3L@vZ0@WhN31OTqd%zfpoC|p)XLYDhr4Q0JOLNTL1t6
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/bootstrap.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {
+  Components.utils.import("resource://gre/modules/Services.jsm");
+  Services.ppmm.loadProcessScript(
+    "resource://my-addon/frame-script.js", false);
+}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/chrome.manifest
@@ -0,0 +1,1 @@
+resource my-addon .
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/frame-script.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Services.cpmm.sendAsyncMessage("my-addon-1");
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>update1@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>0.3</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>toolkit@mozilla.org</em:id>
+        <em:minVersion>0</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Update Tests</em:name>
+
+  </Description>
+</RDF>
new file mode 100644
index 0000000000000000000000000000000000000000..8d7fd01e824611b91915bf00a0ec0c40387645dd
GIT binary patch
literal 1418
zc$^FHW@Zs#U|`^2U^8s@Sida9E{>6bA&!}Wftx{wAv3SIBrzvPuP7xgG=!6Zc~3N3
zI0%<ka5FHnya1{K6MKU%`W-S5sl6Y5<zw2VtvN0?jM&sp%+`0S-M^u!H+uILamU{O
z?=*5Yboi@%Tl;)o@q5dcxmGu)vQ`Qo^IChX&i=tamCt*=B$}z0y58LQzbQhsCwKep
z1Zmb=Ya;FYl8-rywOx6lopoRIP@mTA&$gTF{bqD*TCDrEU5BfH*XbO)&y?!Y^Xrcb
zPCC~TD;1fT+kC(&zu--Nk5}I+7p?99ku5B$;fIpe-tzZyoq59Mc8KMI7ycX9-T3UO
zX)O@-$uLul?atL%)!q>qy<)r}lCydX<_JI8YQnNvtRgzJHHK-M>@G*CHD7;fPE(fq
zcy)rFGha!8am2-ngRF1e1sruAC7MXpl=_&z3R?Aiak!jIUlCjB$_?2cUNe7WG10rp
zKfA9|@tVBy;%i4&A9<PIU!-|||MK07rkv>w^LQ8H_m1@tj{wKg<$D7Hy55|8D_MDG
z{mj=d0=DnJJ5h7BeZaF?<^XsUEjDQP*ffu!b`6ks2Z;HBQIuAcn479woLrPyP@<Pr
z42~uyU=&GXMw9Is$E?E+JgpD+`YZ17VwT(*yKVJ^JnqJM39AIRb)}nl6dkavy{oh9
z&%<i|{(?N|3E$49%wj&Wut4zCK~^Wlw6@p3_HrC-zow(hThL;;Xusm5LcX1bnQL#^
ztn|Ghzr#l7-c6DCOUCb%W!k#0t?P}hh;kP5-?aT`qDfh7Z^i#PJsYzk#moLA?dIIL
zTz>uI%fCO|zmn~HZta}f>@O$ZRX#c#u{~JN_4doJ{{NXkp5|fz0g$Ji56!dy1%U|E
z)5#e{`MIfjxruq1X{p5}_&i;dTAW{6l$@%NTdA9vl9HdNpvQ$8qAJC1tKR^5lYj}I
z2WWUwett=DNl{_}f#9s2;ONI}$m9CGm@7Lh_o~{4R#7eX6<35?j?CrsdUT?!i|xqW
z&NN0*A)Wny|D@c0_h6;YgOz$R%Oe%JSa>d4A7$`bV0>uh-X$*{&fL^}lxIV8<W{?g
z*OGIOX;+<^*(LGl<TX(~?d<y#Pu`nteY^dQK}^&~sg({oi}aTI7QFg)LDwmjTYkR1
z%lpmSe$AF(=$oP~s-v|m(<dqE{F>Pl16OQ3zVF774CB^m0dFLKO4)W*PPNIC-~DiR
zQ1&^siQMYT<~@C58FsgX<w4l=>CN1YU3)j2u9zVDdS4;G_;i-tU(3Gk%v`Yj)A`c1
ze||RV2Y53w$uZ+9C?voU1>`bpX#}xQiVapsv4IwN$Od92Yh(i#11XF|&I*YYtcK!B
qHpqs~U}C^bMEDHFm0XYw{R%V`HSypwk(CXkiv<XMfm+pBKs*3bXaKzc
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/bootstrap.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {
+  Components.utils.import("resource://gre/modules/Services.jsm");
+  Services.ppmm.loadProcessScript(
+    "resource://my-addon/frame-script.js", false);
+}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/chrome.manifest
@@ -0,0 +1,1 @@
+resource my-addon .
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/frame-script.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Services.cpmm.sendAsyncMessage("my-addon-2");
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>update1@tests.mozilla.org</em:id>
+    <em:version>2.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>0.3</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>toolkit@mozilla.org</em:id>
+        <em:minVersion>0</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+    <!-- Front End MetaData -->
+    <em:name>Update Tests</em:name>
+
+  </Description>
+</RDF>
--- a/toolkit/mozapps/extensions/test/browser/browser.ini
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -32,16 +32,20 @@ support-files =
   browser_updatessl.rdf
   browser_updatessl.rdf^headers^
   browser_install.rdf
   browser_install.rdf^headers^
   browser_install.xml
   browser_install1_3.xpi
   browser_eula.xml
   browser_purchase.xml
+  webapi_checkavailable.html
+  webapi_checkchromeframe.xul
+  webapi_checkframed.html
+  webapi_checknavigatedwindow.html
   !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
   !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
   !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
 
@@ -54,10 +58,13 @@ support-files =
 [browser_hotfix.js]
 # Verifies the old style of signing hotfixes
 skip-if = require_signing
 [browser_installssl.js]
 [browser_newaddon.js]
 [browser_updatessl.js]
 [browser_task_next_test.js]
 [browser_discovery_install.js]
+[browser_update.js]
+[browser_webapi.js]
+[browser_webapi_access.js]
 
 [include:browser-common.ini]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_update.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that updates correctly flush caches and that new files gets updated.
+
+function test() {
+  requestLongerTimeout(2);
+  waitForExplicitFinish();
+
+  Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+  Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+
+  run_next_test();
+}
+
+// Install a first version
+add_test(function() {
+  AddonManager.getInstallForURL(TESTROOT + "addons/browser_update1_1.xpi",
+                                function(aInstall) {
+    aInstall.install();
+  }, "application/x-xpinstall");
+
+  Services.ppmm.addMessageListener("my-addon-1", function messageListener() {
+    Services.ppmm.removeMessageListener("my-addon-1", messageListener);
+    ok(true, "first version sent frame script message");
+    run_next_test();
+  });
+});
+
+// Update to a second version and verify that content gets updated
+add_test(function() {
+  AddonManager.getInstallForURL(TESTROOT + "addons/browser_update1_2.xpi",
+                                function(aInstall) {
+    aInstall.install();
+  }, "application/x-xpinstall");
+
+  Services.ppmm.addMessageListener("my-addon-2", function messageListener() {
+    Services.ppmm.removeMessageListener("my-addon-2", messageListener);
+    ok(true, "second version sent frame script message");
+    run_next_test();
+  });
+});
+
+// Finally, cleanup things
+add_test(function() {
+  Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+  Services.prefs.setBoolPref("extensions.checkUpdateSecurity", true);
+
+  AddonManager.getAddonByID("update1@tests.mozilla.org", function(aAddon) {
+    aAddon.uninstall();
+
+    finish();
+  });
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function testWithAPI(task) {
+  return function*() {
+    yield BrowserTestUtils.withNewTab(TESTPAGE, task);
+  }
+}
+
+let gProvider = new MockProvider();
+
+gProvider.createAddons([{
+  id: "addon1@tests.mozilla.org",
+  name: "Test add-on 1",
+  version: "2.1",
+  description: "Short description",
+  type: "extension",
+  userDisabled: false,
+  isActive: true,
+}, {
+  id: "addon2@tests.mozilla.org",
+  name: "Test add-on 2",
+  version: "5.3.7ab",
+  description: null,
+  type: "theme",
+  userDisabled: false,
+  isActive: false,
+}, {
+  id: "addon3@tests.mozilla.org",
+  name: "Test add-on 3",
+  version: "1",
+  description: "Longer description",
+  type: "extension",
+  userDisabled: true,
+  isActive: false,
+}]);
+
+function API_getAddonByID(browser, id) {
+  return ContentTask.spawn(browser, id, function*(id) {
+    let addon = yield content.navigator.mozAddonManager.getAddonByID(id);
+
+    // We can't send native objects back so clone its properties.
+    let result = {};
+    for (let prop in addon) {
+      result[prop] = addon[prop];
+    }
+
+    return result;
+  });
+}
+
+add_task(testWithAPI(function*(browser) {
+  function compareObjects(web, real) {
+    for (let prop of Object.keys(web)) {
+      let webVal = web[prop];
+      let realVal = real[prop];
+
+      switch (prop) {
+        case "isEnabled":
+          realVal = !real.userDisabled;
+          break;
+      }
+
+      // null and undefined don't compare well so stringify them first
+      if (realVal === null || realVal === undefined) {
+        realVal = `${realVal}`;
+        webVal = `${webVal}`;
+      }
+
+      is(webVal, realVal, `Property ${prop} should have the right value in add-on ${real.id}`);
+    }
+  }
+
+  let [a1, a2, a3] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+                                               "addon2@tests.mozilla.org",
+                                               "addon3@tests.mozilla.org"]);
+  let w1 = yield API_getAddonByID(browser, "addon1@tests.mozilla.org");
+  let w2 = yield API_getAddonByID(browser, "addon2@tests.mozilla.org");
+  let w3 = yield API_getAddonByID(browser, "addon3@tests.mozilla.org");
+
+  compareObjects(w1, a1);
+  compareObjects(w2, a2);
+  compareObjects(w3, a3);
+}));
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function check_frame_availability(browser) {
+  return ContentTask.spawn(browser, null, function*() {
+    let frame = content.document.getElementById("frame");
+    return frame.contentWindow.document.getElementById("result").textContent == "true";
+  });
+}
+
+function check_availability(browser) {
+  return ContentTask.spawn(browser, null, function*() {
+    return content.document.getElementById("result").textContent == "true";
+  });
+}
+
+// Test that initially the API isn't available in the test domain
+add_task(function* test_not_available() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that with testing on the API is available in the test domain
+add_task(function* test_available() {
+  Services.prefs.setBoolPref("extensions.webapi.testing", true);
+
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(available, "API should be available.");
+    })
+});
+
+// Test that the API is not available in a bad domain
+add_task(function* test_bad_domain() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that the API is only available in https sites
+add_task(function* test_not_available_http() {
+  yield BrowserTestUtils.withNewTab(`${TESTROOT}webapi_checkavailable.html`,
+    function* test_not_available(browser) {
+      let available = yield check_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that the API is available when in a frame of the test domain
+add_task(function* test_available_framed() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkframed.html`,
+    function* test_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(available, "API should be available.");
+    })
+});
+
+// Test that if the external frame is http then the inner frame doesn't have
+// the API
+add_task(function* test_not_available_http_framed() {
+  yield BrowserTestUtils.withNewTab(`${TESTROOT}webapi_checkframed.html`,
+    function* test_not_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that if the external frame is a bad domain then the inner frame doesn't
+// have the API
+add_task(function* test_not_available_framed() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checkframed.html`,
+    function* test_not_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(!available, "API should not be available.");
+    })
+});
+
+// Test that a window navigated to a bad domain doesn't allow access to the API
+add_task(function* test_navigated_window() {
+  yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checknavigatedwindow.html`,
+    function* test_available(browser) {
+      let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+      yield ContentTask.spawn(browser, null, function*() {
+        yield content.wrappedJSObject.openWindow();
+      });
+
+      // Should be a new tab open
+      let tab = yield tabPromise;
+      let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab));
+
+      ContentTask.spawn(browser, null, function*() {
+        content.wrappedJSObject.navigate();
+      });
+
+      yield loadPromise;
+
+      let available = yield ContentTask.spawn(browser, null, function*() {
+        return content.wrappedJSObject.check();
+      });
+
+      ok(!available, "API should not be available.");
+
+      gBrowser.removeTab(tab);
+    })
+});
+
+// Check that if a page is embedded in a chrome content UI that it can still
+// access the API.
+add_task(function* test_chrome_frame() {
+  yield BrowserTestUtils.withNewTab(`${CHROMEROOT}webapi_checkchromeframe.xul`,
+    function* test_available(browser) {
+      let available = yield check_frame_availability(browser);
+      ok(available, "API should be available.");
+    })
+});
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -21,17 +21,19 @@ var gTestInWindow = /-window$/.test(path
 // Drop the UI type
 if (gTestInWindow) {
   pathParts.splice(pathParts.length - 1, pathParts.length);
 }
 
 const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";
 
 const TESTROOT = "http://example.com/" + RELATIVE_DIR;
+const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR;
 const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
+const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR;
 const CHROMEROOT = pathParts.join("/") + "/";
 const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
 const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
 const PREF_XPI_ENABLED = "xpinstall.enabled";
 const PREF_UPDATEURL = "extensions.update.url";
 const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
 const PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI = "xpinstall.customConfirmationUI";
 const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+document.getElementById("result").textContent = ("mozAddonManager" in window.navigator);
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <browser id="frame" disablehistory="true" flex="1" type="content"
+           src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html"/>
+</window>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<iframe id="frame" height="200" width="200" src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html">
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<script type="text/javascript">
+var nav, win;
+
+function openWindow() {
+  return new Promise(resolve => {
+    win = window.open(window.location);
+
+    win.addEventListener("load", function listener() {
+      nav = win.navigator;
+      resolve();
+    }, false);
+  });
+}
+
+function navigate() {
+  win.location = "http://example.com/";
+}
+
+function check() {
+  return "mozAddonManager" in nav;
+}
+</script>
+</body>
+</html>