Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 17 Apr 2014 22:19:52 -0400
changeset 198656 ec728bfdbb79cb5ef6a847d042bda0cbad1207d0
parent 198611 40164bb4210b8274850100602b78515a1d14bd50 (current diff)
parent 198655 98d6d4c38fcc3115d56fc7f4f05452d1e077b72e (diff)
child 198738 7fe3ee0cf8be3f598d23d610618b1fee976a8fa7
push id486
push userasasaki@mozilla.com
push dateMon, 14 Jul 2014 18:39:42 +0000
treeherdermozilla-release@d33428174ff1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone31.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c.
mobile/android/base/fxa/activities/FxAccountCreateAccountFragment.java
mobile/android/base/resources/drawable/url_bar_right_edge.xml
mobile/android/base/sync/PrefsSource.java
--- a/addon-sdk/source/lib/sdk/addon/installer.js
+++ b/addon-sdk/source/lib/sdk/addon/installer.js
@@ -60,18 +60,22 @@ exports.install = function install(xpiPa
     },
     onDownloadFailed: function(aInstall) {
       this.onInstallFailed(aInstall);
     }
   };
 
   // Order AddonManager to install the addon
   AddonManager.getInstallForFile(file, function(install) {
-    install.addListener(listener);
-    install.install();
+    if (install.error != null) {
+      install.addListener(listener);
+      install.install();
+    } else {
+      reject(install.error);
+    }
   });
 
   return promise;
 };
 
 exports.uninstall = function uninstall(addonId) {
   let { promise, resolve, reject } = defer();
 
--- a/addon-sdk/source/lib/sdk/content/content-worker.js
+++ b/addon-sdk/source/lib/sdk/content/content-worker.js
@@ -281,27 +281,16 @@ const ContentWorker = Object.freeze({
       on: pipe.on.bind(null),
       once: pipe.once.bind(null),
       removeListener: pipe.removeListener.bind(null),
     };
     Object.defineProperty(exports, "self", {
       value: self
     });
 
-    // Deprecated use of on/postMessage from globals
-    exports.postMessage = function deprecatedPostMessage() {
-      console.error("DEPRECATED: The global `postMessage()` function in " +
-                    "content scripts is deprecated in favor of the " +
-                    "`self.postMessage()` function, which works the same. " +
-                    "Replace calls to `postMessage()` with calls to " +
-                    "`self.postMessage()`." +
-                    "For more info on `self.on`, see " +
-                    "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
-      return self.postMessage.apply(null, arguments);
-    };
     exports.on = function deprecatedOn() {
       console.error("DEPRECATED: The global `on()` function in content " +
                     "scripts is deprecated in favor of the `self.on()` " +
                     "function, which works the same. Replace calls to `on()` " +
                     "with calls to `self.on()`" +
                     "For more info on `self.on`, see " +
                     "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.");
       return self.on.apply(null, arguments);
--- a/addon-sdk/source/lib/sdk/l10n/html.js
+++ b/addon-sdk/source/lib/sdk/l10n/html.js
@@ -41,19 +41,21 @@ function onDocumentReady2Translate(event
   let document = event.target;
   document.removeEventListener("DOMContentLoaded", onDocumentReady2Translate,
                                false);
 
   translateElement(document);
 
   try {
     // Finally display document when we finished replacing all text content
-    let winUtils = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
-                                       .getInterface(Ci.nsIDOMWindowUtils);
-    winUtils.removeSheet(hideSheetUri, winUtils.USER_SHEET);
+    if (document.defaultView) {
+      let winUtils = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+                                         .getInterface(Ci.nsIDOMWindowUtils);
+      winUtils.removeSheet(hideSheetUri, winUtils.USER_SHEET);
+    }
   }
   catch(e) {
     console.exception(e);
   }
 }
 
 function onContentWindow(event) {
   let document = event.subject;
--- a/addon-sdk/source/lib/sdk/panel/utils.js
+++ b/addon-sdk/source/lib/sdk/panel/utils.js
@@ -218,17 +218,16 @@ function show(panel, options, anchor) {
 
   open(panel, options, anchor);
 }
 exports.show = show
 
 function setupPanelFrame(frame) {
   frame.setAttribute("flex", 1);
   frame.setAttribute("transparent", "transparent");
-  frame.setAttribute("showcaret", true);
   frame.setAttribute("autocompleteenabled", true);
   if (platform === "darwin") {
     frame.style.borderRadius = "6px";
     frame.style.padding = "1px";
   }
 }
 
 function make(document) {
--- a/addon-sdk/source/test/test-content-script.js
+++ b/addon-sdk/source/test/test-content-script.js
@@ -186,19 +186,16 @@ exports["test postMessage"] = createProx
     assert.equal(event.data, "{\"foo\":\"bar\\n \\\"escaped\\\".\"}",
                      "message data is correct");
 
     helper.done();
   }, false);
 
   helper.createWorker(
     'new ' + function ContentScriptScope() {
-      assert(postMessage === postMessage,
-          "verify that we doesn't generate multiple functions for the same method");
-
       var json = JSON.stringify({foo : "bar\n \"escaped\"."});
 
       document.getElementById("iframe").contentWindow.postMessage(json, "*");
     }
   );
 });
 
 let html = '<input id="input2" type="checkbox" />';
--- a/addon-sdk/source/test/test-content-worker.js
+++ b/addon-sdk/source/test/test-content-worker.js
@@ -21,17 +21,23 @@ const { set: setPref } = require("sdk/pr
 const { isArray } = require("sdk/lang/type");
 const { URL } = require('sdk/url');
 const fixtures = require("./fixtures");
 
 const DEPRECATE_PREF = "devtools.errorconsole.deprecation_warnings";
 
 const DEFAULT_CONTENT_URL = "data:text/html;charset=utf-8,foo";
 
-function makeWindow(contentURL) {
+const WINDOW_SCRIPT_URL = "data:text/html;charset=utf-8," +
+                          "<script>window.addEventListener('message', function (e) {" +
+                          "  if (e.data === 'from -> content-script')" +
+                          "    window.postMessage('from -> window', '*');" +
+                          "});</script>";
+
+function makeWindow() {
   let content =
     "<?xml version=\"1.0\"?>" +
     "<window " +
     "xmlns=\"http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul\">" +
     "<script>var documentValue=true;</script>" +
     "</window>";
   var url = "data:application/vnd.mozilla.xul+xml;charset=utf-8," +
             encodeURIComponent(content);
@@ -777,60 +783,16 @@ exports["test:check worker API with page
         }, 500);
 
       }, false);
     });
 
   }
 );
 
-exports["test:global postMessage"] = WorkerTest(
-  DEFAULT_CONTENT_URL,
-  function(assert, browser, done) {
-    let { loader } = LoaderWithHookedConsole(module, onMessage);
-    setPref(DEPRECATE_PREF, true);
-
-    // Intercept all console method calls
-    let seenMessages = 0;
-    function onMessage(type, message) {
-      seenMessages++;
-      assert.equal(type, "error", "Should be an error");
-      assert.equal(message, "DEPRECATED: The global `postMessage()` function in " +
-                            "content scripts is deprecated in favor of the " +
-                            "`self.postMessage()` function, which works the same. " +
-                            "Replace calls to `postMessage()` with calls to " +
-                            "`self.postMessage()`." +
-                            "For more info on `self.on`, see " +
-                            "<https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/addon-development/web-content.html>.",
-                            "Should have seen the deprecation message")
-    }
-
-    assert.notEqual(browser.contentWindow.location.href, "about:blank",
-                        "window is now on the right document");
-
-    let window = browser.contentWindow
-    let worker = loader.require("sdk/content/worker").Worker({
-      window: window,
-      contentScript: "new " + function WorkerScope() {
-        postMessage("success");
-      },
-      contentScriptWhen: "ready",
-      onMessage: function(msg) {
-        assert.equal("success", msg, "Should have seen the right postMessage call");
-        assert.equal(1, seenMessages, "Should have seen the deprecation message");
-        done();
-      }
-    });
-
-    assert.equal(worker.url, window.location.href,
-                     "worker.url works");
-    worker.postMessage("hi!");
-  }
-);
-
 exports['test:conentScriptFile as URL instance'] = WorkerTest(
   DEFAULT_CONTENT_URL,
   function(assert, browser, done) {
 
     let url = new URL(fixtures.url("test-contentScriptFile.js"));
     let worker =  Worker({
       window: browser.contentWindow,
       contentScriptFile: url,
@@ -884,18 +846,18 @@ exports["test:onDetach in contentScript 
     let worker = Worker({
       window: browser.contentWindow,
       contentScript: 'new ' + function WorkerScope() {
         self.port.on('detach', function(reason) {
           window.location.hash += '!' + reason;
         })
       },
     });
-    browser.contentWindow.addEventListener('hashchange', _ => { 
-      assert.equal(browser.contentWindow.location.hash, '#detach!', 
+    browser.contentWindow.addEventListener('hashchange', _ => {
+      assert.equal(browser.contentWindow.location.hash, '#detach!',
                    "location.href is as expected");
       done();
     })
     worker.destroy();
   }
 );
 
 exports["test:onDetach in contentScript on unload"] = WorkerTest(
@@ -905,18 +867,18 @@ exports["test:onDetach in contentScript 
     let worker = loader.require("sdk/content/worker").Worker({
       window: browser.contentWindow,
       contentScript: 'new ' + function WorkerScope() {
         self.port.on('detach', function(reason) {
           window.location.hash += '!' + reason;
         })
       },
     });
-    browser.contentWindow.addEventListener('hashchange', _ => { 
-      assert.equal(browser.contentWindow.location.hash, '#detach!shutdown', 
+    browser.contentWindow.addEventListener('hashchange', _ => {
+      assert.equal(browser.contentWindow.location.hash, '#detach!shutdown',
                    "location.href is as expected");
       done();
     })
     loader.unload('shutdown');
   }
 );
 
 exports["test:console method log functions properly"] = WorkerTest(
@@ -949,9 +911,30 @@ exports["test:console method log functio
         ]);
 
         done();
       }
     });
   }
 );
 
+exports["test:global postMessage"] = WorkerTest(
+  WINDOW_SCRIPT_URL,
+  function(assert, browser, done) {
+    let contentScript = "window.addEventListener('message', function (e) {" +
+                        "  if (e.data === 'from -> window')" +
+                        "    self.port.emit('response', e.data, e.origin);" +
+                        "});" +
+                        "postMessage('from -> content-script', '*');";
+    let { loader } = LoaderWithHookedConsole(module);
+    let worker =  loader.require("sdk/content/worker").Worker({
+      window: browser.contentWindow,
+      contentScriptWhen: "ready",
+      contentScript: contentScript
+    });
+
+    worker.port.on("response", (data, origin) => {
+      assert.equal(data, "from -> window", "Communication from content-script to window completed");
+      done();
+    });
+});
+
 require("test").run(exports);
--- a/addon-sdk/source/test/test-page-worker.js
+++ b/addon-sdk/source/test/test-page-worker.js
@@ -1,17 +1,15 @@
 /* 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 { Loader } = require('sdk/test/loader');
-const Pages = require("sdk/page-worker");
-const Page = Pages.Page;
+const { Page } = require("sdk/page-worker");
 const { URL } = require("sdk/url");
 const fixtures = require("./fixtures");
 const testURI = fixtures.url("test.html");
 
 const ERR_DESTROYED =
   "Couldn't find the worker to receive this message. " +
   "The script may not be initialized yet, or may already have been unloaded.";
 
@@ -88,27 +86,27 @@ exports.testPageProperties = function(as
   }
 
   assert.ok(function () page.postMessage("foo") || true,
               "postMessage doesn't throw exception on page.");
 }
 
 exports.testConstructorAndDestructor = function(assert, done) {
   let loader = Loader(module);
-  let Pages = loader.require("sdk/page-worker");
+  let { Page } = loader.require("sdk/page-worker");
   let global = loader.sandbox("sdk/page-worker");
 
   let pagesReady = 0;
 
-  let page1 = Pages.Page({
+  let page1 = Page({
     contentScript:      "self.postMessage('')",
     contentScriptWhen:  "end",
     onMessage:          pageReady
   });
-  let page2 = Pages.Page({
+  let page2 = Page({
     contentScript:      "self.postMessage('')",
     contentScriptWhen:  "end",
     onMessage:          pageReady
   });
 
   assert.notEqual(page1, page2,
                       "Page 1 and page 2 should be different objects.");
 
@@ -123,19 +121,19 @@ exports.testConstructorAndDestructor = f
       loader.unload();
       done();
     }
   }
 }
 
 exports.testAutoDestructor = function(assert, done) {
   let loader = Loader(module);
-  let Pages = loader.require("sdk/page-worker");
+  let { Page } = loader.require("sdk/page-worker");
 
-  let page = Pages.Page({
+  let page = Page({
     contentScript: "self.postMessage('')",
     contentScriptWhen: "end",
     onMessage: function() {
       loader.unload();
       assert.ok(isDestroyed(page), "Page correctly unloaded.");
       done();
     }
   });
@@ -311,22 +309,22 @@ exports.testPingPong = function(assert, 
       }
     }
   });
 };
 
 exports.testRedirect = function (assert, done) {
   let page = Page({
     contentURL: 'data:text/html;charset=utf-8,first-page',
-    contentScript: '(function () {' +
+    contentScriptWhen: "end",
+    contentScript: '' +
       'if (/first-page/.test(document.location.href)) ' +
       '  document.location.href = "data:text/html;charset=utf-8,redirect";' +
       'else ' +
-      '  self.port.emit("redirect", document.location.href);' +
-      '})();'
+      '  self.port.emit("redirect", document.location.href);'
   });
 
   page.port.on('redirect', function (url) {
     assert.equal(url, 'data:text/html;charset=utf-8,redirect', 'Reinjects contentScript on reload');
     done();
   });
 };
 
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -1162,19 +1162,19 @@ let RemoteDebugger = {
           } : DebuggerServer.globalActorFactories
         };
         let root = new DebuggerServer.RootActor(connection, parameters);
         root.applicationType = "operating-system";
         return root;
       };
 
 #ifdef MOZ_WIDGET_GONK
-      DebuggerServer.onConnectionChange = function(what) {
+      DebuggerServer.on("connectionchange", function() {
         AdbController.updateState();
-      }
+      });
 #endif
     }
 
     let path = Services.prefs.getCharPref("devtools.debugger.unix-domain-socket") ||
                "/data/local/debugger-socket";
     try {
       DebuggerServer.openListener(path);
       // Temporary event, until bug 942756 lands and offers a way to know
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -2,16 +2,17 @@
 # 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/.
 
 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
   return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
 });
 
 const PREF_SYNC_START_DOORHANGER = "services.sync.ui.showSyncStartDoorhanger";
+const DOORHANGER_ACTIVATE_DELAY_MS = 5000;
 
 let gFxAccounts = {
 
   _initialized: false,
   _inCustomizationMode: false,
 
   get weave() {
     delete this.weave;
@@ -28,26 +29,16 @@ let gFxAccounts = {
       "weave:service:sync:start",
       "weave:service:login:error",
       "weave:service:setup-complete",
       FxAccountsCommon.ONVERIFIED_NOTIFICATION,
       FxAccountsCommon.ONLOGOUT_NOTIFICATION
     ];
   },
 
-  // The set of topics that only the active window should handle.
-  get activeWindowTopics() {
-    // Do all this dance to lazy-load FxAccountsCommon.
-    delete this.activeWindowTopics;
-    return this.activeWindowTopics = new Set([
-      "weave:service:sync:start",
-      FxAccountsCommon.ONVERIFIED_NOTIFICATION
-    ]);
-  },
-
   get button() {
     delete this.button;
     return this.button = document.getElementById("PanelUI-fxa-status");
   },
 
   get loginFailed() {
     // Referencing Weave.Service will implicitly initialize sync, and we don't
     // want to force that - so first check if it is ready.
@@ -59,31 +50,31 @@ let gFxAccounts = {
     }
     // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
     // All other login failures are assumed to be transient and should go
     // away by themselves, so aren't reflected here.
     return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
   },
 
   get isActiveWindow() {
-    let mostRecentNonPopupWindow =
-      RecentWindow.getMostRecentBrowserWindow({allowPopups: false});
-    return window == mostRecentNonPopupWindow;
+    let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+    return fm.activeWindow == window;
   },
 
   init: function () {
     // Bail out if we're already initialized and for pop-up windows.
     if (this._initialized || !window.toolbar.visible) {
       return;
     }
 
     for (let topic of this.topics) {
       Services.obs.addObserver(this, topic, false);
     }
 
+    addEventListener("activate", this);
     gNavToolbox.addEventListener("customizationstarting", this);
     gNavToolbox.addEventListener("customizationending", this);
 
     this._initialized = true;
 
     this.updateUI();
   },
 
@@ -95,50 +86,58 @@ let gFxAccounts = {
     for (let topic of this.topics) {
       Services.obs.removeObserver(this, topic);
     }
 
     this._initialized = false;
   },
 
   observe: function (subject, topic) {
-    // Ignore certain topics if we're not the active window.
-    if (this.activeWindowTopics.has(topic) && !this.isActiveWindow) {
-      return;
-    }
-
     switch (topic) {
       case FxAccountsCommon.ONVERIFIED_NOTIFICATION:
         Services.prefs.setBoolPref(PREF_SYNC_START_DOORHANGER, true);
         break;
       case "weave:service:sync:start":
         this.onSyncStart();
         break;
       default:
         this.updateUI();
         break;
     }
   },
 
   onSyncStart: function () {
+    if (!this.isActiveWindow) {
+      return;
+    }
+
     let showDoorhanger = false;
 
     try {
       showDoorhanger = Services.prefs.getBoolPref(PREF_SYNC_START_DOORHANGER);
     } catch (e) { /* The pref might not exist. */ }
 
     if (showDoorhanger) {
       Services.prefs.clearUserPref(PREF_SYNC_START_DOORHANGER);
       this.showSyncStartedDoorhanger();
     }
   },
 
   handleEvent: function (event) {
-    this._inCustomizationMode = event.type == "customizationstarting";
-    this.updateUI();
+    if (event.type == "activate") {
+      // Our window might have been in the background while we received the
+      // sync:start notification. If still needed, show the doorhanger after
+      // a short delay. Without this delay the doorhanger would not show up
+      // or with a too small delay show up while we're still animating the
+      // window.
+      setTimeout(() => this.onSyncStart(), DOORHANGER_ACTIVATE_DELAY_MS);
+    } else {
+      this._inCustomizationMode = event.type == "customizationstarting";
+      this.updateUI();
+    }
   },
 
   showDoorhanger: function (id) {
     let panel = document.getElementById(id);
     let anchor = document.getElementById("PanelUI-menu-button");
 
     let iconAnchor =
       document.getAnonymousElementByAttribute(anchor, "class",
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6994,20 +6994,16 @@ function restoreLastSession() {
 
 var TabContextMenu = {
   contextTab: null,
   updateContextMenu: function updateContextMenu(aPopupMenu) {
     this.contextTab = aPopupMenu.triggerNode.localName == "tab" ?
                       aPopupMenu.triggerNode : gBrowser.selectedTab;
     let disabled = gBrowser.tabs.length == 1;
 
-    // Enable the "Close Tab" menuitem when the window doesn't close with the last tab.
-    document.getElementById("context_closeTab").disabled =
-      disabled && gBrowser.tabContainer._closeWindowWithLastTab;
-
     var menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple");
     for (let menuItem of menuItems)
       menuItem.disabled = disabled;
 
     disabled = gBrowser.visibleTabs.length == 1;
     menuItems = aPopupMenu.getElementsByAttribute("tbattr", "tabbrowser-multiple-visible");
     for (let menuItem of menuItems)
       menuItem.disabled = disabled;
--- a/browser/base/content/test/general/browser_devices_get_user_media.js
+++ b/browser/base/content/test/general/browser_devices_get_user_media.js
@@ -1,8 +1,12 @@
+/* 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/. */
+
 const kObservedTopics = [
   "getUserMedia:response:allow",
   "getUserMedia:revoke",
   "getUserMedia:response:deny",
   "getUserMedia:request",
   "recording-device-events",
   "recording-window-ended"
 ];
@@ -731,16 +735,60 @@ let gTests = [
 
     info("request audio+video, stop sharing resets both");
     yield stopAndCheckPerm(true, true);
     info("request audio, stop sharing resets audio only");
     yield stopAndCheckPerm(true, false);
     info("request video, stop sharing resets video only");
     yield stopAndCheckPerm(false, true);
   }
+},
+
+{
+  desc: "'Always Allow' ignored and not shown on http pages",
+  run: function checkNoAlwaysOnHttp() {
+    // Load an http page instead of the https version.
+    let deferred = Promise.defer();
+    let browser = gBrowser.selectedTab.linkedBrowser;
+    browser.addEventListener("load", function onload() {
+      browser.removeEventListener("load", onload, true);
+      deferred.resolve();
+    }, true);
+    content.location = content.location.href.replace("https://", "http://");
+    yield deferred.promise;
+
+    // Initially set both permissions to 'allow'.
+    let Perms = Services.perms;
+    let uri = content.document.documentURIObject;
+    Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
+    Perms.add(uri, "camera", Perms.ALLOW_ACTION);
+
+    // Request devices and expect a prompt despite the saved 'Allow' permission,
+    // because the connection isn't secure.
+    yield promisePopupNotificationShown("webRTC-shareDevices", () => {
+      content.wrappedJSObject.requestDevice(true, true);
+    });
+    expectObserverCalled("getUserMedia:request");
+
+    // Ensure that the 'Always Allow' action isn't shown.
+    let alwaysLabel = gNavigatorBundle.getString("getUserMedia.always.label");
+    ok(!!alwaysLabel, "found the 'Always Allow' localized label");
+    let labels = [];
+    let notification = PopupNotifications.panel.firstChild;
+    for (let node of notification.childNodes) {
+      if (node.localName == "menuitem")
+        labels.push(node.getAttribute("label"));
+    }
+    is(labels.indexOf(alwaysLabel), -1, "The 'Always Allow' item isn't shown");
+
+    // Cleanup.
+    yield closeStream(true);
+    Perms.remove(uri.host, "camera");
+    Perms.remove(uri.host, "microphone");
+  }
 }
 
 ];
 
 function test() {
   waitForExplicitFinish();
 
   let tab = gBrowser.addTab();
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -158,18 +158,18 @@ let gSyncPane = {
         let checkbox = document.getElementById("fxa-pweng-chk");
         let help = document.getElementById("fxa-pweng-help");
         let allowPasswordsEngine = service.allowPasswordsEngine;
 
         if (!allowPasswordsEngine) {
           checkbox.checked = false;
         }
 
-        checkbox.disabled = !allowPasswordsEngine;
-        help.hidden = allowPasswordsEngine;
+        checkbox.disabled = !allowPasswordsEngine || enginesListDisabled;
+        help.hidden = allowPasswordsEngine || enginesListDisabled;
       });
     // If fxAccountEnabled is false and we are in a "not configured" state,
     // then fxAccounts is probably fully disabled rather than just unconfigured,
     // so handle this case.  This block can be removed once we remove support
     // for fxAccounts being disabled.
     } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
                Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -158,18 +158,18 @@ let gSyncPane = {
         let checkbox = document.getElementById("fxa-pweng-chk");
         let help = document.getElementById("fxa-pweng-help");
         let allowPasswordsEngine = service.allowPasswordsEngine;
 
         if (!allowPasswordsEngine) {
           checkbox.checked = false;
         }
 
-        checkbox.disabled = !allowPasswordsEngine;
-        help.hidden = allowPasswordsEngine;
+        checkbox.disabled = !allowPasswordsEngine || enginesListDisabled;
+        help.hidden = allowPasswordsEngine || enginesListDisabled;
       });
     // If fxAccountEnabled is false and we are in a "not configured" state,
     // then fxAccounts is probably fully disabled rather than just unconfigured,
     // so handle this case.  This block can be removed once we remove support
     // for fxAccounts being disabled.
     } else if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
                Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
       this.page = PAGE_NO_ACCOUNT;
--- a/browser/components/tabview/test/browser_tabview_bug625195.js
+++ b/browser/components/tabview/test/browser_tabview_bug625195.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   is(gBrowser.tabs.length, 1, "Only one tab exist");
 
   let originalTab = gBrowser.tabs[0];
 
   popup(originalTab);
-  ok(document.getElementById("context_closeTab").disabled, "The 'Close tab' menu item is disabled");
+  ok(!document.getElementById("context_closeTab").disabled, "The 'Close tab' menu item is enabled");
   ok(document.getElementById("context_openTabInWindow").disabled, "The 'Move to New Window' menu item is disabled");
 
   let newTabOne = gBrowser.addTab("about:blank", {skipAnimation: true});
 
   waitForExplicitFinish();
 
   showTabView(function() {
     registerCleanupFunction(function () {
--- a/browser/devtools/framework/ToolboxProcess.jsm
+++ b/browser/devtools/framework/ToolboxProcess.jsm
@@ -11,59 +11,104 @@ const DBG_XUL = "chrome://browser/conten
 const CHROME_DEBUGGER_PROFILE_NAME = "-chrome-debugger";
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
 let require = devtools.require;
 let Telemetry = require("devtools/shared/telemetry");
+let EventEmitter = require("devtools/toolkit/event-emitter");
+const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 this.EXPORTED_SYMBOLS = ["BrowserToolboxProcess"];
 
+let processes = Set();
+
 /**
  * Constructor for creating a process that will hold a chrome toolbox.
  *
  * @param function aOnClose [optional]
  *        A function called when the process stops running.
  * @param function aOnRun [optional]
  *        A function called when the process starts running.
  * @param object aOptions [optional]
  *        An object with properties for configuring BrowserToolboxProcess.
  */
 this.BrowserToolboxProcess = function BrowserToolboxProcess(aOnClose, aOnRun, aOptions) {
+  let emitter = new EventEmitter();
+  this.on = emitter.on.bind(emitter);
+  this.off = emitter.off.bind(emitter);
+  this.once = emitter.once.bind(emitter);
+  // Forward any events to the shared emitter.
+  this.emit = function(...args) {
+    emitter.emit(...args);
+    BrowserToolboxProcess.emit(...args);
+  }
+
   // If first argument is an object, use those properties instead of
   // all three arguments
   if (typeof aOnClose === "object") {
-    this._closeCallback = aOnClose.onClose;
-    this._runCallback = aOnClose.onRun;
+    if (aOnClose.onClose) {
+      this.on("close", aOnClose.onClose);
+    }
+    if (aOnClose.onRun) {
+      this.on("run", aOnClose.onRun);
+    }
     this._options = aOnClose;
   } else {
-    this._closeCallback = aOnClose;
-    this._runCallback = aOnRun;
+    if (aOnClose) {
+      this.on("close", aOnClose);
+    }
+    if (aOnRun) {
+      this.on("run", aOnRun);
+    }
     this._options = aOptions || {};
   }
 
   this._telemetry = new Telemetry();
 
   this.close = this.close.bind(this);
   Services.obs.addObserver(this.close, "quit-application", false);
   this._initServer();
   this._initProfile();
   this._create();
+
+  processes.add(this);
 };
 
+EventEmitter.decorate(BrowserToolboxProcess);
+
 /**
  * Initializes and starts a chrome toolbox process.
  * @return object
  */
 BrowserToolboxProcess.init = function(aOnClose, aOnRun, aOptions) {
   return new BrowserToolboxProcess(aOnClose, aOnRun, aOptions);
 };
 
+/**
+ * Passes a set of options to the BrowserAddonActors for the given ID.
+ *
+ * @param aId string
+ *        The ID of the add-on to pass the options to
+ * @param aOptions object
+ *        The options.
+ * @return a promise that will be resolved when complete.
+ */
+BrowserToolboxProcess.setAddonOptions = function DSC_setAddonOptions(aId, aOptions) {
+  let promises = [];
+
+  for (let process of processes.values()) {
+    promises.push(process.debuggerServer.setAddonOptions(aId, aOptions));
+  }
+
+  return promise.all(promises);
+};
+
 BrowserToolboxProcess.prototype = {
   /**
    * Initializes the debugger server.
    */
   _initServer: function() {
     dumpn("Initializing the chrome toolbox server.");
 
     if (!this.loader) {
@@ -72,16 +117,19 @@ BrowserToolboxProcess.prototype = {
       // This allows us to safely use the tools against even the actors and
       // DebuggingServer itself, especially since we can mark this loader as
       // invisible to the debugger (unlike the usual loader settings).
       this.loader = new DevToolsLoader();
       this.loader.invisibleToDebugger = true;
       this.loader.main("devtools/server/main");
       this.debuggerServer = this.loader.DebuggerServer;
       dumpn("Created a separate loader instance for the DebuggerServer.");
+
+      // Forward interesting events.
+      this.debuggerServer.on("connectionchange", this.emit.bind(this));
     }
 
     if (!this.debuggerServer.initialized) {
       this.debuggerServer.init();
       this.debuggerServer.addBrowserActors();
       dumpn("initialized and added the browser actors for the DebuggerServer.");
     }
 
@@ -164,19 +212,17 @@ BrowserToolboxProcess.prototype = {
     dumpn("Running chrome debugging process.");
     let args = ["-no-remote", "-foreground", "-P", this._dbgProfile.name, "-chrome", xulURI];
 
     process.runwAsync(args, args.length, { observe: () => this.close() });
 
     this._telemetry.toolOpened("jsbrowserdebugger");
 
     dumpn("Chrome toolbox is now running...");
-    if (typeof this._runCallback == "function") {
-      this._runCallback.call({}, this);
-    }
+    this.emit("run", this);
   },
 
   /**
    * Closes the remote debugging server and kills the toolbox process.
    */
   close: function() {
     if (this.closed) {
       return;
@@ -191,19 +237,18 @@ BrowserToolboxProcess.prototype = {
 
     this._telemetry.toolClosed("jsbrowserdebugger");
     if (this.debuggerServer) {
       this.debuggerServer.destroy();
     }
 
     dumpn("Chrome toolbox is now closed...");
     this.closed = true;
-    if (typeof this._closeCallback == "function") {
-      this._closeCallback.call({}, this);
-    }
+    this.emit("close", this);
+    processes.delete(this);
   }
 };
 
 /**
  * Shortcuts for accessing various debugger preferences.
  */
 let Prefs = new ViewHelpers.Prefs("devtools.debugger", {
   chromeDebuggingHost: ["Char", "chrome-debugging-host"],
--- a/browser/devtools/framework/connect/connect.js
+++ b/browser/devtools/framework/connect/connect.js
@@ -4,19 +4,21 @@
  * 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 Cu = Components.utils;
 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 let gClient;
 let gConnectionTimeout;
 
 XPCOMUtils.defineLazyGetter(window, 'l10n', function () {
   return Services.strings.createBundle('chrome://browser/locale/devtools/connection-screen.properties');
 });
 
@@ -70,69 +72,107 @@ function submit() {
   let delay = Services.prefs.getIntPref("devtools.debugger.remote-timeout");
   gConnectionTimeout = setTimeout(handleConnectionTimeout, delay);
   gClient.connect(onConnectionReady);
 }
 
 /**
  * Connection is ready. List actors and build buttons.
  */
-function onConnectionReady(aType, aTraits) {
+let onConnectionReady = Task.async(function*(aType, aTraits) {
   clearTimeout(gConnectionTimeout);
-  gClient.listTabs(function(aResponse) {
-    document.body.classList.remove("connecting");
-    document.body.classList.add("actors-mode");
 
-    let parent = document.getElementById("tabActors");
+  let deferred = promise.defer();
+  gClient.listAddons(deferred.resolve);
+  let response = yield deferred.promise;
 
-    // Add Global Process debugging...
-    let globals = JSON.parse(JSON.stringify(aResponse));
-    delete globals.tabs;
-    delete globals.selected;
-    // ...only if there are appropriate actors (a 'from' property will always
-    // be there).
+  let parent = document.getElementById("addonActors")
+  if (!response.error && response.addons.length > 0) {
+    // Add one entry for each add-on.
+    for (let addon of response.addons) {
+      if (!addon.debuggable) {
+        continue;
+      }
+      buildAddonLink(addon, parent);
+    }
+  }
+  else {
+    // Hide the section when there are no add-ons
+    parent.previousElementSibling.remove();
+    parent.remove();
+  }
 
-    // Add one entry for each open tab.
-    for (let i = 0; i < aResponse.tabs.length; i++) {
-      buildLink(aResponse.tabs[i], parent, i == aResponse.selected);
-    }
+  deferred = promise.defer();
+  gClient.listTabs(deferred.resolve);
+  response = yield deferred.promise;
+
+  parent = document.getElementById("tabActors");
 
-    let gParent = document.getElementById("globalActors");
+  // Add Global Process debugging...
+  let globals = JSON.parse(JSON.stringify(response));
+  delete globals.tabs;
+  delete globals.selected;
+  // ...only if there are appropriate actors (a 'from' property will always
+  // be there).
+
+  // Add one entry for each open tab.
+  for (let i = 0; i < response.tabs.length; i++) {
+    buildTabLink(response.tabs[i], parent, i == response.selected);
+  }
+
+  let gParent = document.getElementById("globalActors");
 
-    // Build the Remote Process button
-    if (Object.keys(globals).length > 1) {
-      let a = document.createElement("a");
-      a.onclick = function() {
-        openToolbox(globals, true);
+  // Build the Remote Process button
+  if (Object.keys(globals).length > 1) {
+    let a = document.createElement("a");
+    a.onclick = function() {
+      openToolbox(globals, true);
 
-      }
-      a.title = a.textContent = window.l10n.GetStringFromName("mainProcess");
-      a.className = "remote-process";
-      a.href = "#";
-      gParent.appendChild(a);
     }
-    // Move the selected tab on top
-    let selectedLink = parent.querySelector("a.selected");
-    if (selectedLink) {
-      parent.insertBefore(selectedLink, parent.firstChild);
-    }
+    a.title = a.textContent = window.l10n.GetStringFromName("mainProcess");
+    a.className = "remote-process";
+    a.href = "#";
+    gParent.appendChild(a);
+  }
+  // Move the selected tab on top
+  let selectedLink = parent.querySelector("a.selected");
+  if (selectedLink) {
+    parent.insertBefore(selectedLink, parent.firstChild);
+  }
+
+  document.body.classList.remove("connecting");
+  document.body.classList.add("actors-mode");
 
-    // Ensure the first link is focused
-    let firstLink = parent.querySelector("a:first-of-type");
-    if (firstLink) {
-      firstLink.focus();
-    }
+  // Ensure the first link is focused
+  let firstLink = parent.querySelector("a:first-of-type");
+  if (firstLink) {
+    firstLink.focus();
+  }
+});
 
-  });
+/**
+ * Build one button for an add-on actor.
+ */
+function buildAddonLink(addon, parent) {
+  let a = document.createElement("a");
+  a.onclick = function() {
+    openToolbox({ addonActor: addon.actor, title: addon.name }, true, "jsdebugger");
+  }
+
+  a.textContent = addon.name;
+  a.title = addon.id;
+  a.href = "#";
+
+  parent.appendChild(a);
 }
 
 /**
- * Build one button for an actor.
+ * Build one button for a tab actor.
  */
-function buildLink(tab, parent, selected) {
+function buildTabLink(tab, parent, selected) {
   let a = document.createElement("a");
   a.onclick = function() {
     openToolbox(tab);
   }
 
   a.textContent = tab.title;
   a.title = tab.url;
   if (!a.textContent) {
@@ -168,24 +208,24 @@ function showError(type) {
 function handleConnectionTimeout() {
   showError("timeout");
 }
 
 /**
  * The user clicked on one of the buttons.
  * Opens the toolbox.
  */
-function openToolbox(form, chrome=false) {
+function openToolbox(form, chrome=false, tool="webconsole") {
   let options = {
     form: form,
     client: gClient,
     chrome: chrome
   };
   devtools.TargetFactory.forRemoteTab(options).then((target) => {
     let hostType = devtools.Toolbox.HostType.WINDOW;
-    gDevTools.showToolbox(target, "webconsole", hostType).then((toolbox) => {
+    gDevTools.showToolbox(target, tool, hostType).then((toolbox) => {
       toolbox.once("destroyed", function() {
         gClient.close();
       });
     });
     window.close();
   });
 }
--- a/browser/devtools/framework/connect/connect.xhtml
+++ b/browser/devtools/framework/connect/connect.xhtml
@@ -34,16 +34,18 @@
       </form>
       <p class="error-message error-timeout">&errorTimeout;</p>
       <p class="error-message error-refused">&errorRefused;</p>
       <p class="error-message error-unexpected">&errorUnexpected;</p>
     </section>
     <section id="actors-list">
       <p>&availableTabs;</p>
       <ul class="actors" id="tabActors"></ul>
+      <p>&availableAddons;</p>
+      <ul class="actors" id="addonActors"></ul>
       <p>&availableProcesses;</p>
       <ul class="actors" id="globalActors"></ul>
     </section>
     <section id="connecting">
       <p><img src="chrome://browser/skin/tabbrowser/loading.png"></img> &connecting;</p>
     </section>
     <footer>&remoteHelp;<a target='_' href='https://developer.mozilla.org/docs/Tools/Remote_Debugging'>&remoteDocumentation;</a>&remoteHelpSuffix;</footer>
   </body>
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -817,21 +817,27 @@ MarkupView.prototype = {
           this.updateNodeOuterHTML(aNode, aValue, oldValue);
         }
       });
     });
   },
 
   /**
    * Mark the given node expanded.
-   * @param aNode The NodeFront to mark as expanded.
+   * @param {NodeFront} aNode The NodeFront to mark as expanded.
+   * @param {Boolean} aExpanded Whether the expand or collapse.
+   * @param {Boolean} aExpandDescendants Whether to expand all descendants too
    */
-  setNodeExpanded: function(aNode, aExpanded) {
+  setNodeExpanded: function(aNode, aExpanded, aExpandDescendants) {
     if (aExpanded) {
-      this.expandNode(aNode);
+      if (aExpandDescendants) {
+        this.expandAll(aNode);
+      } else {
+        this.expandNode(aNode);
+      }
     } else {
       this.collapseNode(aNode);
     }
   },
 
   /**
    * Mark the given node selected, and update the inspector.selection
    * object's NodeFront to keep consistent state between UI and selection.
@@ -1408,17 +1414,17 @@ MarkupContainer.prototype = {
       this.elt.classList.add("collapsed");
       this.expander.removeAttribute("open");
     }
   },
 
   _onToggle: function(event) {
     this.markup.navigate(this);
     if(this.hasChildren) {
-      this.markup.setNodeExpanded(this.node, !this.expanded);
+      this.markup.setNodeExpanded(this.node, !this.expanded, event.altKey);
     }
     event.stopPropagation();
   },
 
   _onMouseDown: function(event) {
     let target = event.target;
 
     // Target may be a resource link (generated by the output-parser)
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -4,16 +4,17 @@ subsuite = devtools
 support-files =
   doc_markup_edit.html
   doc_markup_flashing.html
   doc_markup_mutation.html
   doc_markup_navigation.html
   doc_markup_pagesize_01.html
   doc_markup_pagesize_02.html
   doc_markup_search.html
+  doc_markup_toggle.html
   doc_markup_tooltip.png
   head.js
   helper_attributes_test_runner.js
   helper_outerhtml_test_runner.js
 
 [browser_markupview_copy_image_data.js]
 [browser_markupview_css_completion_style_attribute.js]
 [browser_markupview_highlight_hover_01.js]
@@ -32,8 +33,11 @@ support-files =
 [browser_markupview_tag_edit_02.js]
 [browser_markupview_tag_edit_03.js]
 [browser_markupview_tag_edit_04.js]
 [browser_markupview_tag_edit_05.js]
 [browser_markupview_tag_edit_06.js]
 [browser_markupview_tag_edit_07.js]
 [browser_markupview_tag_edit_08.js]
 [browser_markupview_textcontent_edit_01.js]
+[browser_markupview_toggle_01.js]
+[browser_markupview_toggle_02.js]
+[browser_markupview_toggle_03.js]
--- a/browser/devtools/markupview/test/browser_markupview_navigation.js
+++ b/browser/devtools/markupview/test/browser_markupview_navigation.js
@@ -108,24 +108,16 @@ function pressKey(key) {
       EventUtils.synthesizeKey("VK_PAGE_DOWN", {});
       break;
     case "home":
       EventUtils.synthesizeKey("VK_HOME", {});
       break;
   }
 }
 
-function waitForChildrenUpdated(inspector) {
-  let def = promise.defer();
-  inspector.markup._waitForChildren().then(() => {
-    executeSoon(def.resolve);
-  });
-  return def.promise;
-}
-
 function checkSelectedNode(key, className, inspector) {
   let node = inspector.selection.node;
 
   if (className == "*comment*") {
     is(node.nodeType, Node.COMMENT_NODE, "Found a comment after pressing " + key);
   } else if (className == "*text*") {
     is(node.nodeType, Node.TEXT_NODE, "Found text after pressing " + key);
   } else if (className == "*doctype*") {
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_toggle_01.js
@@ -0,0 +1,45 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling (expand/collapse) elements by clicking on twisties
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_toggle.html";
+
+let test = asyncTest(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Getting the container for the UL parent element");
+  let container = getContainerForRawNode("ul", inspector);
+
+  info("Clicking on the UL parent expander, and waiting for children");
+  let onChildren = waitForChildrenUpdated(inspector);
+  let onUpdated = inspector.once("inspector-updated");
+  EventUtils.synthesizeMouseAtCenter(container.expander, {},
+    inspector.markup.doc.defaultView);
+  yield onChildren;
+  yield onUpdated;
+
+  info("Checking that child LI elements have been created");
+  for (let li of content.document.querySelectorAll("li")) {
+    ok(getContainerForRawNode(li, inspector),
+      "A container for the child LI element was created");
+  }
+  ok(container.expanded, "Parent UL container is expanded");
+
+  info("Clicking again on the UL expander");
+  // No need to wait, this is a local, synchronous operation where nodes are
+  // only hidden from the view, not destroyed
+  EventUtils.synthesizeMouseAtCenter(container.expander, {},
+    inspector.markup.doc.defaultView);
+
+  info("Checking that child LI elements have been hidden");
+  for (let li of content.document.querySelectorAll("li")) {
+    let liContainer = getContainerForRawNode(li, inspector);
+    is(liContainer.elt.getClientRects().length, 0,
+      "The container for the child LI element was hidden");
+  }
+  ok(!container.expanded, "Parent UL container is collapsed");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_toggle_02.js
@@ -0,0 +1,45 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling (expand/collapse) elements by dbl-clicking on tag lines
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_toggle.html";
+
+let test = asyncTest(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Getting the container for the UL parent element");
+  let container = getContainerForRawNode("ul", inspector);
+
+  info("Dbl-clicking on the UL parent expander, and waiting for children");
+  let onChildren = waitForChildrenUpdated(inspector);
+  let onUpdated = inspector.once("inspector-updated");
+  EventUtils.synthesizeMouseAtCenter(container.tagLine, {clickCount: 2},
+    inspector.markup.doc.defaultView);
+  yield onChildren;
+  yield onUpdated;
+
+  info("Checking that child LI elements have been created");
+  for (let li of content.document.querySelectorAll("li")) {
+    ok(getContainerForRawNode(li, inspector),
+      "A container for the child LI element was created");
+  }
+  ok(container.expanded, "Parent UL container is expanded");
+
+  info("Dbl-clicking again on the UL expander");
+  // No need to wait, this is a local, synchronous operation where nodes are
+  // only hidden from the view, not destroyed
+  EventUtils.synthesizeMouseAtCenter(container.tagLine, {clickCount: 2},
+    inspector.markup.doc.defaultView);
+
+  info("Checking that child LI elements have been hidden");
+  for (let li of content.document.querySelectorAll("li")) {
+    let liContainer = getContainerForRawNode(li, inspector);
+    is(liContainer.elt.getClientRects().length, 0,
+      "The container for the child LI element was hidden");
+  }
+  ok(!container.expanded, "Parent UL container is collapsed");
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_toggle_03.js
@@ -0,0 +1,44 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test toggling (expand/collapse) elements by alt-clicking on twisties, which
+// should expand all the descendants
+
+const TEST_URL = TEST_URL_ROOT + "doc_markup_toggle.html";
+
+let test = asyncTest(function*() {
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  info("Getting the container for the UL parent element");
+  let container = getContainerForRawNode("ul", inspector);
+
+  info("Alt-clicking on the UL parent expander, and waiting for children");
+  let onUpdated = inspector.once("inspector-updated");
+  EventUtils.synthesizeMouseAtCenter(container.expander, {altKey: true},
+    inspector.markup.doc.defaultView);
+  yield onUpdated;
+  yield waitForMultipleChildrenUpdates(inspector);
+
+  info("Checking that all nodes exist and are expanded");
+  for (let node of content.document.querySelectorAll("ul, li, span, em")) {
+    let nodeContainer = getContainerForRawNode(node, inspector);
+    ok(nodeContainer, "Container for node " + node.tagName + " exists");
+    ok(nodeContainer.expanded,
+      "Container for node " + node.tagName + " is expanded");
+  }
+});
+
+// The expand all operation of the markup-view calls itself recursively and
+// there's not one event we can wait for to know when it's done
+function* waitForMultipleChildrenUpdates(inspector) {
+  // As long as child updates are queued up while we wait for an update already
+  // wait again
+  if (inspector.markup._queuedChildUpdates &&
+      inspector.markup._queuedChildUpdates.size) {
+    yield waitForChildrenUpdated(inspector);
+    return yield waitForMultipleChildrenUpdates(inspector);
+  }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/doc_markup_toggle.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <title>Expanding and collapsing markup-view containers</title>
+</head>
+<body>
+  <ul>
+    <li>
+      <span>list <em>item</em></span>
+    </li>
+    <li>
+      <span>list <em>item</em></span>
+    </li>
+    <li>
+      <span>list <em>item</em></span>
+    </li>
+    <li>
+      <span>list <em>item</em></span>
+    </li>
+    <li>
+      <span>list <em>item</em></span>
+    </li>
+    <li>
+      <span>list <em>item</em></span>
+    </li>
+  </ul>
+</body>
+</html>
\ No newline at end of file
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -162,16 +162,33 @@ function selectNode(nodeOrSelector, insp
 function getContainerForRawNode(nodeOrSelector, {markup}) {
   let front = markup.walker.frontForRawNode(getNode(nodeOrSelector));
   let container = markup.getContainer(front);
   info("Markup-container object for " + nodeOrSelector + " " + container);
   return container;
 }
 
 /**
+ * Using the markupview's _waitForChildren function, wait for all queued
+ * children updates to be handled.
+ * @param {InspectorPanel} inspector The instance of InspectorPanel currently
+ * loaded in the toolbox
+ * @return a promise that resolves when all queued children updates have been
+ * handled
+ */
+function waitForChildrenUpdated({markup}) {
+  info("Waiting for queued children updates to be handled");
+  let def = promise.defer();
+  markup._waitForChildren().then(() => {
+    executeSoon(def.resolve);
+  });
+  return def.promise;
+}
+
+/**
  * Simulate a mouse-over on the markup-container (a line in the markup-view)
  * that corresponds to the node or selector passed.
  * @param {String|DOMNode} nodeOrSelector
  * @param {InspectorPanel} inspector The instance of InspectorPanel currently loaded in the toolbox
  * @return a promise that resolves when the container is hovered and the higlighter
  * is shown on the corresponding node
  */
 function hoverContainer(nodeOrSelector, inspector) {
@@ -347,8 +364,20 @@ function getSelectorSearchBox(inspector)
  */
 function searchUsingSelectorSearch(selector, inspector) {
   info("Entering \"" + selector + "\" into the selector-search input field");
   let field = getSelectorSearchBox(inspector);
   field.focus();
   field.value = selector;
   EventUtils.sendKey("return", inspector.panelWin);
 }
+
+/**
+ * This shouldn't be used in the tests, but is useful when writing new tests or
+ * debugging existing tests in order to introduce delays in the test steps
+ * @param {Number} ms The time to wait
+ * @return A promise that resolves when the time is passed
+ */
+function wait(ms) {
+  let def = promise.defer();
+  content.setTimeout(def.resolve, ms);
+  return def.promise;
+}
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -37,21 +37,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 // would.
 XPCOMUtils.defineLazyGetter(this, "CertUtils",
   function() {
     var mod = {};
     Cu.import("resource://gre/modules/CertUtils.jsm", mod);
     return mod;
   });
 
-#ifdef MOZ_CRASHREPORTER
 XPCOMUtils.defineLazyServiceGetter(this, "gCrashReporter",
                                    "@mozilla.org/xre/app-info;1",
                                    "nsICrashReporter");
-#endif
 
 const FILE_CACHE                = "experiments.json";
 const OBSERVER_TOPIC            = "experiments-changed";
 const MANIFEST_VERSION          = 1;
 const CACHE_VERSION             = 1;
 
 const KEEP_HISTORY_N_DAYS       = 180;
 const MIN_EXPERIMENT_ACTIVE_SECONDS = 60;
@@ -71,46 +69,59 @@ const PREF_HEALTHREPORT_ENABLED = "datar
 
 const PREF_BRANCH_TELEMETRY     = "toolkit.telemetry.";
 const PREF_TELEMETRY_ENABLED    = "enabled";
 
 const TELEMETRY_LOG = {
   // log(key, [kind, experimentId, details])
   ACTIVATION_KEY: "EXPERIMENT_ACTIVATION",
   ACTIVATION: {
-    ACTIVATED: "ACTIVATED",             // successfully activated
-    INSTALL_FAILURE: "INSTALL_FAILURE", // failed to install the extension
-    REJECTED: "REJECTED",               // experiment was rejected because of it's conditions,
-                                        // provides details on which
+    // Successfully activated.
+    ACTIVATED: "ACTIVATED",
+    // Failed to install the add-on.
+    INSTALL_FAILURE: "INSTALL_FAILURE",
+    // Experiment does not meet activation requirements. Details will
+    // be provided.
+    REJECTED: "REJECTED",
   },
 
   // log(key, [kind, experimentId, optionalDetails...])
   TERMINATION_KEY: "EXPERIMENT_TERMINATION",
   TERMINATION: {
-    USERDISABLED: "USERDISABLED", // the user disabled this experiment
-    FROM_API: "FROM_API",         // the experiment disabled itself
-    EXPIRED: "EXPIRED",           // experiment expired e.g. by exceeding the end-date
-    RECHECK: "RECHECK",           // disabled after re-evaluating conditions,
-                                  // provides details on which
+    // The Experiments service was disabled.
+    SERVICE_DISABLED: "SERVICE_DISABLED",
+    // Add-on uninstalled.
+    ADDON_UNINSTALLED: "ADDON_UNINSTALLED",
+    // The experiment disabled itself.
+    FROM_API: "FROM_API",
+    // The experiment expired (e.g. by exceeding the end date).
+    EXPIRED: "EXPIRED",
+    // Disabled after re-evaluating conditions. If this is specified,
+    // details will be provided.
+    RECHECK: "RECHECK",
   },
 };
 
 const gPrefs = new Preferences(PREF_BRANCH);
 const gPrefsTelemetry = new Preferences(PREF_BRANCH_TELEMETRY);
 let gExperimentsEnabled = false;
 let gExperiments = null;
 let gLogAppenderDump = null;
 let gPolicyCounter = 0;
 let gExperimentsCounter = 0;
 let gExperimentEntryCounter = 0;
 
 // Tracks active AddonInstall we know about so we can deny external
 // installs.
 let gActiveInstallURLs = new Set();
 
+// Tracks add-on IDs that are being uninstalled by us. This allows us
+// to differentiate between expected uninstalled and user-driven uninstalls.
+let gActiveUninstallAddonIDs = new Set();
+
 let gLogger;
 let gLogDumping = false;
 
 function configureLogging() {
   if (!gLogger) {
     gLogger = Log.repository.getLogger("Browser.Experiments");
     gLogger.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
   }
@@ -311,16 +322,24 @@ Experiments.Policy.prototype = {
     });
   },
 
   telemetryPayload: function () {
     return TelemetryPing.getPayload();
   },
 };
 
+function AlreadyShutdownError(message="already shut down") {
+  this.name = "AlreadyShutdownError";
+  this.message = message;
+}
+
+AlreadyShutdownError.prototype = new Error();
+AlreadyShutdownError.prototype.constructor = AlreadyShutdownError;
+
 /**
  * Manages the experiments and provides an interface to control them.
  */
 
 Experiments.Experiments = function (policy=new Experiments.Policy()) {
   this._log = Log.repository.getLoggerWithMessagePrefix(
     "Browser.Experiments.Experiments",
     "Experiments #" + gExperimentsCounter++ + "::");
@@ -335,19 +354,16 @@ Experiments.Experiments = function (poli
   this._experiments = null;
   this._refresh = false;
   this._terminateReason = null; // or TELEMETRY_LOG.TERMINATION....
   this._dirty = false;
 
   // Loading the cache happens once asynchronously on startup
   this._loadTask = null;
 
-  // Ignore addon-manager notifications for addons that we are uninstalling ourself
-  this._pendingUninstall = null;
-
   // The _main task handles all other actions:
   // * refreshing the manifest off the network (if _refresh)
   // * disabling/enabling experiments
   // * saving the cache (if _dirty)
   this._mainTask = null;
 
   // Timer for re-evaluating experiment status.
   this._timer = null;
@@ -421,17 +437,21 @@ Experiments.Experiments.prototype = {
 
       if (this._timer) {
         this._timer.clear();
       }
     }
 
     this._shutdown = true;
     if (this._mainTask) {
-      yield this._mainTask;
+      try {
+        yield this._mainTask;
+      } catch (e if e instanceof AlreadyShutdownError) {
+        // We error out of tasks after shutdown via that exception.
+      }
     }
 
     this._log.info("Completed uninitialization.");
   }),
 
   _registerWithAddonManager: function () {
     this._log.trace("Registering instance with Addon Manager.");
 
@@ -444,17 +464,17 @@ Experiments.Experiments.prototype = {
     AddonManager.removeAddonListener(this);
   },
 
   /**
    * Throws an exception if we've already shut down.
    */
   _checkForShutdown: function() {
     if (this._shutdown) {
-      throw Error("uninit() already called");
+      throw new AlreadyShutdownError("uninit() already called");
     }
   },
 
   /**
    * Whether the experiments feature is enabled.
    */
   get enabled() {
     return gExperimentsEnabled;
@@ -463,34 +483,34 @@ Experiments.Experiments.prototype = {
   /**
    * Toggle whether the experiments feature is enabled or not.
    */
   set enabled(enabled) {
     this._log.trace("set enabled(" + enabled + ")");
     gPrefs.set(PREF_ENABLED, enabled);
   },
 
-  _toggleExperimentsEnabled: function (enabled) {
+  _toggleExperimentsEnabled: Task.async(function* (enabled) {
     this._log.trace("_toggleExperimentsEnabled(" + enabled + ")");
     let wasEnabled = gExperimentsEnabled;
     gExperimentsEnabled = enabled && telemetryEnabled();
 
     if (wasEnabled == gExperimentsEnabled) {
       return;
     }
 
     if (gExperimentsEnabled) {
-      this.updateManifest();
+      yield this.updateManifest();
     } else {
-      this.disableExperiment();
+      yield this.disableExperiment(TELEMETRY_LOG.TERMINATION.SERVICE_DISABLED);
       if (this._timer) {
         this._timer.clear();
       }
     }
-  },
+  }),
 
   _telemetryStatusChanged: function () {
     this._toggleExperimentsEnabled(gExperimentsEnabled);
   },
 
   /**
    * Returns a promise that is resolved with an array of `ExperimentInfo` objects,
    * which provide info on the currently and recently active experiments.
@@ -662,39 +682,28 @@ Experiments.Experiments.prototype = {
   notify: function (timer) {
     this._log.trace("notify()");
     this._checkForShutdown();
     return this._run();
   },
 
   // START OF ADD-ON LISTENERS
 
-  onDisabled: function (addon) {
-    this._log.trace("onDisabled() - addon id: " + addon.id);
-    if (addon.id == this._pendingUninstall) {
-      return;
-    }
-    let activeExperiment = this._getActiveExperiment();
-    if (!activeExperiment || activeExperiment._addonId != addon.id) {
-      return;
-    }
-    this.disableExperiment();
-  },
-
   onUninstalled: function (addon) {
     this._log.trace("onUninstalled() - addon id: " + addon.id);
-    if (addon.id == this._pendingUninstall) {
+    if (gActiveUninstallAddonIDs.has(addon.id)) {
       this._log.trace("matches pending uninstall");
       return;
     }
     let activeExperiment = this._getActiveExperiment();
     if (!activeExperiment || activeExperiment._addonId != addon.id) {
       return;
     }
-    this.disableExperiment();
+
+    this.disableExperiment(TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED);
   },
 
   onInstallStarted: function (install) {
     if (install.addon.type != "experiment") {
       return;
     }
 
     // We want to be in control of all experiment add-ons: reject installs
@@ -922,25 +931,27 @@ Experiments.Experiments.prototype = {
       this._log.error("getActiveExperimentId() - should not have more than 1 active experiment");
       throw new Error("have more than 1 active experiment");
     }
 
     return null;
   },
 
   /**
-   * Disable an experiment by id.
-   * @param experimentId The id of the experiment.
-   * @param userDisabled (optional) Whether this is disabled as a result of a user action.
+   * Disables all active experiments.
+   *
    * @return Promise<> Promise that will get resolved once the task is done or failed.
    */
-  disableExperiment: function (userDisabled=true) {
+  disableExperiment: function (reason) {
+    if (!reason) {
+      throw new Error("Must specify a termination reason.");
+    }
+
     this._log.trace("disableExperiment()");
-
-    this._terminateReason = userDisabled ? TELEMETRY_LOG.TERMINATION.USERDISABLED : TELEMETRY_LOG.TERMINATION.FROM_API;
+    this._terminateReason = reason;
     return this._run();
   },
 
   /**
    * The Set of add-on IDs that we know about from manifests.
    */
   get _trackedAddonIds() {
     if (!this._experiments) {
@@ -987,54 +998,53 @@ Experiments.Experiments.prototype = {
     let activeChanged = false;
     let now = this._policy.now();
 
     if (!activeExperiment) {
       // Avoid this pref staying out of sync if there were e.g. crashes.
       gPrefs.set(PREF_ACTIVE_EXPERIMENT, false);
     }
 
+    // Ensure the active experiment is in the proper state. This may install,
+    // uninstall, upgrade, or enable the experiment add-on. What exactly is
+    // abstracted away from us by design.
     if (activeExperiment) {
-      this._pendingUninstall = activeExperiment._addonId;
-      try {
-        let wasStopped;
-        if (this._terminateReason) {
-          yield activeExperiment.stop(this._terminateReason);
-          wasStopped = true;
+      let changes;
+      let shouldStopResult = yield activeExperiment.shouldStop();
+      if (shouldStopResult.shouldStop) {
+        let expireReasons = ["endTime", "maxActiveSeconds"];
+        let kind, reason;
+
+        if (expireReasons.indexOf(shouldStopResult.reason[0]) != -1) {
+          kind = TELEMETRY_LOG.TERMINATION.EXPIRED;
+          reason = null;
         } else {
-          wasStopped = yield activeExperiment.maybeStop();
+          kind = TELEMETRY_LOG.TERMINATION.RECHECK;
+          reason = shouldStopResult.reason;
         }
-        if (wasStopped) {
-          this._dirty = true;
-          this._log.debug("evaluateExperiments() - stopped experiment "
-                        + activeExperiment.id);
-          activeExperiment = null;
-          activeChanged = true;
-        } else if (!gExperimentsEnabled) {
-          // No further actions if the feature is disabled.
-        } else if (activeExperiment.needsUpdate) {
-          this._log.debug("evaluateExperiments() - updating experiment "
-                        + activeExperiment.id);
-          try {
-            yield activeExperiment.stop();
-            yield activeExperiment.start();
-          } catch (e) {
-            this._log.error(e);
-            // On failure try the next experiment.
-            activeExperiment = null;
-          }
-          this._dirty = true;
-          activeChanged = true;
-        } else {
-          yield activeExperiment.ensureActive();
-        }
-      } finally {
-        this._pendingUninstall = null;
+        changes = yield activeExperiment.stop(kind, reason);
+      }
+      else if (this._terminateReason) {
+        changes = yield activeExperiment.stop(this._terminateReason);
+      }
+      else {
+        changes = yield activeExperiment.reconcileAddonState();
+      }
+
+      if (changes) {
+        this._dirty = true;
+        activeChanged = true;
+      }
+
+      if (!activeExperiment._enabled) {
+        activeExperiment = null;
+        activeChanged = true;
       }
     }
+
     this._terminateReason = null;
 
     if (!activeExperiment && gExperimentsEnabled) {
       for (let [id, experiment] of this._experiments) {
         let applicable;
         let reason = null;
         try {
           applicable = yield experiment.isApplicable();
@@ -1047,40 +1057,45 @@ Experiments.Experiments.prototype = {
         if (!applicable && reason && reason[0] != "was-active") {
           // Report this from here to avoid over-reporting.
           let desc = TELEMETRY_LOG.ACTIVATION;
           let data = [TELEMETRY_LOG.ACTIVATION.REJECTED, id];
           data = data.concat(reason);
           TelemetryLog.log(TELEMETRY_LOG.ACTIVATION_KEY, data);
         }
 
-        if (applicable) {
-          this._log.debug("evaluateExperiments() - activating experiment " + id);
-          try {
-            yield experiment.start();
-            activeChanged = true;
-            activeExperiment = experiment;
-            this._dirty = true;
-            break;
-          } catch (e) {
-            // On failure try the next experiment.
-          }
+        if (!applicable) {
+          continue;
+        }
+
+        this._log.debug("evaluateExperiments() - activating experiment " + id);
+        try {
+          yield experiment.start();
+          activeChanged = true;
+          activeExperiment = experiment;
+          this._dirty = true;
+          break;
+        } catch (e) {
+          // On failure, clean up the best we can and try the next experiment.
+          this._log.error("evaluateExperiments() - Unable to start experiment: " + e.message);
+          experiment._enabled = false;
+          yield experiment.reconcileAddonState();
         }
       }
     }
 
+    gPrefs.set(PREF_ACTIVE_EXPERIMENT, activeExperiment != null);
+
     if (activeChanged) {
       Services.obs.notifyObservers(null, OBSERVER_TOPIC, null);
     }
 
-#ifdef MOZ_CRASHREPORTER
-    if (activeExperiment) {
+    if ("@mozilla.org/toolkit/crash-reporter;1" in Cc && activeExperiment) {
       gCrashReporter.annotateCrashReport("ActiveExperiment", activeExperiment.id);
     }
-#endif
   },
 
   /*
    * Schedule the soonest re-check of experiment applicability that is needed.
    */
   _scheduleNextRun: function () {
     this._checkForShutdown();
 
@@ -1122,17 +1137,17 @@ Experiments.Experiments.prototype = {
  */
 
 Experiments.ExperimentEntry = function (policy) {
   this._policy = policy || new Experiments.Policy();
   this._log = Log.repository.getLoggerWithMessagePrefix(
     "Browser.Experiments.Experiments",
     "ExperimentEntry #" + gExperimentEntryCounter++ + "::");
 
-  // Is this experiment running?
+  // Is the experiment supposed to be running.
   this._enabled = false;
   // When this experiment was started, if ever.
   this._startDate = null;
   // When this experiment was ended, if ever.
   this._endDate = null;
   // The condition data from the manifest.
   this._manifestData = null;
   // For an active experiment, signifies whether we need to update the xpi.
@@ -1193,16 +1208,21 @@ Experiments.ExperimentEntry.prototype = 
     "_endDate",
   ]),
 
   DATE_KEYS: new Set([
     "_startDate",
     "_endDate",
   ]),
 
+  ADDON_CHANGE_NONE: 0,
+  ADDON_CHANGE_INSTALL: 1,
+  ADDON_CHANGE_UNINSTALL: 2,
+  ADDON_CHANGE_ENABLE: 4,
+
   /*
    * Initialize entry from the manifest.
    * @param data The experiment data from the manifest.
    * @return boolean Whether initialization succeeded.
    */
   initFromManifestData: function (data) {
     if (!this._isManifestDataValid(data)) {
       return false;
@@ -1482,36 +1502,28 @@ Experiments.ExperimentEntry.prototype = 
       }
 
       throw new Task.Result(true);
     }.bind(this));
   },
 
   /*
    * Start running the experiment.
+   *
    * @return Promise<> Resolved when the operation is complete.
    */
-  start: function () {
+  start: Task.async(function* () {
     this._log.trace("start() for " + this.id);
 
-    return Task.spawn(function* ExperimentEntry_start_task() {
-      let addons = yield installedExperimentAddons();
-      if (addons.length > 0) {
-        this._log.error("start() - there are already "
-                        + addons.length + " experiment addons installed");
-        yield uninstallAddons(addons);
-      }
-
-      yield this._installAddon();
-      gPrefs.set(PREF_ACTIVE_EXPERIMENT, true);
-    }.bind(this));
-  },
+    this._enabled = true;
+    return yield this.reconcileAddonState();
+  }),
 
   // Async install of the addon for this experiment, part of the start task above.
-  _installAddon: function* () {
+  _installAddon: Task.async(function* () {
     let deferred = Promise.defer();
 
     let hash = this._policy.ignoreHashes ? null : this._manifestData.xpiHash;
 
     let install = yield addonInstallForURL(this._manifestData.xpiURL, hash);
     gActiveInstallURLs.add(install.sourceURI.spec);
 
     let failureHandler = (install, handler) => {
@@ -1600,108 +1612,146 @@ Experiments.ExperimentEntry.prototype = 
     ["onDownloadCancelled", "onDownloadFailed", "onInstallCancelled", "onInstallFailed"]
       .forEach(what => {
         listener[what] = install => failureHandler(install, what)
       });
 
     install.addListener(listener);
     install.install();
 
-    return deferred.promise;
-  },
+    return yield deferred.promise;
+  }),
 
-  /*
+  /**
    * Stop running the experiment if it is active.
-   * @param terminationKind (optional) The termination kind, e.g. USERDISABLED or EXPIRED.
-   * @param terminationReason (optional) The termination reason details for
-   *                          termination kind RECHECK.
+   *
+   * @param terminationKind (optional)
+   *        The termination kind, e.g. ADDON_UNINSTALLED or EXPIRED.
+   * @param terminationReason (optional)
+   *        The termination reason details for termination kind RECHECK.
    * @return Promise<> Resolved when the operation is complete.
    */
-  stop: function (terminationKind, terminationReason) {
+  stop: Task.async(function* (terminationKind, terminationReason) {
     this._log.trace("stop() - id=" + this.id + ", terminationKind=" + terminationKind);
     if (!this._enabled) {
-      this._log.warning("stop() - experiment not enabled: " + id);
-      return Promise.reject();
+      throw new Error("Must not call stop() on an inactive experiment.");
     }
 
     this._enabled = false;
-    gPrefs.set(PREF_ACTIVE_EXPERIMENT, false);
+
+    let changes = yield this.reconcileAddonState();
+    let now = this._policy.now();
+    this._lastChangedDate = now;
+    this._endDate = now;
+    this._logTermination(terminationKind, terminationReason);
+
+    return changes;
+  }),
 
-    let deferred = Promise.defer();
-    let updateDates = () => {
-      let now = this._policy.now();
-      this._lastChangedDate = now;
-      this._endDate = now;
-    };
+  /**
+   * Reconcile the state of the add-on against what it's supposed to be.
+   *
+   * If we are active, ensure the add-on is enabled and up to date.
+   *
+   * If we are inactive, ensure the add-on is not installed.
+   */
+  reconcileAddonState: Task.async(function* () {
+    this._log.trace("reconcileAddonState()");
 
-    this._getAddon().then((addon) => {
-      if (!addon) {
-        let message = "could not get Addon for " + this.id;
-        this._log.warn("stop() - " + message);
-        updateDates();
-        deferred.resolve();
-        return;
+    if (!this._enabled) {
+      if (!this._addonId) {
+        this._log.trace("reconcileAddonState() - Experiment is not enabled and " +
+                        "has no add-on. Doing nothing.");
+        return this.ADDON_CHANGE_NONE;
       }
 
-      updateDates();
-      this._logTermination(terminationKind, terminationReason);
-      deferred.resolve(uninstallAddons([addon]));
-    });
+      let addon = yield this._getAddon();
+      if (!addon) {
+        this._log.trace("reconcileAddonState() - Inactive experiment has no " +
+                        "add-on. Doing nothing.");
+        return this.ADDON_CHANGE_NONE;
+      }
 
-    return deferred.promise;
-  },
+      this._log.info("reconcileAddonState() - Uninstalling add-on for inactive " +
+                     "experiment: " + addon.id);
+      gActiveUninstallAddonIDs.add(addon.id);
+      yield uninstallAddons([addon]);
+      gActiveUninstallAddonIDs.delete(addon.id);
+      return this.ADDON_CHANGE_UNINSTALL;
+    }
+
+    // If we get here, we're supposed to be active.
+
+    let changes = 0;
 
-  /**
-   * Try to ensure this experiment is active.
-   *
-   * The returned promise will be resolved if the experiment is active
-   * in the Addon Manager or rejected if it isn't.
-   *
-   * @return Promise<>
-   */
-  ensureActive: Task.async(function* () {
-    this._log.trace("ensureActive() for " + this.id);
+    // That requires an add-on.
+    let currentAddon = yield this._getAddon();
+
+    // If we have an add-on but it isn't up to date, uninstall it
+    // (to prepare for reinstall).
+    if (currentAddon && this._needsUpdate) {
+      this._log.info("reconcileAddonState() - Uninstalling add-on because update " +
+                     "needed: " + currentAddon.id);
+      gActiveUninstallAddonIDs.add(currentAddon.id);
+      yield uninstallAddons([currentAddon]);
+      gActiveUninstallAddonIDs.delete(currentAddon.id);
+      changes |= this.ADDON_CHANGE_UNINSTALL;
+    }
+
+    if (!currentAddon || this._needsUpdate) {
+      this._log.info("reconcileAddonState() - Installing add-on.");
+      yield this._installAddon();
+      changes |= this.ADDON_CHANGE_INSTALL;
+    }
 
     let addon = yield this._getAddon();
     if (!addon) {
-      this._log.warn("Experiment is not installed: " + this._addonId);
-      throw new Error("Experiment is not installed: " + this._addonId);
+      throw new Error("Could not obtain add-on for experiment that should be " +
+                      "enabled.");
     }
 
-    // User disabled likely means the experiment is disabled at startup,
-    // since the permissions don't allow it to be disabled by the user.
+    // If we have the add-on and it is enabled, we are done.
     if (!addon.userDisabled) {
-      return;
+      return changes;
     }
 
     let deferred = Promise.defer();
 
+    // Else we need to enable it.
     let listener = {
       onEnabled: enabledAddon => {
         if (enabledAddon.id != addon.id) {
           return;
         }
 
         AddonManager.removeAddonListener(listener);
         deferred.resolve();
       },
     };
 
     this._log.info("Activating add-on: " + addon.id);
     AddonManager.addAddonListener(listener);
     addon.userDisabled = false;
     yield deferred.promise;
-  }),
+    changes |= this.ADDON_CHANGE_ENABLE;
+
+    this._log.info("Add-on has been enabled: " + addon.id);
+    return changes;
+   }),
 
   /**
    * Obtain the underlying Addon from the Addon Manager.
    *
    * @return Promise<Addon|null>
    */
   _getAddon: function () {
+    if (!this._addonId) {
+      return Promise.resolve(null);
+    }
+
     let deferred = Promise.defer();
 
     AddonManager.getAddonByID(this._addonId, deferred.resolve);
 
     return deferred.promise;
   },
 
   _logTermination: function (terminationKind, terminationReason) {
@@ -1717,53 +1767,28 @@ Experiments.ExperimentEntry.prototype = 
     let data = [terminationKind, this.id];
     if (terminationReason) {
       data = data.concat(terminationReason);
     }
 
     TelemetryLog.log(TELEMETRY_LOG.TERMINATION_KEY, data);
   },
 
-  /*
-   * Stop if experiment stop criteria are met.
-   * @return Promise<boolean> Resolved when done stopping or checking,
-   *                          the value indicates whether it was stopped.
+  /**
+   * Determine whether an active experiment should be stopped.
    */
-  maybeStop: function () {
-    this._log.trace("maybeStop()");
-    return Task.spawn(function* ExperimentEntry_maybeStop_task() {
-      if (!gExperimentsEnabled) {
-        this._log.warn("maybeStop() - should not get here");
-        yield this.stop(TELEMETRY_LOG.TERMINATION.FROM_API);
-        return true;
-      }
+  shouldStop: function () {
+    if (!this._enabled) {
+      throw new Error("shouldStop must not be called on disabled experiments.");
+    }
 
-      let result = yield this._shouldStop();
-      if (result.shouldStop) {
-        let expireReasons = ["endTime", "maxActiveSeconds"];
-        if (expireReasons.indexOf(result.reason[0]) != -1) {
-          yield this.stop(TELEMETRY_LOG.TERMINATION.EXPIRED);
-        } else {
-          yield this.stop(TELEMETRY_LOG.TERMINATION.RECHECK, result.reason);
-        }
-      }
-
-      return result.shouldStop;
-    }.bind(this));
-  },
-
-  _shouldStop: function () {
     let data = this._manifestData;
     let now = this._policy.now() / 1000; // The manifest times are in seconds.
     let maxActiveSec = data.maxActiveSeconds || 0;
 
-    if (!this._enabled) {
-      return Promise.resolve({shouldStop: false});
-    }
-
     let deferred = Promise.defer();
     this.isApplicable().then(
       () => deferred.resolve({shouldStop: false}),
       reason => deferred.resolve({shouldStop: true, reason: reason})
     );
 
     return deferred.promise;
   },
--- a/browser/experiments/moz.build
+++ b/browser/experiments/moz.build
@@ -4,15 +4,15 @@
 
 EXTRA_COMPONENTS += [
     'Experiments.manifest',
     'ExperimentsService.js',
 ]
 
 JS_MODULES_PATH = 'modules/experiments'
 
-EXTRA_PP_JS_MODULES += [
+EXTRA_JS_MODULES += [
   'Experiments.jsm',
 ]
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
 
 SPHINX_TREES['experiments'] = 'docs'
--- a/browser/experiments/test/xpcshell/test_activate.js
+++ b/browser/experiments/test/xpcshell/test_activate.js
@@ -97,51 +97,52 @@ add_task(function* test_startStop() {
   let addons = yield getExperimentAddons();
   Assert.equal(addons.length, 0, "No experiment add-ons are installed.");
 
   defineNow(gPolicy, futureDate(startDate, 5 * MS_IN_ONE_DAY));
   result = yield isApplicable(experiment);
   Assert.equal(result.applicable, true, "Experiment should now be applicable.");
   Assert.equal(experiment.enabled, false, "Experiment should not be enabled.");
 
-  yield experiment.start();
+  let changes = yield experiment.start();
+  Assert.equal(changes, experiment.ADDON_CHANGE_INSTALL, "Add-on was installed.");
   addons = yield getExperimentAddons();
   Assert.equal(experiment.enabled, true, "Experiment should now be enabled.");
   Assert.equal(addons.length, 1, "1 experiment add-on is installed.");
   Assert.equal(addons[0].id, experiment._addonId, "The add-on is the one we expect.");
   Assert.equal(addons[0].userDisabled, false, "The add-on is not userDisabled.");
   Assert.ok(addons[0].isActive, "The add-on is active.");
 
-  yield experiment.stop();
+  changes = yield experiment.stop();
+  Assert.equal(changes, experiment.ADDON_CHANGE_UNINSTALL, "Add-on was uninstalled.");
   addons = yield getExperimentAddons();
   Assert.equal(experiment.enabled, false, "Experiment should not be enabled.");
   Assert.equal(addons.length, 0, "Experiment should be uninstalled from the Addon Manager.");
 
-  yield experiment.start();
+  changes = yield experiment.start();
+  Assert.equal(changes, experiment.ADDON_CHANGE_INSTALL, "Add-on was installed.");
   addons = yield getExperimentAddons();
   Assert.equal(experiment.enabled, true, "Experiment should now be enabled.");
   Assert.equal(addons.length, 1, "1 experiment add-on is installed.");
   Assert.equal(addons[0].id, experiment._addonId, "The add-on is the one we expect.");
   Assert.equal(addons[0].userDisabled, false, "The add-on is not userDisabled.");
   Assert.ok(addons[0].isActive, "The add-on is active.");
 
-  let result = yield experiment._shouldStop();
+  let result = yield experiment.shouldStop();
   Assert.equal(result.shouldStop, false, "shouldStop should be false.");
-  let maybeStop = yield experiment.maybeStop();
-  Assert.equal(maybeStop, false, "Experiment should not have been stopped.");
   Assert.equal(experiment.enabled, true, "Experiment should be enabled.");
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 1, "Experiment still in add-ons manager.");
   Assert.ok(addons[0].isActive, "The add-on is still active.");
 
   defineNow(gPolicy, futureDate(endDate, MS_IN_ONE_DAY));
-  result = yield experiment._shouldStop();
+  result = yield experiment.shouldStop();
   Assert.equal(result.shouldStop, true, "shouldStop should now be true.");
-  maybeStop = yield experiment.maybeStop();
-  Assert.equal(maybeStop, true, "Experiment should have been stopped.");
+  changes = yield experiment.stop();
+  Assert.equal(changes, experiment.ADDON_CHANGE_UNINSTALL, "Add-on should be uninstalled.");
   Assert.equal(experiment.enabled, false, "Experiment should be disabled.");
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 0, "Experiment add-on is uninstalled.");
 
   // Ensure hash validation works.
   // We set an incorrect hash and expect the install to fail.
   experiment._manifestData.xpiHash = "sha1:41014dcc66b4dcedcd973491a1530a32f0517d8a";
   let errored = false;
--- a/browser/experiments/test/xpcshell/test_api.js
+++ b/browser/experiments/test/xpcshell/test_api.js
@@ -419,17 +419,17 @@ add_task(function* test_disableExperimen
     Assert.equal(experimentInfo[k], list[0][k],
                  "Property " + k + " should match reference data.");
   }
 
   // Test disabling the experiment.
 
   now = futureDate(now, 1 * MS_IN_ONE_DAY);
   defineNow(gPolicy, now);
-  yield experiments.disableExperiment();
+  yield experiments.disableExperiment("foo");
 
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry.");
 
   experimentInfo.active = false;
   experimentInfo.endDate = now.getTime();
   for (let k of Object.keys(experimentInfo)) {
     Assert.equal(experimentInfo[k], list[0][k],
@@ -744,17 +744,17 @@ add_task(function* test_userDisabledAndU
   let todayActive = yield experiments.lastActiveToday();
   Assert.ok(todayActive, "Last active for today reports a value.");
   Assert.equal(todayActive.id, list[0].id, "The entry is what we expect.");
 
   // Explicitly disable an experiment.
 
   now = futureDate(now, 20 * MS_IN_ONE_DAY);
   defineNow(gPolicy, now);
-  yield experiments.disableExperiment();
+  yield experiments.disableExperiment("foo");
   Assert.equal(observerFireCount, ++expectedObserverFireCount,
                "Experiments observer should have been called.");
 
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, false, "Experiment should not be active anymore.");
   todayActive = yield experiments.lastActiveToday();
@@ -1400,17 +1400,17 @@ add_task(function* testForeignExperiment
     experiments: [],
   };
 
   yield experiments.init();
 
   let addons = yield getExperimentAddons();
   Assert.equal(addons.length, 0, "Precondition: No experiment add-ons present.");
 
-  let failed;
+  let failed = false;
   try {
     yield AddonTestUtils.installXPIFromURL(gDataRoot + EXPERIMENT1_XPI_NAME, EXPERIMENT1_XPI_SHA1);
   } catch (ex) {
     failed = true;
   }
   Assert.ok(failed, "Add-on install should not have completed successfully");
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 0, "Add-on install should have been cancelled.");
--- a/browser/experiments/test/xpcshell/test_cache.js
+++ b/browser/experiments/test/xpcshell/test_cache.js
@@ -238,12 +238,12 @@ add_task(function* test_cache() {
 
   experimentListData[0].active = false;
   experimentListData[0].endDate = now.getTime();
   checkExperimentListsEqual(experimentListData, list);
   checkExperimentSerializations(experiments._experiments.values());
 
   // Cleanup.
 
-  yield experiments.disableExperiment();
+  yield experiments._toggleExperimentsEnabled(false);
   yield experiments.uninit();
   yield removeCacheFile();
 });
--- a/browser/experiments/test/xpcshell/test_telemetry.js
+++ b/browser/experiments/test/xpcshell/test_telemetry.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource://gre/modules/TelemetryLog.jsm");
 Cu.import("resource://gre/modules/TelemetryPing.jsm");
-Cu.import("resource:///modules/experiments/Experiments.jsm");
+let bsp = Cu.import("resource:///modules/experiments/Experiments.jsm");
 
 
 const FILE_MANIFEST            = "experiments.manifest";
 const MANIFEST_HANDLER         = "manifests/handler";
 
 const SEC_IN_ONE_DAY  = 24 * 60 * 60;
 const MS_IN_ONE_DAY   = SEC_IN_ONE_DAY * 1000;
 
@@ -20,35 +20,17 @@ let gProfileDir          = null;
 let gHttpServer          = null;
 let gHttpRoot            = null;
 let gDataRoot            = null;
 let gReporter            = null;
 let gPolicy              = null;
 let gManifestObject      = null;
 let gManifestHandlerURI  = null;
 
-const TLOG = {
-  // log(key, [kind, experimentId, details])
-  ACTIVATION_KEY: "EXPERIMENT_ACTIVATION",
-  ACTIVATION: {
-    ACTIVATED: "ACTIVATED",
-    INSTALL_FAILURE: "INSTALL_FAILURE",
-    REJECTED: "REJECTED",
-  },
-
-  // log(key, [kind, experimentId, optionalDetails...])
-  TERMINATION_KEY: "EXPERIMENT_TERMINATION",
-  TERMINATION: {
-    USERDISABLED: "USERDISABLED",
-    FROM_API: "FROM_API",
-    EXPIRED: "EXPIRED",
-    RECHECK: "RECHECK",
-  },
-};
-
+const TLOG = bsp.TELEMETRY_LOG;
 
 let gGlobalScope = this;
 function loadAddonManager() {
   let ns = {};
   Cu.import("resource://gre/modules/Services.jsm", ns);
   let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
   let file = do_get_file(head);
   let uri = ns.Services.io.newFileURI(file);
@@ -258,30 +240,30 @@ add_task(function* test_telemetryBasics(
   Assert.equal(list.length, 2, "Experiment list should have 2 entries.");
 
   expectedLogLength += 1;
   log = TelemetryPing.getPayload().log;
   Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries.");
   checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY,
              [TLOG.ACTIVATION.ACTIVATED, EXPERIMENT2_ID]);
 
-  // Fake user-disable of an experiment.
+  // Fake user uninstall of experiment via add-on manager.
 
   now = futureDate(now, MS_IN_ONE_DAY);
   defineNow(gPolicy, now);
 
-  yield experiments.disableExperiment();
+  yield experiments.disableExperiment(TLOG.TERMINATION.ADDON_UNINSTALLED);
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 2, "Experiment list should have 2 entries.");
 
   expectedLogLength += 1;
   log = TelemetryPing.getPayload().log;
   Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries.");
   checkEvent(log[log.length-1], TLOG.TERMINATION_KEY,
-             [TLOG.TERMINATION.USERDISABLED, EXPERIMENT2_ID]);
+             [TLOG.TERMINATION.ADDON_UNINSTALLED, EXPERIMENT2_ID]);
 
   // Trigger update with experiment 1a ready to start.
 
   now = futureDate(now, MS_IN_ONE_DAY);
   defineNow(gPolicy, now);
   gManifestObject.experiments[0].id      = EXPERIMENT3_ID;
   gManifestObject.experiments[0].endTime = dateToSeconds(futureDate(now, 50 * MS_IN_ONE_DAY));
 
@@ -290,22 +272,22 @@ add_task(function* test_telemetryBasics(
   Assert.equal(list.length, 3, "Experiment list should have 3 entries.");
 
   expectedLogLength += 1;
   log = TelemetryPing.getPayload().log;
   Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries.");
   checkEvent(log[log.length-1], TLOG.ACTIVATION_KEY,
              [TLOG.ACTIVATION.ACTIVATED, EXPERIMENT3_ID]);
 
-  // Trigger non-user-disable of an experiment via the API
+  // Trigger disable of an experiment via the API.
 
   now = futureDate(now, MS_IN_ONE_DAY);
   defineNow(gPolicy, now);
 
-  yield experiments.disableExperiment(false);
+  yield experiments.disableExperiment(TLOG.TERMINATION.FROM_API);
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 3, "Experiment list should have 3 entries.");
 
   expectedLogLength += 1;
   log = TelemetryPing.getPayload().log;
   Assert.equal(log.length, expectedLogLength, "Telemetry log should have " + expectedLogLength + " entries.");
   checkEvent(log[log.length-1], TLOG.TERMINATION_KEY,
              [TLOG.TERMINATION.FROM_API, EXPERIMENT3_ID]);
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -225,16 +225,19 @@ Var ControlRightPX
 !insertmacro GetOptions
 !insertmacro StrFilter
 
 !include "locales.nsi"
 !include "branding.nsi"
 
 !include "defines.nsi"
 
+; Must be included after defines.nsi
+!include "locale-fonts.nsh"
+
 ; The OFFICIAL define is a workaround to support different urls for Release and
 ; Beta since they share the same branding when building with other branches that
 ; set the update channel to beta.
 !ifdef OFFICIAL
 !ifdef BETA_UPDATE_CHANNEL
 !undef URLStubDownload
 !define URLStubDownload "http://download.mozilla.org/?os=win&lang=${AB_CD}&product=firefox-beta-latest"
 !undef URLManualDownload
@@ -411,19 +414,37 @@ Function .onInit
   StrCpy $CheckboxSendPing "1"
 !ifdef MOZ_MAINTENANCE_SERVICE
   StrCpy $CheckboxInstallMaintSvc "1"
 !else
   StrCpy $CheckboxInstallMaintSvc "0"
 !endif
   StrCpy $WasOptionsButtonClicked "0"
 
-  CreateFont $FontBlurb "$(^Font)" "12" "500"
-  CreateFont $FontNormal "$(^Font)" "11" "500"
-  CreateFont $FontItalic "$(^Font)" "11" "500" /ITALIC
+  StrCpy $0 ""
+!ifdef FONT_FILE1
+  ${If} ${FileExists} "$FONTS\${FONT_FILE1}"
+    StrCpy $0 "${FONT_NAME1}"
+  ${EndIf}
+!endif
+
+!ifdef FONT_FILE2
+  ${If} $0 == ""
+  ${AndIf} ${FileExists} "$FONTS\${FONT_FILE2}"
+    StrCpy $0 "${FONT_NAME2}"
+  ${EndIf}
+!endif
+
+  ${If} $0 == ""
+    StrCpy $0 "$(^Font)"
+  ${EndIf}
+
+  CreateFont $FontBlurb "$0" "12" "500"
+  CreateFont $FontNormal "$0" "11" "500"
+  CreateFont $FontItalic "$0" "11" "500" /ITALIC
 
   InitPluginsDir
   File /oname=$PLUGINSDIR\bgintro.bmp "bgintro.bmp"
   File /oname=$PLUGINSDIR\appname.bmp "appname.bmp"
   File /oname=$PLUGINSDIR\clock.bmp "clock.bmp"
   File /oname=$PLUGINSDIR\particles.bmp "particles.bmp"
 !ifdef ${AB_CD}_rtl
   ; The horizontally flipped pencil looks better in RTL
--- a/browser/locales/en-US/chrome/browser/devtools/connection-screen.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/connection-screen.dtd
@@ -8,16 +8,17 @@
   - -->
 
 <!ENTITY title      "Connect">
 <!ENTITY header     "Connect to remote device">
 <!ENTITY host       "Host:">
 <!ENTITY port       "Port:">
 <!ENTITY connect    "Connect">
 <!ENTITY connecting "Connecting…">
+<!ENTITY availableAddons "Available remote add-ons:">
 <!ENTITY availableTabs "Available remote tabs:">
 <!ENTITY availableProcesses "Available remote processes:">
 <!ENTITY connectionError "Error:">
 <!ENTITY errorTimeout "Error: connection timeout.">
 <!ENTITY errorRefused "Error: connection refused.">
 <!ENTITY errorUnexpected "Unexpected error.">
 
 <!-- LOCALIZATION NOTE (remoteHelp, remoteDocumentation, remoteHelpSuffix):
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -136,46 +136,49 @@ function prompt(aContentWindow, aCallID,
     // The real callback will be set during the "showing" event. The
     // empty function here is so that PopupNotifications.show doesn't
     // reject the action.
     callback: function() {}
   };
 
   let secondaryActions = [
     {
-      label: stringBundle.getString("getUserMedia.always.label"),
-      accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
-      callback: function () {
-        // don't save unless secure load!
-        mainAction.callback(aSecure);
-      }
-    },
-    {
       label: stringBundle.getString("getUserMedia.denyRequest.label"),
       accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
       callback: function () {
         denyRequest(aCallID);
       }
     },
     {
       label: stringBundle.getString("getUserMedia.never.label"),
       accessKey: stringBundle.getString("getUserMedia.never.accesskey"),
       callback: function () {
         denyRequest(aCallID);
-	// Let someone save "Never" for http sites so that they can be stopped from
-	// bothering you with doorhangers
+        // Let someone save "Never" for http sites so that they can be stopped from
+        // bothering you with doorhangers.
         let perms = Services.perms;
         if (audioDevices.length)
           perms.add(uri, "microphone", perms.DENY_ACTION);
         if (videoDevices.length)
           perms.add(uri, "camera", perms.DENY_ACTION);
       }
     }
   ];
 
+  if (aSecure) {
+    // Don't show the 'Always' action if the connection isn't secure.
+    secondaryActions.unshift({
+      label: stringBundle.getString("getUserMedia.always.label"),
+      accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
+      callback: function () {
+        mainAction.callback(true);
+      }
+    });
+  }
+
   let options = {
     eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "swapping")
         return true;
 
       let chromeDoc = this.browser.ownerDocument;
 
       if (aTopic == "shown") {
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -30,16 +30,21 @@
   display: block;
   width: 64px;
   height: 64px;
   position: absolute;
   animation: moveX 3.05s linear 0s infinite alternate,
              moveY 3.4s linear 0s infinite alternate;
 }
 
+#PanelUI-popup #PanelUI-contents:-moz-locale-dir(rtl):empty::before {
+  animation: moveXRTL 3.05s linear 0s infinite alternate,
+             moveY 3.4s linear 0s infinite alternate;
+}
+
 #PanelUI-popup #PanelUI-contents:empty:hover::before {
   background-image: url(chrome://browser/skin/customizableui/whimsy.png);
 }
 
 @media (min-resolution: 2dppx) {
   #PanelUI-popup #PanelUI-contents:empty::before {
     background-image: url(chrome://browser/skin/customizableui/whimsy-bw@2x.png);
     background-size: 64px 64px;
@@ -48,16 +53,22 @@
     background-image: url(chrome://browser/skin/customizableui/whimsy@2x.png);
   }
 }
 
 @keyframes moveX {
   /* These values are adjusted for the padding on the panel. */
   from { margin-left: -15px; } to { margin-left: calc(100% - 49px); }
 }
+
+@keyframes moveXRTL {
+  /* These values are adjusted for the padding on the panel. */
+  from { margin-right: -15px; } to { margin-right: calc(100% - 49px); }
+}
+
 @keyframes moveY {
   /* These values are adjusted for the padding and height of the panel. */
   from { margin-top: -.5em; } to { margin-top: calc(64px - .5em); }
 }
 
 #PanelUI-button {
   background-image: linear-gradient(to bottom, hsla(0,0%,100%,0), hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, hsla(0,0%,100%,0)),
                     linear-gradient(to bottom, hsla(210,54%,20%,0), hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, hsla(210,54%,20%,0)),
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -288,29 +288,45 @@ case "$target" in
         fi
     fi
 
     android_tools="$android_sdk_root"/tools
     android_platform_tools="$android_sdk_root"/platform-tools
     if test ! -d "$android_platform_tools" ; then
         android_platform_tools="$android_sdk"/tools # SDK Tools < r8
     fi
-    # The build tools got moved around to different directories in
-    # SDK Tools r22.  Try to locate them.
+
+    dnl The build tools got moved around to different directories in SDK
+    dnl Tools r22. Try to locate them. This is awful, but, from
+    dnl http://stackoverflow.com/a/4495368, the following sorts versions
+    dnl of the form x.y.z.a.b from newest to oldest:
+    dnl sort -t. -k 1,1nr -k 2,2nr -k 3,3nr -k 4,4nr -k 5,5nr
+    dnl We want to favour the newer versions that start with 'android-';
+    dnl that's what the sed is about.
+    dnl We might iterate over directories that aren't build-tools at all;
+    dnl we use the presence of aapt as a marker.
+    AC_MSG_CHECKING([for android build-tools directory])
     android_build_tools=""
-    for suffix in android-4.4 android-4.3 android-4.2.2 19.0.3 19.0.2 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0; do
-        tools_directory="$android_sdk_root/build-tools/$suffix"
-        if test -d "$tools_directory" ; then
+    for suffix in `ls "$android_sdk_root/build-tools" | sed -e "s,android-,999.," | sort -t. -k 1,1nr -k 2,2nr -k 3,3nr -k 4,4nr -k 5,5nr`; do
+        tools_directory=`echo "$android_sdk_root/build-tools/$suffix" | sed -e "s,999.,android-,"`
+        if test -d "$tools_directory" -a -f "$tools_directory/aapt"; then
             android_build_tools="$tools_directory"
             break
         fi
     done
     if test -z "$android_build_tools" ; then
         android_build_tools="$android_platform_tools" # SDK Tools < r22
     fi
+
+    if test -d "$android_build_tools" -a -f "$android_build_tools/aapt"; then
+        AC_MSG_RESULT([$android_build_tools])
+    else
+        AC_MSG_ERROR([not found. Please check your SDK for the subdirectory of build-tools. With the current configuration, it should be in $android_sdk_root/build_tools])
+    fi
+
     ANDROID_SDK="${android_sdk}"
     ANDROID_SDK_ROOT="${android_sdk_root}"
     if test -e "${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar" ; then
         ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar"
     else
         ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/support/v4/android-support-v4.jar";
     fi
     ANDROID_TOOLS="${android_tools}"
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -149,17 +149,16 @@ public abstract class GeckoApp
     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";
     public static final String ACTION_WEBAPP_PREFIX        = "org.mozilla.gecko.WEBAPP";
 
     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
 
     public static final String PREFS_ALLOW_STATE_BUNDLE    = "allowStateBundle";
-    public static final String PREFS_CRASHED               = "crashed";
     public static final String PREFS_OOM_EXCEPTION         = "OOMException";
     public static final String PREFS_VERSION_CODE          = "versionCode";
     public static final String PREFS_WAS_STOPPED           = "wasStopped";
     public static final String PREFS_CLEANUP_TEMP_FILES    = "cleanupTempFiles";
 
     public static final String SAVED_STATE_IN_BACKGROUND   = "inBackground";
     public static final String SAVED_STATE_PRIVATE_SESSION = "privateSession";
 
@@ -1767,19 +1766,22 @@ public abstract class GeckoApp
                 public void run() {
                     prefs.edit()
                          .putInt(PREFS_VERSION_CODE, versionCode)
                          .commit();
                 }
             });
 
             shouldRestore = true;
-        } else if (savedInstanceState != null || getSessionRestorePreference().equals("always") || getRestartFromIntent()) {
+        } else if (savedInstanceState != null ||
+                   getSessionRestorePreference().equals("always") ||
+                   getRestartFromIntent() ||
+                   prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, false)) {
             // We're coming back from a background kill by the OS, the user
-            // has chosen to always restore, or we just restarted.
+            // has chosen to always restore, we restarted, or we crashed.
             shouldRestore = true;
         }
 
         return shouldRestore;
     }
 
     private String getSessionRestorePreference() {
         return getSharedPreferences().getString(GeckoPreferences.PREFS_RESTORE_SESSION, "quit");
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -685,17 +685,16 @@ sync_java_files = [
     'sync/net/WBORequestDelegate.java',
     'sync/NoCollectionKeysSetException.java',
     'sync/NodeAuthenticationException.java',
     'sync/NonArrayJSONException.java',
     'sync/NonObjectJSONException.java',
     'sync/NullClusterURLException.java',
     'sync/PersistedMetaGlobal.java',
     'sync/PrefsBackoffHandler.java',
-    'sync/PrefsSource.java',
     'sync/receivers/SyncAccountDeletedReceiver.java',
     'sync/receivers/SyncAccountDeletedService.java',
     'sync/receivers/UpgradeReceiver.java',
     'sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java',
     'sync/repositories/android/AndroidBrowserBookmarksRepository.java',
     'sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java',
     'sync/repositories/android/AndroidBrowserHistoryDataAccessor.java',
     'sync/repositories/android/AndroidBrowserHistoryDataExtender.java',
--- a/mobile/android/base/background/fxa/FxAccountClient10.java
+++ b/mobile/android/base/background/fxa/FxAccountClient10.java
@@ -171,42 +171,45 @@ public class FxAccountClient10 {
    */
   protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
     protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
 
     protected final RequestDelegate<T> delegate;
 
     protected final byte[] tokenId;
     protected final byte[] reqHMACKey;
-    protected final boolean payload;
     protected final SkewHandler skewHandler;
 
     /**
      * Create a delegate for an un-authenticated resource.
      */
     public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
-      this(resource, delegate, null, null, false);
+      this(resource, delegate, null, null);
     }
 
     /**
      * Create a delegate for a Hawk-authenticated resource.
+     * <p>
+     * Every Hawk request that encloses an entity (PATCH, POST, and PUT) will
+     * include the payload verification hash.
      */
-    public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, final byte[] tokenId, final byte[] reqHMACKey, final boolean authenticatePayload) {
+    public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, final byte[] tokenId, final byte[] reqHMACKey) {
       super(resource);
       this.delegate = delegate;
       this.reqHMACKey = reqHMACKey;
       this.tokenId = tokenId;
-      this.payload = authenticatePayload;
       this.skewHandler = SkewHandler.getSkewHandlerForResource(resource);
     }
 
     @Override
     public AuthHeaderProvider getAuthHeaderProvider() {
       if (tokenId != null && reqHMACKey != null) {
-        return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload, skewHandler.getSkewInSeconds());
+        // We always include the payload verification hash for FxA Hawk-authenticated requests.
+        final boolean includePayloadVerificationHash = true;
+        return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, includePayloadVerificationHash, skewHandler.getSkewInSeconds());
       }
       return super.getAuthHeaderProvider();
     }
 
     @Override
     public String getUserAgent() {
       return FxAccountConstants.USER_AGENT;
     }
@@ -478,17 +481,17 @@ public class FxAccountClient10 {
     BaseResource resource;
     try {
       resource = new BaseResource(new URI(serverURI + "session/create"));
     } catch (URISyntaxException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
-    resource.delegate = new ResourceDelegate<TwoTokens>(resource, delegate, tokenId, reqHMACKey, false) {
+    resource.delegate = new ResourceDelegate<TwoTokens>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
           byte[] keyFetchToken = new byte[32];
           byte[] sessionToken = new byte[32];
           unbundleBody(body, requestKey, FxAccountUtils.KW("session/create"), keyFetchToken, sessionToken);
           delegate.handleSuccess(new TwoTokens(keyFetchToken, sessionToken));
           return;
@@ -514,17 +517,17 @@ public class FxAccountClient10 {
     BaseResource resource;
     try {
       resource = new BaseResource(new URI(serverURI + "session/destroy"));
     } catch (URISyntaxException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
-    resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey, false) {
+    resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         delegate.handleSuccess(null);
       }
     };
     post(resource, null, delegate);
   }
 
@@ -603,17 +606,17 @@ public class FxAccountClient10 {
     BaseResource resource;
     try {
       resource = new BaseResource(new URI(serverURI + "account/keys"));
     } catch (URISyntaxException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
-    resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey, false) {
+    resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
           byte[] kA = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
           byte[] wrapkB = new byte[FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES];
           unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
           delegate.handleSuccess(new TwoKeys(kA, wrapkB));
           return;
@@ -665,17 +668,17 @@ public class FxAccountClient10 {
     BaseResource resource;
     try {
       resource = new BaseResource(new URI(serverURI + "recovery_email/status"));
     } catch (URISyntaxException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
-    resource.delegate = new ResourceDelegate<StatusResponse>(resource, delegate, tokenId, reqHMACKey, false) {
+    resource.delegate = new ResourceDelegate<StatusResponse>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
           String[] requiredStringFields = new String[] { JSON_KEY_EMAIL };
           body.throwIfFieldsMissingOrMisTyped(requiredStringFields, String.class);
           String email = body.getString(JSON_KEY_EMAIL);
           Boolean verified = body.getBoolean(JSON_KEY_VERIFIED);
           delegate.handleSuccess(new StatusResponse(email, verified));
@@ -707,17 +710,17 @@ public class FxAccountClient10 {
     BaseResource resource;
     try {
       resource = new BaseResource(new URI(serverURI + "certificate/sign"));
     } catch (URISyntaxException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
-    resource.delegate = new ResourceDelegate<String>(resource, delegate, tokenId, reqHMACKey, true) {
+    resource.delegate = new ResourceDelegate<String>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         String cert = body.getString("cert");
         if (cert == null) {
           delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
           return;
         }
         delegate.handleSuccess(cert);
@@ -748,17 +751,17 @@ public class FxAccountClient10 {
     BaseResource resource;
     try {
       resource = new BaseResource(new URI(serverURI + "recovery_email/resend_code"));
     } catch (URISyntaxException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
-    resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey, false) {
+    resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
           delegate.handleSuccess(null);
           return;
         } catch (Exception e) {
           delegate.handleError(e);
           return;
--- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
@@ -1,16 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
 import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
@@ -19,27 +22,32 @@ import org.mozilla.gecko.fxa.FxAccountCo
 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.setup.Constants;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
+import android.accounts.Account;
 import android.accounts.AccountManager;
+import android.content.Context;
 import android.content.Intent;
+import android.os.AsyncTask;
 import android.text.Editable;
 import android.text.TextWatcher;
 import android.text.method.PasswordTransformationMethod;
 import android.text.method.SingleLineTransformationMethod;
 import android.util.Patterns;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnFocusChangeListener;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 
 abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay {
   public FxAccountAbstractSetupActivity() {
@@ -49,17 +57,17 @@ abstract public class FxAccountAbstractS
   protected FxAccountAbstractSetupActivity(int resume) {
     super(resume);
   }
 
   private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName();
 
   protected int minimumPasswordLength = 8;
 
-  protected EditText emailEdit;
+  protected AutoCompleteTextView emailEdit;
   protected EditText passwordEdit;
   protected Button showPasswordButton;
   protected TextView remoteErrorTextView;
   protected Button button;
   protected ProgressBar progressBar;
 
   protected void createShowPasswordButton() {
     showPasswordButton.setOnClickListener(new OnClickListener() {
@@ -307,9 +315,59 @@ abstract public class FxAccountAbstractS
   /**
    * Factory function that produces a new PasswordStretcher instance.
    *
    * @return PasswordStretcher instance.
    */
   protected PasswordStretcher makePasswordStretcher(String password) {
     return new QuickPasswordStretcher(password);
   }
+
+  protected abstract static class GetAccountsAsyncTask extends AsyncTask<Void, Void, Account[]> {
+    protected final Context context;
+
+    public GetAccountsAsyncTask(Context context) {
+      super();
+      this.context = context;
+    }
+
+    @Override
+    protected Account[] doInBackground(Void... params) {
+      return AccountManager.get(context).getAccounts();
+    }
+  }
+
+  /**
+   * This updates UI, so needs to be done on the foreground thread.
+   */
+  protected void populateEmailAddressAutocomplete(Account[] accounts) {
+    // First a set, since we don't want repeats.
+    final Set<String> emails = new HashSet<String>();
+    for (Account account : accounts) {
+      if (!Patterns.EMAIL_ADDRESS.matcher(account.name).matches()) {
+        continue;
+      }
+      emails.add(account.name);
+    }
+
+    // And then sorted in alphabetical order.
+    final String[] sortedEmails = emails.toArray(new String[0]);
+    Arrays.sort(sortedEmails);
+
+    final ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, android.R.layout.simple_dropdown_item_1line, sortedEmails);
+    emailEdit.setAdapter(adapter);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+
+    // Getting Accounts accesses databases on disk, so needs to be done on a
+    // background thread.
+    final GetAccountsAsyncTask task = new GetAccountsAsyncTask(this) {
+      @Override
+      public void onPostExecute(Account[] accounts) {
+        populateEmailAddressAutocomplete(accounts);
+      }
+    };
+    task.execute();
+  }
 }
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
@@ -30,16 +30,17 @@ import android.content.Intent;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.text.Spannable;
 import android.text.Spanned;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ClickableSpan;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.EditText;
 import android.widget.ListView;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
 /**
@@ -61,17 +62,17 @@ public class FxAccountCreateAccountActiv
    */
   @Override
   public void onCreate(Bundle icicle) {
     Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
 
     super.onCreate(icicle);
     setContentView(R.layout.fxaccount_create_account);
 
-    emailEdit = (EditText) ensureFindViewById(null, R.id.email, "email edit");
+    emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
     passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
     showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
     yearEdit = (EditText) ensureFindViewById(null, R.id.year_edit, "year edit");
     remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
     button = (Button) ensureFindViewById(null, R.id.button, "create account button");
     progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
     chooseCheckBox = (CheckBox) ensureFindViewById(null, R.id.choose_what_to_sync_checkbox, "choose what to sync check box");
     selectedEngines = new HashMap<String, Boolean>();
deleted file mode 100644
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountFragment.java
+++ /dev/null
@@ -1,191 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.fxa.activities;
-
-import android.support.v4.app.Fragment;
-
-public class FxAccountCreateAccountFragment extends Fragment { // implements OnClickListener {
-  protected static final String LOG_TAG = FxAccountCreateAccountFragment.class.getSimpleName();
-//
-//  protected FxAccountSetupActivity activity;
-//
-//  protected EditText emailEdit;
-//  protected EditText passwordEdit;
-//  protected EditText password2Edit;
-//  protected Button button;
-//
-//  protected TextView emailError;
-//  protected TextView passwordError;
-//
-//  protected TextChangedListener textChangedListener;
-//  protected EditorActionListener editorActionListener;
-//  protected OnFocusChangeListener focusChangeListener;
-//  @Override
-//  public void onCreate(Bundle savedInstanceState) {
-//    super.onCreate(savedInstanceState);
-//    // Retain this fragment across configuration changes. See, for example,
-//    // http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html
-//    // This fragment will own AsyncTask instances which should not be
-//    // interrupted by configuration changes (and activity changes).
-//    setRetainInstance(true);
-//  }
-
-//  @Override
-//  public View onCreateView(LayoutInflater inflater, ViewGroup container,
-//      Bundle savedInstanceState) {
-//    View v = inflater.inflate(R.layout.fxaccount_create_account_fragment, container, false);
-//
-//    FxAccountSetupActivity.linkifyTextViews(v, new int[] { R.id.description, R.id.policy });
-//
-//    emailEdit = (EditText) ensureFindViewById(v, R.id.email, "email");
-//    passwordEdit = (EditText) ensureFindViewById(v, R.id.password, "password");
-//    // Second password can be null.
-//    password2Edit = (EditText) v.findViewById(R.id.password2);
-//
-//    emailError = (TextView) ensureFindViewById(v, R.id.email_error, "email error");
-//    passwordError = (TextView) ensureFindViewById(v, R.id.password_error, "password error");
-//
-//    textChangedListener = new TextChangedListener();
-//    editorActionListener = new EditorActionListener();
-//    focusChangeListener = new FocusChangeListener();
-//
-//    addListeners(emailEdit);
-//    addListeners(passwordEdit);
-//    if (password2Edit != null) {
-//      addListeners(password2Edit);
-//    }
-//
-//    button = (Button) ensureFindViewById(v, R.id.create_account_button, "button");
-//    button.setOnClickListener(this);
-//    return v;
-//  }
-
-//  protected void onCreateAccount(View button) {
-//    Logger.debug(LOG_TAG, "onCreateAccount: Asking for username/password for new account.");
-//    String email = emailEdit.getText().toString();
-//    String password = passwordEdit.getText().toString();
-//    activity.signUp(email, password);
-//  }
-//
-//  @Override
-//  public void onClick(View v) {
-//    switch (v.getId()) {
-//    case R.id.create_account_button:
-//      if (!validate(false)) {
-//        return;
-//      }
-//      onCreateAccount(v);
-//      break;
-//    }
-//  }
-//
-//  protected void addListeners(EditText editText) {
-//    editText.addTextChangedListener(textChangedListener);
-//    editText.setOnEditorActionListener(editorActionListener);
-//    editText.setOnFocusChangeListener(focusChangeListener);
-//  }
-//
-//  protected class FocusChangeListener implements OnFocusChangeListener {
-//    @Override
-//    public void onFocusChange(View v, boolean hasFocus) {
-//      if (hasFocus) {
-//        return;
-//      }
-//      validate(false);
-//    }
-//  }
-//
-//  protected class EditorActionListener implements OnEditorActionListener {
-//    @Override
-//    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
-//      validate(false);
-//      return false;
-//    }
-//  }
-//
-//  protected class TextChangedListener implements TextWatcher {
-//    @Override
-//    public void afterTextChanged(Editable s) {
-//      validate(true);
-//    }
-//
-//    @Override
-//    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
-//      // Do nothing.
-//    }
-//
-//    @Override
-//    public void onTextChanged(CharSequence s, int start, int before, int count) {
-//      // Do nothing.
-//    }
-//  }
-//
-//  /**
-//   * Show or hide error messaging.
-//   *
-//   * @param removeOnly
-//   *          if true, possibly remove existing error messages but do not set an
-//   *          error message if one was not present.
-//   * @param errorResourceId
-//   *          of error string, or -1 to hide.
-//   * @param errorView
-//   *          <code>TextView</code> instance to display error message in.
-//   * @param edits
-//   *          <code>EditText</code> instances to style.
-//   */
-//  protected void setError(boolean removeOnly, int errorResourceId, TextView errorView, EditText... edits) {
-//    if (removeOnly && errorResourceId != -1) {
-//      return;
-//    }
-//
-//    int res = errorResourceId == -1 ? R.drawable.fxaccount_textfield_background : R.drawable.fxaccount_textfield_error_background;
-//    for (EditText edit : edits) {
-//      if (edit == null) {
-//        continue;
-//      }
-//      edit.setBackgroundResource(res);
-//    }
-//    if (errorResourceId == -1) {
-//      errorView.setVisibility(View.GONE);
-//      errorView.setText(null);
-//    } else {
-//      errorView.setText(errorResourceId);
-//      errorView.setVisibility(View.VISIBLE);
-//    }
-//  }
-//
-//  protected boolean validate(boolean removeOnly) {
-//    boolean enabled = true;
-//    final String email = emailEdit.getText().toString();
-//    final String password = passwordEdit.getText().toString();
-//
-//    if (email.length() == 0 || Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
-//      setError(removeOnly, -1, emailError, emailEdit);
-//    } else {
-//      enabled = false;
-//      setError(removeOnly, R.string.fxaccount_bad_email, emailError, emailEdit);
-//    }
-//
-//    if (password2Edit != null) {
-//      final String password2 = password2Edit.getText().toString();
-//      enabled = enabled && password2.length() > 0;
-//
-//      boolean passwordsMatch = password.equals(password2);
-//      if (passwordsMatch) {
-//        setError(removeOnly, -1, passwordError, passwordEdit, password2Edit);
-//      } else {
-//        enabled = false;
-//        setError(removeOnly, R.string.fxaccount_bad_passwords, passwordError, passwordEdit, password2Edit);
-//      }
-//    }
-//
-//    if (enabled != button.isEnabled()) {
-//      Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button.");
-//      button.setEnabled(enabled);
-//    }
-//
-//    return enabled;
-//  }
-}
--- a/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
@@ -18,16 +18,17 @@ import org.mozilla.gecko.background.fxa.
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
 /**
  * Activity which displays sign in screen to the user.
  */
@@ -41,17 +42,17 @@ public class FxAccountSignInActivity ext
    */
   @Override
   public void onCreate(Bundle icicle) {
     Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
 
     super.onCreate(icicle);
     setContentView(R.layout.fxaccount_sign_in);
 
-    emailEdit = (EditText) ensureFindViewById(null, R.id.email, "email edit");
+    emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
     passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
     showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
     remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
     button = (Button) ensureFindViewById(null, R.id.button, "sign in button");
     progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
 
     minimumPasswordLength = 1; // Minimal restriction on passwords entered to sign in.
     createSignInButton();
--- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
@@ -23,16 +23,17 @@ import org.mozilla.gecko.fxa.authenticat
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
 /**
  * Activity which displays a screen for updating the local password.
  */
@@ -54,17 +55,17 @@ public class FxAccountUpdateCredentialsA
    */
   @Override
   public void onCreate(Bundle icicle) {
     Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
 
     super.onCreate(icicle);
     setContentView(R.layout.fxaccount_update_credentials);
 
-    emailEdit = (EditText) ensureFindViewById(null, R.id.email, "email edit");
+    emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
     passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
     showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
     remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
     button = (Button) ensureFindViewById(null, R.id.button, "update credentials");
     progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
 
     minimumPasswordLength = 1; // Minimal restriction on passwords entered to sign in.
     createButton();
--- a/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
+++ b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
@@ -1,45 +1,39 @@
 /* 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.fxa.sync;
 
 import java.io.IOException;
-import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Collections;
 import java.util.HashMap;
 
 import org.json.simple.parser.ParseException;
-import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
 import android.content.Context;
 
 public class FxAccountGlobalSession extends GlobalSession {
-  private static final String LOG_TAG = FxAccountGlobalSession.class.getSimpleName();
-
-  public FxAccountGlobalSession(String storageEndpoint, SyncConfiguration config, BaseGlobalSessionCallback callback,
-      Context context, ClientsDataDelegate clientsDelegate)
-      throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException, URISyntaxException {
+  public FxAccountGlobalSession(SyncConfiguration config,
+                                BaseGlobalSessionCallback callback,
+                                Context context,
+                                ClientsDataDelegate clientsDelegate)
+                                    throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException, URISyntaxException {
     super(config, callback, context, clientsDelegate, null);
-    URI storageURI = new URI(storageEndpoint);
-    this.config.setClusterURL(storageURI);
-    FxAccountConstants.pii(LOG_TAG, "clusterURL is " + config.getClusterURLString());
   }
 
   @Override
   public void prepareStages() {
     super.prepareStages();
     HashMap<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>();
     stages.putAll(this.stages);
     stages.put(Stage.ensureClusterURL, new CheckPreconditionsStage());
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -15,32 +15,29 @@ import java.util.concurrent.ExecutorServ
 import java.util.concurrent.Executors;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
-import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
-import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AccountPickler;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
 import org.mozilla.gecko.fxa.login.Married;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
 import org.mozilla.gecko.fxa.login.StateFactory;
 import org.mozilla.gecko.sync.BackoffHandler;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.PrefsBackoffHandler;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.ThreadPool;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
@@ -363,35 +360,40 @@ public class FxAccountSyncAdapter extend
           ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
 
           // We compute skew over time using SkewHandler. This yields an unchanging
           // skew adjustment that the HawkAuthHeaderProvider uses to adjust its
           // timestamps. Eventually we might want this to adapt within the scope of a
           // global session.
           final SkewHandler storageServerSkewHandler = SkewHandler.getSkewHandlerForHostname(storageHostname);
           final long storageServerSkew = storageServerSkewHandler.getSkewInSeconds();
-          final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, storageServerSkew);
+          // We expect Sync to upload large sets of records. Calculating the
+          // payload verification hash for these record sets could be expensive,
+          // so we explicitly do not send payload verification hashes to the
+          // Sync storage endpoint.
+          final boolean includePayloadVerificationHash = false;
+          final AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), includePayloadVerificationHash, storageServerSkew);
 
           final Context context = getContext();
           final SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
 
           Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
           syncConfig.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
+          syncConfig.setClusterURL(storageServerURI);
 
-          globalSession = new FxAccountGlobalSession(token.endpoint, syncConfig, callback, context, clientsDataDelegate);
+          globalSession = new FxAccountGlobalSession(syncConfig, callback, context, clientsDataDelegate);
           globalSession.start();
         } catch (Exception e) {
           callback.handleError(globalSession, e);
           return;
         }
       }
 
       @Override
       public void handleFailure(TokenServerException e) {
-        debugAssertion(audience, assertion);
         handleError(e);
       }
 
       @Override
       public void handleError(Exception e) {
         Logger.error(LOG_TAG, "Failed to get token.", e);
         callback.handleError(null, e);
       }
@@ -606,39 +608,9 @@ public class FxAccountSyncAdapter extend
     } catch (Exception e) {
       Logger.error(LOG_TAG, "Got error syncing.", e);
       syncDelegate.handleError(e);
     }
 
     Logger.info(LOG_TAG, "Syncing done.");
     lastSyncRealtimeMillis = SystemClock.elapsedRealtime();
   }
-
-  protected void debugAssertion(String audience, String assertion) {
-    final CountDownLatch verifierLatch = new CountDownLatch(1);
-    BrowserIDRemoteVerifierClient client = new BrowserIDRemoteVerifierClient(URI.create(BrowserIDRemoteVerifierClient.DEFAULT_VERIFIER_URL));
-    client.verify(audience, assertion, new BrowserIDVerifierDelegate() {
-      @Override
-      public void handleSuccess(ExtendedJSONObject response) {
-        Logger.info(LOG_TAG, "Remote verifier returned success: " + response.toJSONString());
-        verifierLatch.countDown();
-      }
-
-      @Override
-      public void handleFailure(ExtendedJSONObject response) {
-        Logger.warn(LOG_TAG, "Remote verifier returned failure: " + response.toJSONString());
-        verifierLatch.countDown();
-      }
-
-      @Override
-      public void handleError(Exception e) {
-        Logger.error(LOG_TAG, "Remote verifier returned error.", e);
-        verifierLatch.countDown();
-      }
-    });
-
-    try {
-      verifierLatch.await();
-    } catch (InterruptedException e) {
-      Logger.error(LOG_TAG, "Got error.", e);
-    }
-  }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -49,16 +49,17 @@
 <!ENTITY go "Go">
 <!ENTITY search "Search">
 <!ENTITY reload "Reload">
 <!ENTITY forward "Forward">
 <!ENTITY menu "Menu">
 <!ENTITY back "Back">
 <!ENTITY stop "Stop">
 <!ENTITY site_security "Site Security">
+<!ENTITY edit_mode_cancel "Cancel">
 
 <!ENTITY close_tab "Close Tab">
 <!ENTITY one_tab "1 tab">
 <!-- Localization note (num_tabs2) : Number of tabs is always more than one.
      We can't use android plural forms, sadly. See bug #753859. -->
 <!ENTITY num_tabs2 "&formatD; tabs">
 <!ENTITY new_tab_opened "New tab opened">
 
--- a/mobile/android/base/menu/MenuItemActionView.java
+++ b/mobile/android/base/menu/MenuItemActionView.java
@@ -56,17 +56,19 @@ public class MenuItemActionView extends 
         mMenuItem = (MenuItemDefault) findViewById(R.id.menu_item);
         mMenuButton = (MenuItemActionBar) findViewById(R.id.menu_item_button);
         mActionButtons = new ArrayList<ImageButton>();
     }
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         View parent = (View) getParent();
-        if ((right - left) < parent.getMeasuredWidth() || mActionButtons.size() != 0) {
+        final int padding = getPaddingLeft() + getPaddingRight();
+        final int parentPadding = parent.getPaddingLeft() + parent.getPaddingRight();
+        if ((right - left - padding) < (parent.getMeasuredWidth() - parentPadding) || mActionButtons.size() != 0) {
             // Use the icon.
             mMenuItem.setVisibility(View.GONE);
             mMenuButton.setVisibility(View.VISIBLE);
         } else {
             // Use the button.
             mMenuItem.setVisibility(View.VISIBLE);
             mMenuButton.setVisibility(View.GONE);
         }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -434,16 +434,17 @@ gbjar.generated_sources += [
     'org/mozilla/gecko/SysInfo.java',
     'org/mozilla/gecko/widget/ThemedEditText.java',
     'org/mozilla/gecko/widget/ThemedImageButton.java',
     'org/mozilla/gecko/widget/ThemedImageView.java',
     'org/mozilla/gecko/widget/ThemedLinearLayout.java',
     'org/mozilla/gecko/widget/ThemedRelativeLayout.java',
     'org/mozilla/gecko/widget/ThemedTextSwitcher.java',
     'org/mozilla/gecko/widget/ThemedTextView.java',
+    'org/mozilla/gecko/widget/ThemedView.java',
 ]
 if CONFIG['MOZ_CRASHREPORTER']:
     gbjar.sources += [ 'CrashReporter.java' ]
     ANDROID_RES_DIRS += [ SRCDIR + '/crashreporter/res' ]
 
 gbjar.sources += sync_java_files
 gbjar.generated_sources += sync_generated_java_files
 gbjar.extra_jars = [
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..70caf842d33e52296d730f9016c4c50bf89b0838
GIT binary patch
literal 301
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpUtFP<)rAr-gY-rUP|*no%S!MVDl
ze6qW~$THV%UUVzLAmjq~PKD^VyirCj-yQ36ekP@@^k!gW`H_0y?LUpJ+oG=bzxH{{
zb*<{#y{Fm?E%nznMXm4SVB8Y7cllSIryL7juD$N?q2cdC_gsO7d=mkc2Lb*J9Ig`{
z+A2snwta8NV3EAXyht#mg1^HJEXa6Rm_hDfTR#&6i=Z7xgP_X~4F)%Xhl?2`7!Ucg
zC9rVVOE(BA{HbAZ`|;h0!PZHA-;rNE|9<zsxw5oHta!D~@~sOaQ@vOE);4YQns+el
x*Ttk?5A%L~top^=cg5YuYN?0=1LL)|=CA)`mdn@tv;g{_!PC{xWt~$(69E0Qd+`7O
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..608a80f47210fc07d464d5b672dbb2ebcf4cf2d3
GIT binary patch
literal 205
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJDV{ElAr-gYUfalfKtX`np>1L6
zGLg(py3Hva=f&?U%ADkw`G~u`!Jmt<Yo_Pq29`P8JKim?+O@orYo=Is2-^kQXO*5V
z-8-MXWKj@XAn-VbMM1E}lI=ss%Lpb1g#$B68T5Xz>M;ihYTRd(5L{5ho#67I$kyRM
z=a<<5|D|8?FH-JH`kAV-zwar((9g*_@kcp54(Q0Y);#-k?Askzp!*m+UHx3vIVCg!
E0H9e+EC2ui
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-v12/fxaccount_password_active.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+  <solid
+      android:color="@android:color/white" />
+  <stroke
+      android:width="@dimen/fxaccount_stroke_width"
+      android:color="@color/fxaccount_input_borderActive" />
+  <corners
+      android:radius="@dimen/fxaccount_corner_radius"
+      android:topLeftRadius="@dimen/fxaccount_corner_radius"
+      android:topRightRadius="0dp"
+      android:bottomLeftRadius="@dimen/fxaccount_corner_radius"
+      android:bottomRightRadius="0dp" />
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-v12/fxaccount_password_button_hide_active.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+  <solid
+      android:color="@color/fxaccount_password_hide_backgroundcolor" />
+  <stroke
+      android:width="@dimen/fxaccount_stroke_width"
+      android:color="@color/fxaccount_input_borderActive" />
+  <corners
+      android:radius="@dimen/fxaccount_corner_radius"
+      android:topLeftRadius="0dp"
+      android:topRightRadius="@dimen/fxaccount_corner_radius"
+      android:bottomLeftRadius="0dp"
+      android:bottomRightRadius="@dimen/fxaccount_corner_radius" />
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-v12/fxaccount_password_button_show_active.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+  <solid
+      android:color="@color/fxaccount_password_show_backgroundcolor" />
+  <stroke
+      android:width="@dimen/fxaccount_stroke_width"
+      android:color="@color/fxaccount_input_borderActive" />
+  <corners
+      android:radius="@dimen/fxaccount_corner_radius"
+      android:topLeftRadius="0dp"
+      android:topRightRadius="@dimen/fxaccount_corner_radius"
+      android:bottomLeftRadius="0dp"
+      android:bottomRightRadius="@dimen/fxaccount_corner_radius" />
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-v12/fxaccount_password_inactive.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+  <solid
+      android:color="@android:color/white" />
+  <stroke
+      android:width="@dimen/fxaccount_stroke_width"
+      android:color="@color/fxaccount_input_borderInactive" />
+  <corners
+      android:radius="@dimen/fxaccount_corner_radius"
+      android:topLeftRadius="@dimen/fxaccount_corner_radius"
+      android:topRightRadius="0dp"
+      android:bottomLeftRadius="@dimen/fxaccount_corner_radius"
+      android:bottomRightRadius="0dp" />
+</shape>
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..4b6f1f34fd46470288a7a999b0479f55ce51871c
GIT binary patch
literal 370
zc%17D@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV6^mfaSW-r_4c-5(IE!`m%zLO
zXLm3)3f~VF3@_UB?m#G?8pCz>+l}=XR)+mu#gJLPW6joor7Qvt42(dc;rY(`nz+fz
zI~!T&uXQZ;?t5~IG3}@Orb+833l#jEUmbn&acIl_Y4PexdshFE;t+W7WABTV9DDwp
zX=HrHB)pKN;ax7%7r|}Q<}=i`tbMBPAfK51lk<Z6ji@@9FiVKtPJ;$JCav>~zc^Od
zu)k1V@JVJtf5WL_nBW2%<`qgGfP!5IP8T<5F=^Q|GB|KHPJhl2$`Ut^xkRv{lJA21
zjHj#~e>ha?IMx2~sM&F-+HtAx<5Atmr=HiLlGmx4*QJ)%tuEK2BG>y=O0G{$u3w$c
yL#6M83ZJK?+VcfV7H{J7w$^(uuLk$|%>2U~$xb_eg>VDIlEKr}&t;ucLK6VOFpa(d
--- a/mobile/android/base/resources/drawable/fxaccount_password_active.xml
+++ b/mobile/android/base/resources/drawable/fxaccount_password_active.xml
@@ -5,15 +5,18 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
   <solid
       android:color="@android:color/white" />
   <stroke
       android:width="@dimen/fxaccount_stroke_width"
       android:color="@color/fxaccount_input_borderActive" />
+  <!-- On Android pre v12/3.0/Gingerbread, bottom left and bottom
+       right are swapped. These values correct this bug; the resources
+       that don't need correction are in res/drawable-v12. -->
   <corners
       android:radius="@dimen/fxaccount_corner_radius"
       android:topLeftRadius="@dimen/fxaccount_corner_radius"
       android:topRightRadius="0dp"
-      android:bottomLeftRadius="@dimen/fxaccount_corner_radius"
-      android:bottomRightRadius="0dp" />
+      android:bottomLeftRadius="0dp"
+      android:bottomRightRadius="@dimen/fxaccount_corner_radius" />
 </shape>
--- a/mobile/android/base/resources/drawable/fxaccount_password_button_hide_active.xml
+++ b/mobile/android/base/resources/drawable/fxaccount_password_button_hide_active.xml
@@ -5,15 +5,18 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
   <solid
       android:color="@color/fxaccount_password_hide_backgroundcolor" />
   <stroke
       android:width="@dimen/fxaccount_stroke_width"
       android:color="@color/fxaccount_input_borderActive" />
+  <!-- On Android pre v12/3.0/Gingerbread, bottom left and bottom
+       right are swapped. These values correct this bug; the resources
+       that don't need correction are in res/drawable-v12. -->
   <corners
       android:radius="@dimen/fxaccount_corner_radius"
       android:topLeftRadius="0dp"
       android:topRightRadius="@dimen/fxaccount_corner_radius"
-      android:bottomLeftRadius="0dp"
-      android:bottomRightRadius="@dimen/fxaccount_corner_radius" />
+      android:bottomRightRadius="0dp"
+      android:bottomLeftRadius="@dimen/fxaccount_corner_radius" />
 </shape>
--- a/mobile/android/base/resources/drawable/fxaccount_password_button_show_active.xml
+++ b/mobile/android/base/resources/drawable/fxaccount_password_button_show_active.xml
@@ -5,15 +5,18 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
   <solid
       android:color="@color/fxaccount_password_show_backgroundcolor" />
   <stroke
       android:width="@dimen/fxaccount_stroke_width"
       android:color="@color/fxaccount_input_borderActive" />
+  <!-- On Android pre v12/3.0/Gingerbread, bottom left and bottom
+       right are swapped. These values correct this bug; the resources
+       that don't need correction are in res/drawable-v12. -->
   <corners
       android:radius="@dimen/fxaccount_corner_radius"
       android:topLeftRadius="0dp"
       android:topRightRadius="@dimen/fxaccount_corner_radius"
-      android:bottomLeftRadius="0dp"
-      android:bottomRightRadius="@dimen/fxaccount_corner_radius" />
+      android:bottomRightRadius="0dp"
+      android:bottomLeftRadius="@dimen/fxaccount_corner_radius" />
 </shape>
--- a/mobile/android/base/resources/drawable/fxaccount_password_inactive.xml
+++ b/mobile/android/base/resources/drawable/fxaccount_password_inactive.xml
@@ -5,15 +5,18 @@
 
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="rectangle">
   <solid
       android:color="@android:color/white" />
   <stroke
       android:width="@dimen/fxaccount_stroke_width"
       android:color="@color/fxaccount_input_borderInactive" />
+  <!-- On Android pre v12/3.0/Gingerbread, bottom left and bottom
+       right are swapped. These values correct this bug; the resources
+       that don't need correction are in res/drawable-v12. -->
   <corners
       android:radius="@dimen/fxaccount_corner_radius"
       android:topLeftRadius="@dimen/fxaccount_corner_radius"
       android:topRightRadius="0dp"
-      android:bottomLeftRadius="@dimen/fxaccount_corner_radius"
-      android:bottomRightRadius="0dp" />
+      android:bottomLeftRadius="0dp"
+      android:bottomRightRadius="@dimen/fxaccount_corner_radius" />
 </shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toolbar_separator.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+
+    <solid android:color="#a6aeb4"/>
+    <size android:width="1dp" />
+
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toolbar_separator_pb.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+
+    <solid android:color="#222"/>
+    <size android:width="1dp" />
+
+</shape>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/toolbar_separator_selector.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Note that android:color="@color/toolbar_separator" (which uses a selector)
+     directly in the <shape> does not appear to work, so instead we select
+     between two shapes with different colors. -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:gecko="http://schemas.android.com/apk/res-auto">
+
+    <item gecko:state_private="true" android:drawable="@drawable/toolbar_separator_pb"/>
+    <item android:drawable="@drawable/toolbar_separator"/>
+
+</selector>
rename from mobile/android/base/resources/drawable/url_bar_right_edge.xml
rename to mobile/android/base/resources/drawable/url_bar_translating_edge.xml
--- a/mobile/android/base/resources/layout/browser_toolbar.xml
+++ b/mobile/android/base/resources/layout/browser_toolbar.xml
@@ -7,43 +7,54 @@
        xmlns:gecko="http://schemas.android.com/apk/res-auto">
 
     <ImageButton android:id="@+id/back"
                  style="@style/UrlBar.ImageButton.Unused"/>
 
     <ImageButton android:id="@+id/forward"
                  style="@style/UrlBar.ImageButton.Unused"/>
 
+    <!-- Note: any layout parameters setting the right edge of
+         this View should be matched in the url_bar_translating_edge.
+
+         Note 2: On devices where the editing mode cancel items are
+         wider than the tabs and similar buttons (e.g. hardware menu
+         button), the url bar will shrink, in which case the LayoutParams
+         of this View are changed dynamically. -->
     <ImageView android:id="@+id/url_bar_entry"
                style="@style/UrlBar.Button"
                android:layout_marginLeft="4dp"
                android:layout_marginRight="-19dp"
                android:layout_marginTop="5dp"
                android:layout_marginBottom="5dp"
                android:layout_centerVertical="true"
                android:layout_toLeftOf="@+id/tabs"
                android:paddingRight="4dp"
                android:duplicateParentState="true"
                android:clickable="false"
                android:focusable="false"
                android:src="@drawable/url_bar_entry"
                android:scaleType="fitXY"/>
 
-    <ImageView android:id="@+id/url_bar_right_edge"
+    <!-- A View that clips with url_bar_entry and translates
+         around it to animate shrinking or growing the url bar,
+         which occurs in the display/editing mode transitions. -->
+    <ImageView android:id="@+id/url_bar_translating_edge"
                style="@style/UrlBar.Button"
                android:layout_alignLeft="@id/url_bar_entry"
-               android:layout_alignRight="@id/url_bar_entry"
+               android:layout_toLeftOf="@+id/tabs"
                android:layout_alignTop="@id/url_bar_entry"
                android:layout_alignBottom="@id/url_bar_entry"
+               android:layout_marginRight="-19dp"
                android:paddingRight="4dp"
                android:duplicateParentState="true"
                android:clickable="false"
                android:focusable="false"
                android:visibility="invisible"
-               android:src="@drawable/url_bar_right_edge"
+               android:src="@drawable/url_bar_translating_edge"
                android:scaleType="fitXY"/>
 
     <LinearLayout android:id="@+id/menu_items"
                   style="@style/UrlBar.ImageButton.Unused"/>
 
     <org.mozilla.gecko.toolbar.ShapedButton android:id="@+id/menu"
                                             style="@style/UrlBar.ImageButton"
                                             android:layout_width="48dip"
@@ -81,18 +92,39 @@
                         style="@style/UrlBar.ImageButton.TabCount"
                         android:layout_width="24dip"
                         android:layout_height="24dip"
                         android:layout_marginLeft="40dip"
                         android:layout_marginRight="8dip"
                         android:layout_marginTop="12dip"
                         android:layout_alignRight="@id/tabs"/>
 
+    <!-- Note that the edit components are invisible so that the views
+         depending on their location can properly layout. -->
+    <ImageView android:id="@+id/edit_cancel"
+               style="@style/UrlBar.ImageButton.Icon"
+               android:layout_alignParentRight="true"
+               android:src="@drawable/close_edit_mode"
+               android:contentDescription="@string/edit_mode_cancel"
+               android:visibility="invisible"/>
+
+    <org.mozilla.gecko.widget.ThemedView android:id="@+id/edit_separator"
+                                         android:layout_toLeftOf="@id/edit_cancel"
+                                         android:layout_width="1dip"
+                                         android:layout_height="match_parent"
+                                         android:layout_marginTop="12dp"
+                                         android:layout_marginBottom="12dp"
+                                         android:layout_marginLeft="8dp"
+                                         android:layout_marginRight="2dp"
+                                         android:background="@drawable/toolbar_separator_selector"
+                                         android:visibility="invisible"/>
+
     <org.mozilla.gecko.toolbar.ToolbarEditLayout android:id="@+id/edit_layout"
                   style="@style/UrlBar.Button"
+                  android:layout_toLeftOf="@id/edit_separator"
                   android:layout_marginLeft="4dp"
                   android:layout_marginRight="4dp"
                   android:paddingLeft="8dp"
                   android:visibility="gone"/>
 
     <org.mozilla.gecko.toolbar.ToolbarDisplayLayout android:id="@+id/display_layout"
                   style="@style/UrlBar.Button"
                   android:layout_toLeftOf="@id/tabs"
--- a/mobile/android/base/resources/layout/fxaccount_email_password_view.xml
+++ b/mobile/android/base/resources/layout/fxaccount_email_password_view.xml
@@ -7,26 +7,27 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android" >
 
     <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical" >
 
-        <EditText
+        <AutoCompleteTextView
             android:id="@+id/email"
             style="@style/FxAccountEditItem"
             android:layout_marginBottom="10dp"
+            android:completionThreshold="2"
             android:ems="10"
             android:hint="@string/fxaccount_email_hint"
             android:inputType="textEmailAddress" >
 
             <requestFocus />
-        </EditText>
+        </AutoCompleteTextView>
 
         <LinearLayout
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:orientation="horizontal" >
 
             <EditText
                 android:id="@+id/password"
@@ -34,25 +35,51 @@
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
                 android:layout_weight="1"
                 android:background="@drawable/fxaccount_password_background"
                 android:ems="10"
                 android:hint="@string/fxaccount_password_hint"
                 android:inputType="textPassword" />
 
-            <Button
-                android:id="@+id/show_password"
-                style="@android:style/Widget.Button"
+            <!-- For the following, I beg forgiveness. The show/hide button is a
+                 toggle button; its text depends on its state. The text for each
+                 state could be a different length. We want to maintain the
+                 button's width regardless of its state. To achieve this, we
+                 size the actual button to its container, and include two
+                 invisible (but present for layout purposes) buttons, one of
+                 each state. The container wraps the larger of the two dummy
+                 buttons; the actual button sizes to the container; and we're
+                 happy. Be thankful there are not three buttons! -->
+            <FrameLayout
                 android:layout_width="wrap_content"
                 android:layout_height="fill_parent"
                 android:layout_weight="0"
-                android:background="@drawable/fxaccount_password_button_show_background"
-                android:minHeight="0dp"
-                android:padding="0dp"
-                android:text="@string/fxaccount_password_show"
-                android:textColor="@color/fxaccount_input_textColor"
-                android:textSize="20sp" >
-            </Button>
+                android:orientation="horizontal" >
+
+                <Button
+                    android:id="@+id/show_password"
+                    style="@style/FxAccountShowHidePasswordButton"
+                    android:layout_width="fill_parent"
+                    android:layout_height="fill_parent"
+                    android:text="@string/fxaccount_password_show" >
+                </Button>
+
+                <Button
+                    style="@style/FxAccountShowHidePasswordButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="fill_parent"
+                    android:text="@string/fxaccount_password_show"
+                    android:visibility="invisible" >
+                </Button>
+
+                <Button
+                    style="@style/FxAccountShowHidePasswordButton"
+                    android:layout_width="wrap_content"
+                    android:layout_height="fill_parent"
+                    android:text="@string/fxaccount_password_hide"
+                    android:visibility="invisible" >
+                </Button>
+            </FrameLayout>
         </LinearLayout>
     </LinearLayout>
 
-</merge>
\ No newline at end of file
+</merge>
--- a/mobile/android/base/resources/values-large-v11/fxaccount_styles.xml
+++ b/mobile/android/base/resources/values-large-v11/fxaccount_styles.xml
@@ -8,16 +8,25 @@
 <resources xmlns:android="http://schemas.android.com/apk/res/android">
 
     <style name="FxAccountMiddle">
         <item name="android:orientation">vertical</item>
         <item name="android:layout_width">500dp</item>
         <item name="android:minWidth">500dp</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_weight">1</item>
-        <item name="android:layout_gravity">center</item>
+        <!-- layout_gravity controls where the middle container goes
+             in its parent, and gravity controls where the container
+             places its internal content.  Usually, the middle
+             container is a LinearLayout and the container is a
+             ScrollView.  In this case, layout_gravity="center"
+             interacts badly with vertical scrolling.  What is needed
+             is layout_gravity="center_horizontal" and
+             gravity="center". -->
+        <item name="android:gravity">center</item>
+        <item name="android:layout_gravity">center_horizontal</item>
         <item name="android:paddingTop">25dp</item>
         <item name="android:paddingLeft">12dp</item>
         <item name="android:paddingRight">12dp</item>
         <item name="android:paddingBottom">15dp</item>
     </style>
 
-</resources>
\ No newline at end of file
+</resources>
--- a/mobile/android/base/resources/values/fxaccount_dimens.xml
+++ b/mobile/android/base/resources/values/fxaccount_dimens.xml
@@ -9,15 +9,20 @@
 
   <!-- The amount of horizontal padding that appears inside the email,
        password, etc input fields. -->
   <dimen name="fxaccount_input_padding_horizontal">20dp</dimen>
   <!-- And the amount of vertical padding that appears inside input
        fields. -->
   <dimen name="fxaccount_input_padding_vertical">10dp</dimen>
 
+  <!-- The amount of horizontal padding that appears around the show/hide
+       password button.  Vertical padding is provided by the adjacent input
+       field. -->
+  <dimen name="fxaccount_show_password_padding_horizontal">8dp</dimen>
+
   <!-- Preference fragment padding, bottom -->
   <dimen name="preference_fragment_padding_bottom">0dp</dimen>
   <!-- Preference fragment padding, sides -->
   <dimen name="preference_fragment_padding_side">16dp</dimen>
 
   <integer name="preference_fragment_scrollbarStyle">0x02000000</integer> <!-- outsideOverlay -->
 </resources>
--- a/mobile/android/base/resources/values/fxaccount_styles.xml
+++ b/mobile/android/base/resources/values/fxaccount_styles.xml
@@ -125,9 +125,19 @@
 
     <style name="FxAccountCheckBox">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_marginBottom">10dp</item>
         <item name="android:textColor">@drawable/fxaccount_checkbox_textcolor</item>
     </style>
 
+    <style name="FxAccountShowHidePasswordButton" parent="@android:style/Widget.Button">
+        <item name="android:background">@drawable/fxaccount_password_button_show_background</item>
+        <item name="android:minHeight">0dp</item>
+        <item name="android:minWidth">0dp</item>
+        <item name="android:paddingLeft">@dimen/fxaccount_show_password_padding_horizontal</item>
+        <item name="android:paddingRight">@dimen/fxaccount_show_password_padding_horizontal</item>
+        <item name="android:textColor">@color/fxaccount_input_textColor</item>
+        <item name="android:textSize">20sp</item>
+    </style>
+
 </resources>
\ No newline at end of file
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -232,16 +232,17 @@
   <string name="apps">&apps;</string>
   <string name="char_encoding">&char_encoding;</string>
   <string name="new_tab">&new_tab;</string>
   <string name="new_private_tab">&new_private_tab;</string>
   <string name="close_all_tabs">&close_all_tabs;</string>
   <string name="tabs_normal">&tabs_normal;</string>
   <string name="tabs_private">&tabs_private;</string>
   <string name="tabs_synced">&tabs_synced;</string>
+  <string name="edit_mode_cancel">&edit_mode_cancel;</string>
 
   <string name="site_settings_title">&site_settings_title3;</string>
   <string name="site_settings_cancel">&site_settings_cancel;</string>
   <string name="site_settings_clear">&site_settings_clear;</string>
   <string name="site_settings_no_settings">&site_settings_no_settings;</string>
 
   <string name="reading_list_added">&reading_list_added;</string>
   <string name="reading_list_removed">&reading_list_removed;</string>
--- a/mobile/android/base/sync/GlobalSession.java
+++ b/mobile/android/base/sync/GlobalSession.java
@@ -52,20 +52,19 @@ import org.mozilla.gecko.sync.stage.Form
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 import org.mozilla.gecko.sync.stage.UploadMetaGlobalStage;
 
 import android.content.Context;
-import android.content.SharedPreferences;
 import ch.boye.httpclientandroidlib.HttpResponse;
 
-public class GlobalSession implements PrefsSource, HttpResponseObserver {
+public class GlobalSession implements HttpResponseObserver {
   private static final String LOG_TAG = "GlobalSession";
 
   public static final long STORAGE_VERSION = 5;
 
   public SyncConfiguration config = null;
 
   protected Map<Stage, GlobalSyncStage> stages;
   public Stage currentState = Stage.idle;
@@ -272,24 +271,16 @@ public class GlobalSession implements Pr
       nextStage.execute(this);
     } catch (Exception ex) {
       Logger.warn(LOG_TAG, "Caught exception " + ex + " running stage " + next);
       this.abort(ex, "Uncaught exception in stage.");
       return;
     }
   }
 
-  /*
-   * PrefsSource methods.
-   */
-  @Override
-  public SharedPreferences getPrefs(String name, int mode) {
-    return this.getContext().getSharedPreferences(name, mode);
-  }
-
   public Context getContext() {
     return this.context;
   }
 
   /**
    * Begin a sync.
    * <p>
    * The caller is responsible for:
deleted file mode 100644
--- a/mobile/android/base/sync/PrefsSource.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.sync;
-
-import android.content.SharedPreferences;
-
-/**
- * Implement PrefsSource to allow other components to fetch a SharedPreferences
- * instance via a Context that you provide.
- *
- * This allows components to use SharedPreferences without being tightly
- * coupled to an Activity.
- *
- * @author rnewman
- *
- */
-public interface PrefsSource {
-  /**
-   * Return a SharedPreferences instance.
-   * @param name
-   *        A String, used to identify a preferences 'branch'. Must not be null.
-   * @param mode
-   *        A bitmask mode, as described in http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29.
-   * @return
-   *        A new or existing SharedPreferences instance.
-   */
-  public SharedPreferences getPrefs(String name, int mode);
-}
--- a/mobile/android/base/sync/SyncConfiguration.java
+++ b/mobile/android/base/sync/SyncConfiguration.java
@@ -256,24 +256,16 @@ public class SyncConfiguration {
   public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
 
   public static final String PREF_ACCOUNT_GUID = "account.guid";
   public static final String PREF_CLIENT_NAME = "account.clientName";
   public static final String PREF_NUM_CLIENTS = "account.numClients";
 
   private static final String API_VERSION = "1.5";
 
-  /**
-   * Create a new SyncConfiguration instance. Pass in a PrefsSource to
-   * provide access to preferences.
-   */
-  public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, String prefsPath, PrefsSource prefsSource) {
-    this(username, authHeaderProvider, prefsSource.getPrefs(prefsPath, Utils.SHARED_PREFERENCES_MODE));
-  }
-
   public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs) {
     this.username = username;
     this.authHeaderProvider = authHeaderProvider;
     this.prefs = prefs;
     this.loadFromPrefs(prefs);
   }
 
   public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) {
--- a/mobile/android/base/sync/net/HawkAuthHeaderProvider.java
+++ b/mobile/android/base/sync/net/HawkAuthHeaderProvider.java
@@ -13,16 +13,17 @@ import java.security.InvalidKeyException
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Locale;
 
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
 
 import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.Utils;
 
 import ch.boye.httpclientandroidlib.Header;
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpEntityEnclosingRequest;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
@@ -50,24 +51,30 @@ public class HawkAuthHeaderProvider impl
   protected final boolean includePayloadHash;
   protected final long skewSeconds;
 
   /**
    * Create a Hawk Authorization header provider.
    * <p>
    * Hawk specifies no mechanism by which a client receives an
    * identifier-and-key pair from the server.
+   * <p>
+   * Hawk requests can include a payload verification hash with requests that
+   * enclose an entity (PATCH, POST, and PUT requests).  <b>You should default
+   * to including the payload verification hash<b> unless you have a good reason
+   * not to -- the server can always ignore payload verification hashes provided
+   * by the client.
    *
    * @param id
    *          to name requests with.
    * @param key
    *          to sign request with.
    *
    * @param includePayloadHash
-   *          true if message integrity hash should be included in signed
+   *          true if payload verification hash should be included in signed
    *          request header. See <a href="https://github.com/hueniverse/hawk#payload-validation">https://github.com/hueniverse/hawk#payload-validation</a>.
    *
    * @param skewSeconds
    *          a number of seconds by which to skew the current time when
    *          computing a header.
    */
   public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash, long skewSeconds) {
     if (id == null) {
@@ -77,21 +84,16 @@ public class HawkAuthHeaderProvider impl
       throw new IllegalArgumentException("key must not be null");
     }
     this.id = id;
     this.key = key;
     this.includePayloadHash = includePayloadHash;
     this.skewSeconds = skewSeconds;
   }
 
-  public HawkAuthHeaderProvider(String id, byte[] key, boolean includePayloadHash) {
-    this(id, key, includePayloadHash, 0L);
-  }
-
-
   /**
    * @return the current time in milliseconds.
    */
   @SuppressWarnings("static-method")
   protected long now() {
     return System.currentTimeMillis();
   }
 
@@ -136,24 +138,19 @@ public class HawkAuthHeaderProvider impl
       throw new IllegalArgumentException("nonce must not be null.");
     }
     if (nonce.length() == 0) {
       throw new IllegalArgumentException("nonce must not be empty.");
     }
 
     String payloadHash = null;
     if (includePayloadHash) {
-      if (!(request instanceof HttpEntityEnclosingRequest)) {
-        throw new IllegalArgumentException("cannot specify payload for request without an entity");
-      }
-      HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
-      if (entity == null) {
-        throw new IllegalArgumentException("cannot specify payload for request with a null entity");
-      }
-      payloadHash = Base64.encodeBase64String(getPayloadHash(entity));
+      payloadHash = getPayloadHashString(request);
+    } else {
+      Logger.debug(LOG_TAG, "Configured to not include payload hash for this request.");
     }
 
     String app = null;
     String dlg = null;
     String requestString = getRequestString(request, "header", timestamp, nonce, payloadHash, extra, app, dlg);
     String macString = getSignature(requestString.getBytes("UTF-8"), this.key);
 
     StringBuilder sb = new StringBuilder();
@@ -179,16 +176,48 @@ public class HawkAuthHeaderProvider impl
     sb.append("mac=\"");
     sb.append(macString);
     sb.append("\"");
 
     return new BasicHeader("Authorization", sb.toString());
   }
 
   /**
+   * Get the payload verification hash for the given request, if possible.
+   * <p>
+   * Returns null if the request does not enclose an entity (is not an HTTP
+   * PATCH, POST, or PUT). Throws if the payload verification hash cannot be
+   * computed.
+   *
+   * @param request
+   *          to compute hash for.
+   * @return verification hash, or null if the request does not enclose an entity.
+   * @throws IllegalArgumentException if the request does not enclose a valid non-null entity.
+   * @throws UnsupportedEncodingException
+   * @throws NoSuchAlgorithmException
+   * @throws IOException
+   */
+  protected static String getPayloadHashString(HttpRequestBase request)
+      throws UnsupportedEncodingException, NoSuchAlgorithmException, IOException, IllegalArgumentException {
+    final boolean shouldComputePayloadHash = request instanceof HttpEntityEnclosingRequest;
+    if (!shouldComputePayloadHash) {
+      Logger.debug(LOG_TAG, "Not computing payload verification hash for non-enclosing request.");
+      return null;
+    }
+    if (!(request instanceof HttpEntityEnclosingRequest)) {
+      throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request without an entity");
+    }
+    final HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+    if (entity == null) {
+      throw new IllegalArgumentException("Cannot compute payload verification hash for enclosing request with a null entity");
+    }
+    return Base64.encodeBase64String(getPayloadHash(entity));
+  }
+
+  /**
    * Escape the user-provided extra string for the ext="" header attribute.
    * <p>
    * Hawk escapes the header ext="" attribute differently than it does the extra
    * line in the normalized request string.
    * <p>
    * See <a href="https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385">https://github.com/hueniverse/hawk/blob/871cc597973110900467bd3dfb84a3c892f678fb/lib/browser.js#L385</a>.
    *
    * @param extra to escape.
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -29,16 +29,17 @@ import org.mozilla.gecko.toolbar.Toolbar
 import org.mozilla.gecko.toolbar.ToolbarDisplayLayout.UpdateFlags;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.widget.ThemedImageButton;
 import org.mozilla.gecko.widget.ThemedImageView;
 import org.mozilla.gecko.widget.ThemedRelativeLayout;
+import org.mozilla.gecko.widget.ThemedView;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.StateListDrawable;
 import android.os.Build;
 import android.text.TextUtils;
 import android.util.AttributeSet;
@@ -52,16 +53,17 @@ import android.view.View;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.Interpolator;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.PopupWindow;
+import android.widget.RelativeLayout;
 
 /**
 * {@code BrowserToolbar} is single entry point for users of the toolbar
 * subsystem i.e. this should be the only import outside the 'toolbar'
 * package.
 *
 * {@code BrowserToolbar} serves at the single event bus for all
 * sub-components in the toolbar. It tracks tab events and gecko messages
@@ -110,149 +112,159 @@ public class BrowserToolbar extends Them
         DISPLAY
     }
 
     enum ForwardButtonAnimation {
         SHOW,
         HIDE
     }
 
-    private ToolbarDisplayLayout mUrlDisplayLayout;
-    private ToolbarEditLayout mUrlEditLayout;
-    private View mUrlBarEntry;
-    private ImageView mUrlBarRightEdge;
-    private boolean mSwitchingTabs;
-    private ShapedButton mTabs;
-    private ImageButton mBack;
-    private ImageButton mForward;
+    private ToolbarDisplayLayout urlDisplayLayout;
+    private ToolbarEditLayout urlEditLayout;
+    private View urlBarEntry;
+    private RelativeLayout.LayoutParams urlBarEntryDefaultLayoutParams;
+    private RelativeLayout.LayoutParams urlBarEntryShrunkenLayoutParams;
+    private ImageView urlBarTranslatingEdge;
+    private boolean isSwitchingTabs;
+    private ShapedButton tabsButton;
+    private ImageButton backButton;
+    private ImageButton forwardButton;
 
-    private ToolbarProgressView mProgressBar;
-    private TabCounter mTabsCounter;
-    private ThemedImageButton mMenu;
-    private ThemedImageView mMenuIcon;
-    private LinearLayout mActionItemBar;
-    private MenuPopup mMenuPopup;
-    private List<View> mFocusOrder;
+    private ToolbarProgressView progressBar;
+    private TabCounter tabsCounter;
+    private ThemedImageButton menuButton;
+    private ThemedImageView menuIcon;
+    private LinearLayout actionItemBar;
+    private MenuPopup menuPopup;
+    private List<View> focusOrder;
 
-    private OnActivateListener mActivateListener;
-    private OnCommitListener mCommitListener;
-    private OnDismissListener mDismissListener;
-    private OnFilterListener mFilterListener;
-    private OnFocusChangeListener mFocusChangeListener;
-    private OnStartEditingListener mStartEditingListener;
-    private OnStopEditingListener mStopEditingListener;
+    private final ThemedView editSeparator;
+    private final View editCancel;
+
+    private boolean shouldShrinkURLBar = false;
+
+    private OnActivateListener activateListener;
+    private OnFocusChangeListener focusChangeListener;
+    private OnStartEditingListener startEditingListener;
+    private OnStopEditingListener stopEditingListener;
 
-    final private BrowserApp mActivity;
-    private boolean mHasSoftMenuButton;
+    final private BrowserApp activity;
+    private boolean hasSoftMenuButton;
 
-    private UIMode mUIMode;
-    private boolean mAnimatingEntry;
+    private UIMode uiMode;
+    private boolean isAnimatingEntry;
 
-    private int mUrlBarViewOffset;
-    private int mDefaultForwardMargin;
+    private int urlBarViewOffset;
+    private int defaultForwardMargin;
 
-    private static final Interpolator sButtonsInterpolator = new AccelerateInterpolator();
+    private static final Interpolator buttonsInterpolator = new AccelerateInterpolator();
 
     private static final int FORWARD_ANIMATION_DURATION = 450;
 
-    private final LightweightTheme mTheme;
+    private final LightweightTheme theme;
 
     public BrowserToolbar(Context context) {
         this(context, null);
     }
 
     public BrowserToolbar(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
+        theme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
 
         // BrowserToolbar is attached to BrowserApp only.
-        mActivity = (BrowserApp) context;
+        activity = (BrowserApp) context;
 
         // Inflate the content.
         LayoutInflater.from(context).inflate(R.layout.browser_toolbar, this);
 
         Tabs.registerOnTabsChangedListener(this);
-        mSwitchingTabs = true;
-        mAnimatingEntry = false;
+        isSwitchingTabs = true;
+        isAnimatingEntry = false;
 
         registerEventListener("Reader:Click");
         registerEventListener("Reader:LongClick");
 
-        mAnimatingEntry = false;
+        final Resources res = getResources();
+        urlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left);
+        defaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
+        urlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
+        urlBarEntry = findViewById(R.id.url_bar_entry);
+        urlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
 
-        final Resources res = getResources();
-        mUrlBarViewOffset = res.getDimensionPixelSize(R.dimen.url_bar_offset_left);
-        mDefaultForwardMargin = res.getDimensionPixelSize(R.dimen.forward_default_offset);
-        mUrlDisplayLayout = (ToolbarDisplayLayout) findViewById(R.id.display_layout);
-        mUrlBarEntry = findViewById(R.id.url_bar_entry);
-        mUrlEditLayout = (ToolbarEditLayout) findViewById(R.id.edit_layout);
+        urlBarEntryDefaultLayoutParams = (RelativeLayout.LayoutParams) urlBarEntry.getLayoutParams();
+        urlBarEntryShrunkenLayoutParams = new RelativeLayout.LayoutParams(urlBarEntryDefaultLayoutParams);
+        urlBarEntryShrunkenLayoutParams.addRule(RelativeLayout.ALIGN_RIGHT, R.id.edit_layout);
+        urlBarEntryShrunkenLayoutParams.rightMargin = 0;
 
-        // This will clip the right edge's image at 60% of its width
-        mUrlBarRightEdge = (ImageView) findViewById(R.id.url_bar_right_edge);
-        if (mUrlBarRightEdge != null) {
-            mUrlBarRightEdge.getDrawable().setLevel(6000);
+        // This will clip the translating edge's image at 60% of its width
+        urlBarTranslatingEdge = (ImageView) findViewById(R.id.url_bar_translating_edge);
+        if (urlBarTranslatingEdge != null) {
+            urlBarTranslatingEdge.getDrawable().setLevel(6000);
         }
 
-        mTabs = (ShapedButton) findViewById(R.id.tabs);
-        mTabsCounter = (TabCounter) findViewById(R.id.tabs_counter);
+        tabsButton = (ShapedButton) findViewById(R.id.tabs);
+        tabsCounter = (TabCounter) findViewById(R.id.tabs_counter);
         if (Build.VERSION.SDK_INT >= 11) {
-            mTabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+            tabsCounter.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
         }
 
-        mBack = (ImageButton) findViewById(R.id.back);
-        setButtonEnabled(mBack, false);
-        mForward = (ImageButton) findViewById(R.id.forward);
-        setButtonEnabled(mForward, false);
+        backButton = (ImageButton) findViewById(R.id.back);
+        setButtonEnabled(backButton, false);
+        forwardButton = (ImageButton) findViewById(R.id.forward);
+        setButtonEnabled(forwardButton, false);
 
-        mMenu = (ThemedImageButton) findViewById(R.id.menu);
-        mMenuIcon = (ThemedImageView) findViewById(R.id.menu_icon);
-        mActionItemBar = (LinearLayout) findViewById(R.id.menu_items);
-        mHasSoftMenuButton = !HardwareUtils.hasMenuButton();
+        menuButton = (ThemedImageButton) findViewById(R.id.menu);
+        menuIcon = (ThemedImageView) findViewById(R.id.menu_icon);
+        actionItemBar = (LinearLayout) findViewById(R.id.menu_items);
+        hasSoftMenuButton = !HardwareUtils.hasMenuButton();
+
+        editSeparator = (ThemedView) findViewById(R.id.edit_separator);
+        editCancel = findViewById(R.id.edit_cancel);
 
         // We use different layouts on phones and tablets, so adjust the focus
         // order appropriately.
-        mFocusOrder = new ArrayList<View>();
+        focusOrder = new ArrayList<View>();
         if (HardwareUtils.isTablet()) {
-            mFocusOrder.addAll(Arrays.asList(mTabs, mBack, mForward, this));
-            mFocusOrder.addAll(mUrlDisplayLayout.getFocusOrder());
-            mFocusOrder.addAll(Arrays.asList(mActionItemBar, mMenu));
+            focusOrder.addAll(Arrays.asList(tabsButton, backButton, forwardButton, this));
+            focusOrder.addAll(urlDisplayLayout.getFocusOrder());
+            focusOrder.addAll(Arrays.asList(actionItemBar, menuButton));
         } else {
-            mFocusOrder.add(this);
-            mFocusOrder.addAll(mUrlDisplayLayout.getFocusOrder());
-            mFocusOrder.addAll(Arrays.asList(mTabs, mMenu));
+            focusOrder.add(this);
+            focusOrder.addAll(urlDisplayLayout.getFocusOrder());
+            focusOrder.addAll(Arrays.asList(tabsButton, menuButton));
         }
 
         setUIMode(UIMode.DISPLAY);
     }
 
     @Override
     public void onAttachedToWindow() {
         super.onAttachedToWindow();
 
         setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
-                if (mActivateListener != null) {
-                    mActivateListener.onActivate();
+                if (activateListener != null) {
+                    activateListener.onActivate();
                 }
             }
         });
 
         setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() {
             @Override
             public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
                 // We don't the context menu while editing
                 if (isEditing()) {
                     return;
                 }
 
                 // NOTE: Use MenuUtils.safeSetVisible because some actions might
                 // be on the Page menu
 
-                MenuInflater inflater = mActivity.getMenuInflater();
+                MenuInflater inflater = activity.getMenuInflater();
                 inflater.inflate(R.menu.titlebar_contextmenu, menu);
 
                 String clipboard = Clipboard.getText();
                 if (TextUtils.isEmpty(clipboard)) {
                     menu.findItem(R.id.pasteandgo).setVisible(false);
                     menu.findItem(R.id.paste).setVisible(false);
                 }
 
@@ -275,117 +287,126 @@ public class BrowserToolbar extends Them
                     MenuUtils.safeSetVisible(menu, R.id.subscribe, false);
                     MenuUtils.safeSetVisible(menu, R.id.add_search_engine, false);
                 }
 
                 MenuUtils.safeSetVisible(menu, R.id.share, !GeckoProfile.get(getContext()).inGuestMode());
             }
         });
 
-        mUrlDisplayLayout.setOnStopListener(new OnStopListener() {
+        urlDisplayLayout.setOnStopListener(new OnStopListener() {
             @Override
             public Tab onStop() {
                 final Tab tab = Tabs.getInstance().getSelectedTab();
                 if (tab != null) {
                     tab.doStop();
                     return tab;
                 }
 
                 return null;
             }
         });
 
-        mUrlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() {
+        urlDisplayLayout.setOnTitleChangeListener(new OnTitleChangeListener() {
             @Override
             public void onTitleChange(CharSequence title) {
                 final String contentDescription;
                 if (title != null) {
                     contentDescription = title.toString();
                 } else {
-                    contentDescription = mActivity.getString(R.string.url_bar_default_text);
+                    contentDescription = activity.getString(R.string.url_bar_default_text);
                 }
 
                 // The title and content description should
                 // always be sync.
                 setContentDescription(contentDescription);
             }
         });
 
-        mUrlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+        urlEditLayout.setOnFocusChangeListener(new View.OnFocusChangeListener() {
             @Override
             public void onFocusChange(View v, boolean hasFocus) {
                 setSelected(hasFocus);
-                if (mFocusChangeListener != null) {
-                    mFocusChangeListener.onFocusChange(v, hasFocus);
+                if (focusChangeListener != null) {
+                    focusChangeListener.onFocusChange(v, hasFocus);
                 }
             }
         });
 
-        mTabs.setOnClickListener(new Button.OnClickListener() {
+        tabsButton.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View v) {
                 toggleTabs();
             }
         });
-        mTabs.setImageLevel(0);
+        tabsButton.setImageLevel(0);
 
-        mBack.setOnClickListener(new Button.OnClickListener() {
+        backButton.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View view) {
                 Tabs.getInstance().getSelectedTab().doBack();
             }
         });
-        mBack.setOnLongClickListener(new Button.OnLongClickListener() {
+        backButton.setOnLongClickListener(new Button.OnLongClickListener() {
             @Override
             public boolean onLongClick(View view) {
                 return Tabs.getInstance().getSelectedTab().showBackHistory();
             }
         });
 
-        mForward.setOnClickListener(new Button.OnClickListener() {
+        forwardButton.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View view) {
                 Tabs.getInstance().getSelectedTab().doForward();
             }
         });
-        mForward.setOnLongClickListener(new Button.OnLongClickListener() {
+        forwardButton.setOnLongClickListener(new Button.OnLongClickListener() {
             @Override
             public boolean onLongClick(View view) {
                 return Tabs.getInstance().getSelectedTab().showForwardHistory();
             }
         });
 
-        if (mHasSoftMenuButton) {
-            mMenu.setVisibility(View.VISIBLE);
-            mMenuIcon.setVisibility(View.VISIBLE);
+        if (editCancel != null) {
+            editCancel.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    cancelEdit();
+                }
+            });
+        }
 
-            mMenu.setOnClickListener(new Button.OnClickListener() {
+        if (hasSoftMenuButton) {
+            menuButton.setVisibility(View.VISIBLE);
+            menuIcon.setVisibility(View.VISIBLE);
+
+            menuButton.setOnClickListener(new Button.OnClickListener() {
                 @Override
                 public void onClick(View view) {
-                    mActivity.openOptionsMenu();
+                    activity.openOptionsMenu();
                 }
             });
         }
     }
 
     public void setProgressBar(ToolbarProgressView progressBar) {
-        mProgressBar = progressBar;
+        this.progressBar = progressBar;
     }
 
     public void refresh() {
-        mUrlDisplayLayout.dismissSiteIdentityPopup();
+        urlDisplayLayout.dismissSiteIdentityPopup();
     }
 
     public boolean onBackPressed() {
         if (isEditing()) {
             stopEditing();
             return true;
         }
 
-        return mUrlDisplayLayout.dismissSiteIdentityPopup();
+        return urlDisplayLayout.dismissSiteIdentityPopup();
     }
 
     public boolean onKey(int keyCode, KeyEvent event) {
         if (event.getAction() != KeyEvent.ACTION_DOWN) {
             return false;
         }
 
         // Galaxy Note sends key events for the stylus that are outside of the
@@ -403,17 +424,17 @@ public class BrowserToolbar extends Them
             keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
             keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
             keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||
             keyCode == KeyEvent.KEYCODE_DEL ||
             keyCode == KeyEvent.KEYCODE_VOLUME_UP ||
             keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
             return false;
         } else if (isEditing()) {
-            return mUrlEditLayout.onKey(keyCode, event);
+            return urlEditLayout.onKey(keyCode, event);
         }
 
         return false;
     }
 
     @Override
     public boolean onTouchEvent(MotionEvent event) {
         // If the motion event has occured below the toolbar (due to the scroll
@@ -430,17 +451,17 @@ public class BrowserToolbar extends Them
         super.onSizeChanged(w, h, oldw, oldh);
 
         if (h != oldh) {
             // Post this to happen outside of onSizeChanged, as this may cause
             // a layout change and relayouts within a layout change don't work.
             post(new Runnable() {
                 @Override
                 public void run() {
-                    mActivity.refreshToolbarHeight();
+                    activity.refreshToolbarHeight();
                 }
             });
         }
     }
 
     @Override
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         Log.d(LOGTAG, "onTabChanged: " + msg);
@@ -454,19 +475,19 @@ public class BrowserToolbar extends Them
         switch (msg) {
             case ADDED:
             case CLOSED:
                 updateTabCount(tabs.getDisplayCount());
                 break;
             case RESTORED:
                 // TabCount fixup after OOM
             case SELECTED:
-                mUrlDisplayLayout.dismissSiteIdentityPopup();
+                urlDisplayLayout.dismissSiteIdentityPopup();
                 updateTabCount(tabs.getDisplayCount());
-                mSwitchingTabs = true;
+                isSwitchingTabs = true;
                 break;
         }
 
         if (tabs.isSelectedTab(tab)) {
             final EnumSet<UpdateFlags> flags = EnumSet.noneOf(UpdateFlags.class);
 
             // Progress-related handling
             switch (msg) {
@@ -474,18 +495,18 @@ public class BrowserToolbar extends Them
                     updateProgressVisibility(tab, Tab.LOAD_PROGRESS_INIT);
                     // Fall through.
                 case ADDED:
                 case LOCATION_CHANGE:
                 case LOAD_ERROR:
                 case LOADED:
                 case STOP:
                     flags.add(UpdateFlags.PROGRESS);
-                    if (mProgressBar.getVisibility() == View.VISIBLE) {
-                        mProgressBar.animateProgress(tab.getLoadProgress());
+                    if (progressBar.getVisibility() == View.VISIBLE) {
+                        progressBar.animateProgress(tab.getLoadProgress());
                     }
                     break;
 
                 case SELECTED:
                     flags.add(UpdateFlags.PROGRESS);
                     updateProgressVisibility();
                     break;
             }
@@ -537,167 +558,175 @@ public class BrowserToolbar extends Them
                 updateDisplayLayout(tab, flags);
             }
         }
 
         switch (msg) {
             case SELECTED:
             case LOAD_ERROR:
             case LOCATION_CHANGE:
-                mSwitchingTabs = false;
+                isSwitchingTabs = false;
         }
     }
 
     private void updateProgressVisibility() {
         final Tab selectedTab = Tabs.getInstance().getSelectedTab();
         updateProgressVisibility(selectedTab, selectedTab.getLoadProgress());
     }
 
     private void updateProgressVisibility(Tab selectedTab, int progress) {
         if (!isEditing() && selectedTab.getState() == Tab.STATE_LOADING) {
-            mProgressBar.setProgress(progress);
-            mProgressBar.setVisibility(View.VISIBLE);
+            progressBar.setProgress(progress);
+            progressBar.setVisibility(View.VISIBLE);
         } else {
-            mProgressBar.setVisibility(View.GONE);
+            progressBar.setVisibility(View.GONE);
         }
     }
 
     public boolean isVisible() {
         return ViewHelper.getTranslationY(this) == 0;
     }
 
     @Override
     public void setNextFocusDownId(int nextId) {
         super.setNextFocusDownId(nextId);
-        mTabs.setNextFocusDownId(nextId);
-        mBack.setNextFocusDownId(nextId);
-        mForward.setNextFocusDownId(nextId);
-        mUrlDisplayLayout.setNextFocusDownId(nextId);
-        mMenu.setNextFocusDownId(nextId);
+        tabsButton.setNextFocusDownId(nextId);
+        backButton.setNextFocusDownId(nextId);
+        forwardButton.setNextFocusDownId(nextId);
+        urlDisplayLayout.setNextFocusDownId(nextId);
+        menuButton.setNextFocusDownId(nextId);
     }
 
     private int getUrlBarEntryTranslation() {
-        return getWidth() - mUrlBarEntry.getRight();
+        if (editSeparator == null) {
+            // We are on tablet, and there is no animation so return a translation of 0.
+            return 0;
+        }
+
+        // We would ideally use the right-most point of the edit layout instead of the
+        // edit separator and its margin, but it is not inflated when this code initially runs.
+        final LayoutParams lp = (LayoutParams) editSeparator.getLayoutParams();
+        return editSeparator.getLeft() - lp.leftMargin - urlBarEntry.getRight();
     }
 
     private int getUrlBarCurveTranslation() {
-        return getWidth() - mTabs.getLeft();
+        return getWidth() - tabsButton.getLeft();
     }
 
     private boolean canDoBack(Tab tab) {
         return (tab.canDoBack() && !isEditing());
     }
 
     private boolean canDoForward(Tab tab) {
         return (tab.canDoForward() && !isEditing());
     }
 
     private void addTab() {
-        mActivity.addTab();
+        activity.addTab();
     }
 
     private void toggleTabs() {
-        if (mActivity.areTabsShown()) {
-            if (mActivity.hasTabsSideBar())
-                mActivity.hideTabs();
+        if (activity.areTabsShown()) {
+            if (activity.hasTabsSideBar())
+                activity.hideTabs();
         } else {
             // hide the virtual keyboard
             InputMethodManager imm =
-                    (InputMethodManager) mActivity.getSystemService(Context.INPUT_METHOD_SERVICE);
-            imm.hideSoftInputFromWindow(mTabs.getWindowToken(), 0);
+                    (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+            imm.hideSoftInputFromWindow(tabsButton.getWindowToken(), 0);
 
             Tab tab = Tabs.getInstance().getSelectedTab();
             if (tab != null) {
                 if (!tab.isPrivate())
-                    mActivity.showNormalTabs();
+                    activity.showNormalTabs();
                 else
-                    mActivity.showPrivateTabs();
+                    activity.showPrivateTabs();
             }
         }
     }
 
     private void updateTabCountAndAnimate(int count) {
         // Don't animate if the toolbar is hidden.
         if (!isVisible()) {
             updateTabCount(count);
             return;
         }
 
         // If toolbar is in edit mode on a phone, this means the entry is expanded
         // and the tabs button is translated offscreen. Don't trigger tabs counter
         // updates until the tabs button is back on screen.
         // See stopEditing()
         if (!isEditing() || HardwareUtils.isTablet()) {
-            mTabsCounter.setCount(count);
+            tabsCounter.setCount(count);
 
-            mTabs.setContentDescription((count > 1) ?
-                                        mActivity.getString(R.string.num_tabs, count) :
-                                        mActivity.getString(R.string.one_tab));
+            tabsButton.setContentDescription((count > 1) ?
+                                             activity.getString(R.string.num_tabs, count) :
+                                             activity.getString(R.string.one_tab));
         }
     }
 
     private void updateTabCount(int count) {
         // If toolbar is in edit mode on a phone, this means the entry is expanded
         // and the tabs button is translated offscreen. Don't trigger tabs counter
         // updates until the tabs button is back on screen.
         // See stopEditing()
         if (isEditing() && !HardwareUtils.isTablet()) {
             return;
         }
 
         // Set TabCounter based on visibility
-        if (isVisible() && ViewHelper.getAlpha(mTabsCounter) != 0 && !isEditing()) {
-            mTabsCounter.setCountWithAnimation(count);
+        if (isVisible() && ViewHelper.getAlpha(tabsCounter) != 0 && !isEditing()) {
+            tabsCounter.setCountWithAnimation(count);
         } else {
-            mTabsCounter.setCount(count);
+            tabsCounter.setCount(count);
         }
 
         // Update A11y information
-        mTabs.setContentDescription((count > 1) ?
-                                    mActivity.getString(R.string.num_tabs, count) :
-                                    mActivity.getString(R.string.one_tab));
+        tabsButton.setContentDescription((count > 1) ?
+                                         activity.getString(R.string.num_tabs, count) :
+                                         activity.getString(R.string.one_tab));
     }
 
     private void updateDisplayLayout(Tab tab, EnumSet<UpdateFlags> flags) {
-        if (mSwitchingTabs) {
+        if (isSwitchingTabs) {
             flags.add(UpdateFlags.DISABLE_ANIMATIONS);
         }
 
-        mUrlDisplayLayout.updateFromTab(tab, flags);
+        urlDisplayLayout.updateFromTab(tab, flags);
 
         if (flags.contains(UpdateFlags.TITLE)) {
             if (!isEditing()) {
-                mUrlEditLayout.setText(tab.getURL());
+                urlEditLayout.setText(tab.getURL());
             }
         }
 
         if (flags.contains(UpdateFlags.PROGRESS)) {
             updateFocusOrder();
         }
     }
 
     private void updateFocusOrder() {
         View prevView = null;
 
         // If the element that has focus becomes disabled or invisible, focus
         // is given to the URL bar.
         boolean needsNewFocus = false;
 
-        for (View view : mFocusOrder) {
+        for (View view : focusOrder) {
             if (view.getVisibility() != View.VISIBLE || !view.isEnabled()) {
                 if (view.hasFocus()) {
                     needsNewFocus = true;
                 }
                 continue;
             }
 
-            if (view == mActionItemBar) {
-                final int childCount = mActionItemBar.getChildCount();
+            if (view == actionItemBar) {
+                final int childCount = actionItemBar.getChildCount();
                 for (int child = 0; child < childCount; child++) {
-                    View childView = mActionItemBar.getChildAt(child);
+                    View childView = actionItemBar.getChildAt(child);
                     if (prevView != null) {
                         childView.setNextFocusLeftId(prevView.getId());
                         prevView.setNextFocusRightId(childView.getId());
                     }
                     prevView = childView;
                 }
             } else {
                 if (prevView != null) {
@@ -713,99 +742,96 @@ public class BrowserToolbar extends Them
         }
     }
 
     public void onEditSuggestion(String suggestion) {
         if (!isEditing()) {
             return;
         }
 
-        mUrlEditLayout.onEditSuggestion(suggestion);
+        urlEditLayout.onEditSuggestion(suggestion);
     }
 
     public void setTitle(CharSequence title) {
-        mUrlDisplayLayout.setTitle(title);
+        urlDisplayLayout.setTitle(title);
     }
 
     public void prepareTabsAnimation(PropertyAnimator animator, boolean tabsAreShown) {
         if (!tabsAreShown) {
             PropertyAnimator buttonsAnimator =
-                    new PropertyAnimator(animator.getDuration(), sButtonsInterpolator);
+                    new PropertyAnimator(animator.getDuration(), buttonsInterpolator);
 
-            buttonsAnimator.attach(mTabsCounter,
+            buttonsAnimator.attach(tabsCounter,
                                    PropertyAnimator.Property.ALPHA,
                                    1.0f);
 
-            if (mHasSoftMenuButton && !HardwareUtils.isTablet()) {
-                buttonsAnimator.attach(mMenuIcon,
+            if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
+                buttonsAnimator.attach(menuIcon,
                                        PropertyAnimator.Property.ALPHA,
                                        1.0f);
             }
 
             buttonsAnimator.start();
 
             return;
         }
 
-        ViewHelper.setAlpha(mTabsCounter, 0.0f);
+        ViewHelper.setAlpha(tabsCounter, 0.0f);
 
-        if (mHasSoftMenuButton && !HardwareUtils.isTablet()) {
-            ViewHelper.setAlpha(mMenuIcon, 0.0f);
+        if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
+            ViewHelper.setAlpha(menuIcon, 0.0f);
         }
     }
 
     public void finishTabsAnimation(boolean tabsAreShown) {
         if (tabsAreShown) {
             return;
         }
 
         PropertyAnimator animator = new PropertyAnimator(150);
 
-        animator.attach(mTabsCounter,
+        animator.attach(tabsCounter,
                         PropertyAnimator.Property.ALPHA,
                         1.0f);
 
-        if (mHasSoftMenuButton && !HardwareUtils.isTablet()) {
-            animator.attach(mMenuIcon,
+        if (hasSoftMenuButton && !HardwareUtils.isTablet()) {
+            animator.attach(menuIcon,
                             PropertyAnimator.Property.ALPHA,
                             1.0f);
         }
 
         animator.start();
     }
 
     public void setOnActivateListener(OnActivateListener listener) {
-        mActivateListener = listener;
+        activateListener = listener;
     }
 
     public void setOnCommitListener(OnCommitListener listener) {
-        mCommitListener = listener;
-        mUrlEditLayout.setOnCommitListener(listener);
+        urlEditLayout.setOnCommitListener(listener);
     }
 
     public void setOnDismissListener(OnDismissListener listener) {
-        mDismissListener = listener;
-        mUrlEditLayout.setOnDismissListener(listener);
+        urlEditLayout.setOnDismissListener(listener);
     }
 
     public void setOnFilterListener(OnFilterListener listener) {
-        mFilterListener = listener;
-        mUrlEditLayout.setOnFilterListener(listener);
+        urlEditLayout.setOnFilterListener(listener);
     }
 
     public void setOnFocusChangeListener(OnFocusChangeListener listener) {
-        mFocusChangeListener = listener;
+        focusChangeListener = listener;
     }
 
     public void setOnStartEditingListener(OnStartEditingListener listener) {
-        mStartEditingListener = listener;
+        startEditingListener = listener;
     }
 
     public void setOnStopEditingListener(OnStopEditingListener listener) {
-        mStopEditingListener = listener;
+        stopEditingListener = listener;
     }
 
     private void showUrlEditLayout() {
         setUrlEditLayoutVisibility(true, null);
     }
 
     private void showUrlEditLayout(PropertyAnimator animator) {
         setUrlEditLayoutVisibility(true, animator);
@@ -815,52 +841,66 @@ public class BrowserToolbar extends Them
         setUrlEditLayoutVisibility(false, null);
     }
 
     private void hideUrlEditLayout(PropertyAnimator animator) {
         setUrlEditLayoutVisibility(false, animator);
     }
 
     private void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
-        final View viewToShow = (showEditLayout ? mUrlEditLayout : mUrlDisplayLayout);
-        final View viewToHide = (showEditLayout ? mUrlDisplayLayout : mUrlEditLayout);
+        urlEditLayout.prepareAnimation(showEditLayout, animator);
 
-        if (showEditLayout) {
-            mUrlEditLayout.prepareShowAnimation(animator);
-        }
+        final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout);
+        final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout);
 
         if (animator == null) {
             viewToHide.setVisibility(View.GONE);
             viewToShow.setVisibility(View.VISIBLE);
+
+            final int cancelVisibility = (showEditLayout ? View.VISIBLE : View.INVISIBLE);
+            setCancelVisibility(cancelVisibility);
             return;
         }
 
         ViewHelper.setAlpha(viewToShow, 0.0f);
         animator.attach(viewToShow,
                         PropertyAnimator.Property.ALPHA,
                         1.0f);
 
         animator.attach(viewToHide,
                         PropertyAnimator.Property.ALPHA,
                         0.0f);
 
         animator.addPropertyAnimationListener(new PropertyAnimationListener() {
             @Override
             public void onPropertyAnimationStart() {
                 viewToShow.setVisibility(View.VISIBLE);
+                if (!showEditLayout) {
+                    setCancelVisibility(View.INVISIBLE);
+                }
             }
 
             @Override
             public void onPropertyAnimationEnd() {
                 viewToHide.setVisibility(View.GONE);
                 ViewHelper.setAlpha(viewToHide, 1.0f);
+                if (showEditLayout) {
+                    setCancelVisibility(View.VISIBLE);
+                }
             }
         });
     }
 
+    private void setCancelVisibility(final int visibility) {
+        if (editSeparator != null && editCancel != null) {
+            editSeparator.setVisibility(visibility);
+            editCancel.setVisibility(visibility);
+        }
+    }
+
     /**
      * Disables and dims all toolbar elements which are not
      * related to editing mode.
      */
     private void updateChildrenForEditing() {
         // This is for the tablet UI only
         if (!HardwareUtils.isTablet()) {
             return;
@@ -869,154 +909,165 @@ public class BrowserToolbar extends Them
         // Disable toolbar elemens while in editing mode
         final boolean enabled = !isEditing();
 
         // This alpha value has to be in sync with the one used
         // in setButtonEnabled().
         final float alpha = (enabled ? 1.0f : 0.24f);
 
         if (!enabled) {
-            mTabsCounter.onEnterEditingMode();
+            tabsCounter.onEnterEditingMode();
         }
 
-        mTabs.setEnabled(enabled);
-        ViewHelper.setAlpha(mTabsCounter, alpha);
-        mMenu.setEnabled(enabled);
-        ViewHelper.setAlpha(mMenuIcon, alpha);
+        tabsButton.setEnabled(enabled);
+        ViewHelper.setAlpha(tabsCounter, alpha);
+        menuButton.setEnabled(enabled);
+        ViewHelper.setAlpha(menuIcon, alpha);
 
-        final int actionItemsCount = mActionItemBar.getChildCount();
+        final int actionItemsCount = actionItemBar.getChildCount();
         for (int i = 0; i < actionItemsCount; i++) {
-            mActionItemBar.getChildAt(i).setEnabled(enabled);
+            actionItemBar.getChildAt(i).setEnabled(enabled);
         }
-        ViewHelper.setAlpha(mActionItemBar, alpha);
+        ViewHelper.setAlpha(actionItemBar, alpha);
 
         final Tab tab = Tabs.getInstance().getSelectedTab();
         if (tab != null) {
-            setButtonEnabled(mBack, canDoBack(tab));
-            setButtonEnabled(mForward, canDoForward(tab));
+            setButtonEnabled(backButton, canDoBack(tab));
+            setButtonEnabled(forwardButton, canDoForward(tab));
 
             // Once the editing mode is finished, we have to ensure that the
             // forward button slides away if necessary. This is because we might
             // have only disabled it (without hiding it) when the toolbar entered
             // editing mode.
             if (!isEditing()) {
                 animateForwardButton(canDoForward(tab) ?
                                      ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
             }
         }
     }
 
     private void setUIMode(final UIMode uiMode) {
-        mUIMode = uiMode;
-        mUrlEditLayout.setEnabled(uiMode == UIMode.EDIT);
+        this.uiMode = uiMode;
+        urlEditLayout.setEnabled(uiMode == UIMode.EDIT);
     }
 
     /**
      * Returns whether or not the URL bar is in editing mode (url bar is expanded, hiding the new
      * tab button). Note that selection state is independent of editing mode.
      */
     public boolean isEditing() {
-        return (mUIMode == UIMode.EDIT);
+        return (uiMode == UIMode.EDIT);
     }
 
     public void startEditing(String url, PropertyAnimator animator) {
         if (isEditing()) {
             return;
         }
 
-        mUrlEditLayout.setText(url != null ? url : "");
+        urlEditLayout.setText(url != null ? url : "");
 
         setUIMode(UIMode.EDIT);
         updateChildrenForEditing();
 
         updateProgressVisibility();
 
-        if (mStartEditingListener != null) {
-            mStartEditingListener.onStartEditing();
+        if (startEditingListener != null) {
+            startEditingListener.onStartEditing();
         }
 
-        if (mUrlBarRightEdge != null) {
-            mUrlBarRightEdge.setVisibility(View.VISIBLE);
+        final int curveTranslation = getUrlBarCurveTranslation();
+        final int entryTranslation = getUrlBarEntryTranslation();
+        shouldShrinkURLBar = (entryTranslation < 0);
+
+        if (urlBarTranslatingEdge != null) {
+            urlBarTranslatingEdge.setVisibility(View.VISIBLE);
+            if (shouldShrinkURLBar) {
+                urlBarEntry.setLayoutParams(urlBarEntryShrunkenLayoutParams);
+            }
         }
 
-        final int entryTranslation = getUrlBarEntryTranslation();
-        final int curveTranslation = getUrlBarCurveTranslation();
-
-        // This animation doesn't make much sense in a sidebar UI
-        if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) {
+        if (Build.VERSION.SDK_INT < 11) {
+            showEditingWithoutAnimation(entryTranslation, curveTranslation);
+        } else if (HardwareUtils.isTablet()) {
+            // No animation.
             showUrlEditLayout();
-
-            if (!HardwareUtils.isTablet()) {
-                if (mUrlBarRightEdge != null) {
-                    ViewHelper.setTranslationX(mUrlBarRightEdge, entryTranslation);
-                }
+        } else {
+            showEditingWithPhoneAnimation(animator, entryTranslation, curveTranslation);
+        }
+    }
 
-                ViewHelper.setTranslationX(mTabs, curveTranslation);
-                ViewHelper.setTranslationX(mTabsCounter, curveTranslation);
-                ViewHelper.setTranslationX(mActionItemBar, curveTranslation);
+    private void showEditingWithoutAnimation(final int entryTranslation,
+            final int curveTranslation) {
+        showUrlEditLayout();
 
-                if (mHasSoftMenuButton) {
-                    ViewHelper.setTranslationX(mMenu, curveTranslation);
-                    ViewHelper.setTranslationX(mMenuIcon, curveTranslation);
-                }
-            }
-
-            return;
+        if (urlBarTranslatingEdge != null) {
+            ViewHelper.setTranslationX(urlBarTranslatingEdge, entryTranslation);
         }
 
-        if (mAnimatingEntry)
+        ViewHelper.setTranslationX(tabsButton, curveTranslation);
+        ViewHelper.setTranslationX(tabsCounter, curveTranslation);
+        ViewHelper.setTranslationX(actionItemBar, curveTranslation);
+
+        if (hasSoftMenuButton) {
+            ViewHelper.setTranslationX(menuButton, curveTranslation);
+            ViewHelper.setTranslationX(menuIcon, curveTranslation);
+        }
+    }
+
+    private void showEditingWithPhoneAnimation(final PropertyAnimator animator,
+            final int entryTranslation, final int curveTranslation) {
+        if (isAnimatingEntry)
             return;
 
         // Highlight the toolbar from the start of the animation.
         setSelected(true);
 
-        mUrlDisplayLayout.prepareStartEditingAnimation();
+        urlDisplayLayout.prepareStartEditingAnimation();
 
-        // Slide the right side elements of the toolbar
-
-        if (mUrlBarRightEdge != null) {
-            animator.attach(mUrlBarRightEdge,
+        // Slide toolbar elements.
+        if (urlBarTranslatingEdge != null) {
+            animator.attach(urlBarTranslatingEdge,
                             PropertyAnimator.Property.TRANSLATION_X,
                             entryTranslation);
         }
 
-        animator.attach(mTabs,
+        animator.attach(tabsButton,
                         PropertyAnimator.Property.TRANSLATION_X,
                         curveTranslation);
-        animator.attach(mTabsCounter,
+        animator.attach(tabsCounter,
                         PropertyAnimator.Property.TRANSLATION_X,
                         curveTranslation);
-        animator.attach(mActionItemBar,
+        animator.attach(actionItemBar,
                         PropertyAnimator.Property.TRANSLATION_X,
                         curveTranslation);
 
-        if (mHasSoftMenuButton) {
-            animator.attach(mMenu,
+        if (hasSoftMenuButton) {
+            animator.attach(menuButton,
                             PropertyAnimator.Property.TRANSLATION_X,
                             curveTranslation);
 
-            animator.attach(mMenuIcon,
+            animator.attach(menuIcon,
                             PropertyAnimator.Property.TRANSLATION_X,
                             curveTranslation);
         }
 
         showUrlEditLayout(animator);
 
         animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
             @Override
             public void onPropertyAnimationStart() {
             }
 
             @Override
             public void onPropertyAnimationEnd() {
-                mAnimatingEntry = false;
+                isAnimatingEntry = false;
             }
         });
 
-        mAnimatingEntry = true;
+        isAnimatingEntry = true;
     }
 
     /**
      * Exits edit mode without updating the toolbar title.
      *
      * @return the url that was entered
      */
     public String cancelEdit() {
@@ -1032,309 +1083,329 @@ public class BrowserToolbar extends Them
         final String url = stopEditing();
         if (!TextUtils.isEmpty(url)) {
             setTitle(url);
         }
         return url;
     }
 
     private String stopEditing() {
-        final String url = mUrlEditLayout.getText();
+        final String url = urlEditLayout.getText();
         if (!isEditing()) {
             return url;
         }
         setUIMode(UIMode.DISPLAY);
 
         updateChildrenForEditing();
 
-        if (mStopEditingListener != null) {
-            mStopEditingListener.onStopEditing();
+        if (stopEditingListener != null) {
+            stopEditingListener.onStopEditing();
         }
 
         updateProgressVisibility();
 
-        if (HardwareUtils.isTablet() || Build.VERSION.SDK_INT < 11) {
+        if (Build.VERSION.SDK_INT < 11) {
+            stopEditingWithoutAnimation();
+        } else if (HardwareUtils.isTablet()) {
+            // No animation.
             hideUrlEditLayout();
-
-            if (!HardwareUtils.isTablet()) {
-                updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
-
-                if (mUrlBarRightEdge != null) {
-                    ViewHelper.setTranslationX(mUrlBarRightEdge, 0);
-                }
-
-                ViewHelper.setTranslationX(mTabs, 0);
-                ViewHelper.setTranslationX(mTabsCounter, 0);
-                ViewHelper.setTranslationX(mActionItemBar, 0);
-
-                if (mHasSoftMenuButton) {
-                    ViewHelper.setTranslationX(mMenu, 0);
-                    ViewHelper.setTranslationX(mMenuIcon, 0);
-                }
-            }
-
-            return url;
+        } else {
+            stopEditingWithPhoneAnimation();
         }
 
+        return url;
+    }
+
+    private void stopEditingWithoutAnimation() {
+        hideUrlEditLayout();
+
+        updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
+
+        if (urlBarTranslatingEdge != null) {
+            urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
+            ViewHelper.setTranslationX(urlBarTranslatingEdge, 0);
+            if (shouldShrinkURLBar) {
+                urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
+            }
+        }
+
+        ViewHelper.setTranslationX(tabsButton, 0);
+        ViewHelper.setTranslationX(tabsCounter, 0);
+        ViewHelper.setTranslationX(actionItemBar, 0);
+
+        if (hasSoftMenuButton) {
+            ViewHelper.setTranslationX(menuButton, 0);
+            ViewHelper.setTranslationX(menuIcon, 0);
+        }
+    }
+
+    private void stopEditingWithPhoneAnimation() {
         final PropertyAnimator contentAnimator = new PropertyAnimator(250);
         contentAnimator.setUseHardwareLayer(false);
 
-        // Shrink the urlbar entry back to its original size
-
-        if (mUrlBarRightEdge != null) {
-            contentAnimator.attach(mUrlBarRightEdge,
+        // Slide the toolbar back to its original size.
+        if (urlBarTranslatingEdge != null) {
+            contentAnimator.attach(urlBarTranslatingEdge,
                                    PropertyAnimator.Property.TRANSLATION_X,
                                    0);
         }
 
-        contentAnimator.attach(mTabs,
+        contentAnimator.attach(tabsButton,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
-        contentAnimator.attach(mTabsCounter,
+        contentAnimator.attach(tabsCounter,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
-        contentAnimator.attach(mActionItemBar,
+        contentAnimator.attach(actionItemBar,
                                PropertyAnimator.Property.TRANSLATION_X,
                                0);
 
-        if (mHasSoftMenuButton) {
-            contentAnimator.attach(mMenu,
+        if (hasSoftMenuButton) {
+            contentAnimator.attach(menuButton,
                                    PropertyAnimator.Property.TRANSLATION_X,
                                    0);
 
-            contentAnimator.attach(mMenuIcon,
+            contentAnimator.attach(menuIcon,
                                    PropertyAnimator.Property.TRANSLATION_X,
                                    0);
         }
 
         hideUrlEditLayout(contentAnimator);
 
         contentAnimator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
             @Override
             public void onPropertyAnimationStart() {
             }
 
             @Override
             public void onPropertyAnimationEnd() {
-                if (mUrlBarRightEdge != null) {
-                    mUrlBarRightEdge.setVisibility(View.INVISIBLE);
+                if (urlBarTranslatingEdge != null) {
+                    urlBarTranslatingEdge.setVisibility(View.INVISIBLE);
+                    if (shouldShrinkURLBar) {
+                        urlBarEntry.setLayoutParams(urlBarEntryDefaultLayoutParams);
+                    }
                 }
 
                 PropertyAnimator buttonsAnimator = new PropertyAnimator(300);
-                mUrlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator);
+                urlDisplayLayout.prepareStopEditingAnimation(buttonsAnimator);
                 buttonsAnimator.start();
 
-                mAnimatingEntry = false;
+                isAnimatingEntry = false;
 
                 // Trigger animation to update the tabs counter once the
                 // tabs button is back on screen.
                 updateTabCountAndAnimate(Tabs.getInstance().getDisplayCount());
             }
         });
 
-        mAnimatingEntry = true;
+        isAnimatingEntry = true;
         contentAnimator.start();
-
-        return url;
     }
 
     public void setButtonEnabled(ImageButton button, boolean enabled) {
         final Drawable drawable = button.getDrawable();
         if (drawable != null) {
             // This alpha value has to be in sync with the one used
             // in updateChildrenForEditing().
             drawable.setAlpha(enabled ? 255 : 61);
         }
 
         button.setEnabled(enabled);
     }
 
     public void updateBackButton(Tab tab) {
-        setButtonEnabled(mBack, canDoBack(tab));
+        setButtonEnabled(backButton, canDoBack(tab));
     }
 
     private void animateForwardButton(final ForwardButtonAnimation animation) {
         // If the forward button is not visible, we must be
         // in the phone UI.
-        if (mForward.getVisibility() != View.VISIBLE) {
+        if (forwardButton.getVisibility() != View.VISIBLE) {
             return;
         }
 
         final boolean showing = (animation == ForwardButtonAnimation.SHOW);
 
         // if the forward button's margin is non-zero, this means it has already
         // been animated to be visible¸ and vice-versa.
-        MarginLayoutParams fwdParams = (MarginLayoutParams) mForward.getLayoutParams();
-        if ((fwdParams.leftMargin > mDefaultForwardMargin && showing) ||
-            (fwdParams.leftMargin == mDefaultForwardMargin && !showing)) {
+        MarginLayoutParams fwdParams = (MarginLayoutParams) forwardButton.getLayoutParams();
+        if ((fwdParams.leftMargin > defaultForwardMargin && showing) ||
+            (fwdParams.leftMargin == defaultForwardMargin && !showing)) {
             return;
         }
 
         // We want the forward button to show immediately when switching tabs
         final PropertyAnimator forwardAnim =
-                new PropertyAnimator(mSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION);
-        final int width = mForward.getWidth() / 2;
+                new PropertyAnimator(isSwitchingTabs ? 10 : FORWARD_ANIMATION_DURATION);
+        final int width = forwardButton.getWidth() / 2;
 
         forwardAnim.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
             @Override
             public void onPropertyAnimationStart() {
                 if (!showing) {
                     // Set the margin before the transition when hiding the forward button. We
                     // have to do this so that the favicon isn't clipped during the transition
                     MarginLayoutParams layoutParams =
-                        (MarginLayoutParams) mUrlDisplayLayout.getLayoutParams();
+                        (MarginLayoutParams) urlDisplayLayout.getLayoutParams();
                     layoutParams.leftMargin = 0;
 
                     // Do the same on the URL edit container
-                    layoutParams = (MarginLayoutParams) mUrlEditLayout.getLayoutParams();
+                    layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
                     layoutParams.leftMargin = 0;
 
                     requestLayout();
                     // Note, we already translated the favicon, site security, and text field
                     // in prepareForwardAnimation, so they should appear to have not moved at
                     // all at this point.
                 }
             }
 
             @Override
             public void onPropertyAnimationEnd() {
                 if (showing) {
                     MarginLayoutParams layoutParams =
-                        (MarginLayoutParams) mUrlDisplayLayout.getLayoutParams();
-                    layoutParams.leftMargin = mUrlBarViewOffset;
+                        (MarginLayoutParams) urlDisplayLayout.getLayoutParams();
+                    layoutParams.leftMargin = urlBarViewOffset;
 
-                    layoutParams = (MarginLayoutParams) mUrlEditLayout.getLayoutParams();
-                    layoutParams.leftMargin = mUrlBarViewOffset;
+                    layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
+                    layoutParams.leftMargin = urlBarViewOffset;
                 }
 
-                mUrlDisplayLayout.finishForwardAnimation();
+                urlDisplayLayout.finishForwardAnimation();
 
-                MarginLayoutParams layoutParams = (MarginLayoutParams) mForward.getLayoutParams();
-                layoutParams.leftMargin = mDefaultForwardMargin + (showing ? width : 0);
-                ViewHelper.setTranslationX(mForward, 0);
+                MarginLayoutParams layoutParams = (MarginLayoutParams) forwardButton.getLayoutParams();
+                layoutParams.leftMargin = defaultForwardMargin + (showing ? width : 0);
+                ViewHelper.setTranslationX(forwardButton, 0);
 
                 requestLayout();
             }
         });
 
         prepareForwardAnimation(forwardAnim, animation, width);
         forwardAnim.start();
     }
 
     public void updateForwardButton(Tab tab) {
         final boolean enabled = canDoForward(tab);
-        if (mForward.isEnabled() == enabled)
+        if (forwardButton.isEnabled() == enabled)
             return;
 
         // Save the state on the forward button so that we can skip animations
         // when there's nothing to change
-        setButtonEnabled(mForward, enabled);
+        setButtonEnabled(forwardButton, enabled);
         animateForwardButton(enabled ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
     }
 
     private void prepareForwardAnimation(PropertyAnimator anim, ForwardButtonAnimation animation, int width) {
         if (animation == ForwardButtonAnimation.HIDE) {
-            anim.attach(mForward,
+            anim.attach(forwardButton,
                       PropertyAnimator.Property.TRANSLATION_X,
                       -width);
-            anim.attach(mForward,
+            anim.attach(forwardButton,
                       PropertyAnimator.Property.ALPHA,
                       0);
 
         } else {
-            anim.attach(mForward,
+            anim.attach(forwardButton,
                       PropertyAnimator.Property.TRANSLATION_X,
                       width);
-            anim.attach(mForward,
+            anim.attach(forwardButton,
                       PropertyAnimator.Property.ALPHA,
                       1);
         }
 
-        mUrlDisplayLayout.prepareForwardAnimation(anim, animation, width);
+        urlDisplayLayout.prepareForwardAnimation(anim, animation, width);
     }
 
     @Override
     public boolean addActionItem(View actionItem) {
-        mActionItemBar.addView(actionItem);
+        actionItemBar.addView(actionItem);
         return true;
     }
 
     @Override
     public void removeActionItem(View actionItem) {
-        mActionItemBar.removeView(actionItem);
+        actionItemBar.removeView(actionItem);
     }
 
     @Override
     public void setPrivateMode(boolean isPrivate) {
         super.setPrivateMode(isPrivate);
 
-        mTabs.setPrivateMode(isPrivate);
-        mMenu.setPrivateMode(isPrivate);
-        mMenuIcon.setPrivateMode(isPrivate);
-        mUrlEditLayout.setPrivateMode(isPrivate);
-
-        if (mBack instanceof BackButton) {
-            ((BackButton) mBack).setPrivateMode(isPrivate);
+        tabsButton.setPrivateMode(isPrivate);
+        menuButton.setPrivateMode(isPrivate);
+        menuIcon.setPrivateMode(isPrivate);
+        urlEditLayout.setPrivateMode(isPrivate);
+        if (editSeparator != null) {
+            editSeparator.setPrivateMode(isPrivate);
         }
 
-        if (mForward instanceof ForwardButton) {
-            ((ForwardButton) mForward).setPrivateMode(isPrivate);
+        if (backButton instanceof BackButton) {
+            ((BackButton) backButton).setPrivateMode(isPrivate);
+        }
+
+        if (forwardButton instanceof ForwardButton) {
+            ((ForwardButton) forwardButton).setPrivateMode(isPrivate);
         }
     }
 
     public void show() {
         setVisibility(View.VISIBLE);
     }
 
     public void hide() {
         setVisibility(View.GONE);
     }
 
     public View getDoorHangerAnchor() {
-        return mUrlDisplayLayout.getDoorHangerAnchor();
+        return urlDisplayLayout.getDoorHangerAnchor();
     }
 
     public void onDestroy() {
         Tabs.unregisterOnTabsChangedListener(this);
 
         unregisterEventListener("Reader:Click");
         unregisterEventListener("Reader:LongClick");
     }
 
     public boolean openOptionsMenu() {
-        if (!mHasSoftMenuButton)
+        if (!hasSoftMenuButton) {
             return false;
+        }
 
         // Initialize the popup.
-        if (mMenuPopup == null) {
-            View panel = mActivity.getMenuPanel();
-            mMenuPopup = new MenuPopup(mActivity);
-            mMenuPopup.setPanelView(panel);
+        if (menuPopup == null) {
+            View panel = activity.getMenuPanel();
+            menuPopup = new MenuPopup(activity);
+            menuPopup.setPanelView(panel);
 
-            mMenuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
+            menuPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
                 @Override
                 public void onDismiss() {
-                    mActivity.onOptionsMenuClosed(null);
+                    activity.onOptionsMenuClosed(null);
                 }
             });
         }
 
         GeckoAppShell.getGeckoInterface().invalidateOptionsMenu();
-        if (!mMenuPopup.isShowing())
-            mMenuPopup.showAsDropDown(mMenu);
+        if (!menuPopup.isShowing()) {
+            menuPopup.showAsDropDown(menuButton);
+        }
 
         return true;
     }
 
     public boolean closeOptionsMenu() {
-        if (!mHasSoftMenuButton)
+        if (!hasSoftMenuButton) {
             return false;
+        }
 
-        if (mMenuPopup != null && mMenuPopup.isShowing())
-            mMenuPopup.dismiss();
+        if (menuPopup != null && menuPopup.isShowing()) {
+            menuPopup.dismiss();
+        }
 
         return true;
     }
 
     private void registerEventListener(String event) {
         GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
     }
 
@@ -1355,17 +1426,17 @@ public class BrowserToolbar extends Them
             if (tab != null) {
                 tab.addToReadingList();
             }
         }
     }
 
     @Override
     public void onLightweightThemeChanged() {
-        Drawable drawable = mTheme.getDrawable(this);
+        Drawable drawable = theme.getDrawable(this);
         if (drawable == null)
             return;
 
         StateListDrawable stateList = new StateListDrawable();
         stateList.addState(PRIVATE_STATE_SET, getColorDrawable(R.color.background_private));
         stateList.addState(EMPTY_STATE_SET, drawable);
 
         setBackgroundDrawable(stateList);
--- a/mobile/android/base/toolbar/ToolbarEditLayout.java
+++ b/mobile/android/base/toolbar/ToolbarEditLayout.java
@@ -120,17 +120,25 @@ public class ToolbarEditLayout extends T
     }
 
     private void showSoftInput() {
         InputMethodManager imm =
                (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
         imm.showSoftInput(mEditText, InputMethodManager.SHOW_IMPLICIT);
     }
 
-    void prepareShowAnimation(PropertyAnimator animator) {
+    void prepareAnimation(final boolean showing, final PropertyAnimator animator) {
+        if (showing) {
+            prepareShowAnimation(animator);
+        } else {
+            prepareHideAnimation(animator);
+        }
+    }
+
+    private void prepareShowAnimation(final PropertyAnimator animator) {
         if (animator == null) {
             mEditText.requestFocus();
             showSoftInput();
             return;
         }
 
         animator.addPropertyAnimationListener(new PropertyAnimationListener() {
             @Override
@@ -142,16 +150,35 @@ public class ToolbarEditLayout extends T
             @Override
             public void onPropertyAnimationEnd() {
                 ViewHelper.setAlpha(mGo, 1.0f);
                 showSoftInput();
             }
         });
     }
 
+    private void prepareHideAnimation(final PropertyAnimator animator) {
+        if (animator == null) {
+            return;
+        }
+
+        animator.addPropertyAnimationListener(new PropertyAnimationListener() {
+            @Override
+            public void onPropertyAnimationStart() {
+                ViewHelper.setAlpha(mGo, 0.0f);
+            }
+
+            @Override
+            public void onPropertyAnimationEnd() {
+                // The enclosing view is invisible, so unhide the go button.
+                ViewHelper.setAlpha(mGo, 1.0f);
+            }
+        });
+    }
+
     void setOnCommitListener(OnCommitListener listener) {
         mCommitListener = listener;
         mEditText.setOnCommitListener(listener);
     }
 
     void setOnDismissListener(OnDismissListener listener) {
         mEditText.setOnDismissListener(listener);
     }
--- a/mobile/android/base/widget/ThemedEditText.java.in
+++ b/mobile/android/base/widget/ThemedEditText.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE EditText
+#define VIEW_NAME_SUFFIX EditText
+#define BASE_TYPE android.widget.EditText
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedImageButton.java.in
+++ b/mobile/android/base/widget/ThemedImageButton.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE ImageButton
+#define VIEW_NAME_SUFFIX ImageButton
+#define BASE_TYPE android.widget.ImageButton
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedImageView.java.in
+++ b/mobile/android/base/widget/ThemedImageView.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE ImageView
+#define VIEW_NAME_SUFFIX ImageView
+#define BASE_TYPE android.widget.ImageView
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedLinearLayout.java.in
+++ b/mobile/android/base/widget/ThemedLinearLayout.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE LinearLayout
+#define VIEW_NAME_SUFFIX LinearLayout
+#define BASE_TYPE android.widget.LinearLayout
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedRelativeLayout.java.in
+++ b/mobile/android/base/widget/ThemedRelativeLayout.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE RelativeLayout
+#define VIEW_NAME_SUFFIX RelativeLayout
+#define BASE_TYPE android.widget.RelativeLayout
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedTextSwitcher.java.in
+++ b/mobile/android/base/widget/ThemedTextSwitcher.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE TextSwitcher
+#define VIEW_NAME_SUFFIX TextSwitcher
+#define BASE_TYPE android.widget.TextSwitcher
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedTextView.java.in
+++ b/mobile/android/base/widget/ThemedTextView.java.in
@@ -1,3 +1,4 @@
 #filter substitution
-#define VIEWTYPE TextView
+#define VIEW_NAME_SUFFIX TextView
+#define BASE_TYPE android.widget.TextView
 #include ThemedView.java.frag
--- a/mobile/android/base/widget/ThemedView.java.frag
+++ b/mobile/android/base/widget/ThemedView.java.frag
@@ -7,36 +7,35 @@ package org.mozilla.gecko.widget;
 import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.LightweightTheme;
 import org.mozilla.gecko.R;
 
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.graphics.drawable.ColorDrawable;
 import android.util.AttributeSet;
-import android.widget.@VIEWTYPE@;
 
-public class Themed@VIEWTYPE@ extends @VIEWTYPE@
-                              implements LightweightTheme.OnChangeListener {
+public class Themed@VIEW_NAME_SUFFIX@ extends @BASE_TYPE@
+                                     implements LightweightTheme.OnChangeListener {
     private final LightweightTheme mTheme;
 
     private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
     private static final int[] STATE_LIGHT = { R.attr.state_light };
     private static final int[] STATE_DARK = { R.attr.state_dark };
 
     protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
     protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
     protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
 
     private boolean mIsPrivate = false;
     private boolean mIsLight = false;
     private boolean mIsDark = false;
     private boolean mAutoUpdateTheme = true;
 
-    public Themed@VIEWTYPE@(Context context, AttributeSet attrs) {
+    public Themed@VIEW_NAME_SUFFIX@(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
         mAutoUpdateTheme = a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
         a.recycle();
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/widget/ThemedView.java.in
@@ -0,0 +1,4 @@
+#filter substitution
+#define VIEW_NAME_SUFFIX View
+#define BASE_TYPE android.view.View
+#include ThemedView.java.frag
--- a/mobile/android/chrome/content/dbg-browser-actors.js
+++ b/mobile/android/chrome/content/dbg-browser-actors.js
@@ -18,16 +18,17 @@
  *
  * * @param aConnection DebuggerServerConnection
  *        The conection to the client.
  */
 function createRootActor(aConnection)
 {
   let parameters = {
     tabList: new MobileTabList(aConnection),
+    addonList: new BrowserAddonList(aConnection),
     globalActorFactories: DebuggerServer.globalActorFactories,
     onShutdown: sendShutdownEvent
   };
   return new RootActor(aConnection, parameters);
 }
 
 /**
  * A live list of BrowserTabActors representing the current browser tabs,
--- a/mobile/android/tests/background/junit3/src/sync/TestSyncConfiguration.java
+++ b/mobile/android/tests/background/junit3/src/sync/TestSyncConfiguration.java
@@ -4,28 +4,23 @@
 package org.mozilla.gecko.background.sync;
 
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
-import org.mozilla.gecko.sync.PrefsSource;
 import org.mozilla.gecko.sync.SyncConfiguration;
 
 import android.content.SharedPreferences;
 
-public class TestSyncConfiguration extends AndroidSyncTestCase implements PrefsSource {
+public class TestSyncConfiguration extends AndroidSyncTestCase {
   public static final String TEST_PREFS_NAME = "test";
 
-  /*
-   * PrefsSource methods.
-   */
-  @Override
   public SharedPreferences getPrefs(String name, int mode) {
     return this.getApplicationContext().getSharedPreferences(name, mode);
   }
 
   /**
    * Ensure that declined engines persist through prefs.
    */
   public void testDeclinedEngineNames() {
@@ -140,11 +135,11 @@ public class TestSyncConfiguration exten
     SyncConfiguration config = null;
     config = newSyncConfiguration();
     config.loadFromPrefs(prefs);
     // Forms should not be selected if history is not present.
     assertTrue(config.userSelectedEngines.isEmpty());
   }
 
   protected SyncConfiguration newSyncConfiguration() {
-    return new SyncConfiguration(null, null, TEST_PREFS_NAME, this);
+    return new SyncConfiguration(null, null, getPrefs(TEST_PREFS_NAME, 0));
   }
 }
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -50,16 +50,19 @@ user_pref("toolkit.telemetry.notifiedOpt
 // Existing tests assume there is no font size inflation.
 user_pref("font.size.inflation.emPerLine", 0);
 user_pref("font.size.inflation.minTwips", 0);
 
 // AddonManager tests require that the experiments feature be enabled.
 user_pref("experiments.enabled", true);
 user_pref("experiments.supported", true);
 user_pref("experiments.logging.level", "Trace");
+// Point the manifest at something local so we don't risk it hitting production
+// data and installing experiments that may vary over time.
+user_pref("experiments.manifest.uri", "http://%(server)s/experiments-dummy/manifest");
 
 // Only load extensions from the application and user profile
 // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
 user_pref("extensions.enabledScopes", 5);
 // Disable metadata caching for installed add-ons by default
 user_pref("extensions.getAddons.cache.enabled", false);
 // Disable intalling any distribution add-ons
 user_pref("extensions.installDistroAddons", false);
--- a/toolkit/devtools/DevToolsUtils.js
+++ b/toolkit/devtools/DevToolsUtils.js
@@ -3,21 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /* General utilities used throughout devtools. */
 
 // hasChrome is provided as a global by the loader. It is true if we are running
 // on the main thread, and false if we are running on a worker thread.
-if (hasChrome) {
-  var { Ci, Cu } = require("chrome");
-  var Services = require("Services");
-  var setTimeout = Cu.import("resource://gre/modules/Timer.jsm", {}).setTimeout;
-}
+var { Ci, Cu } = require("chrome");
+var Services = require("Services");
+var setTimeout = Cu.import("resource://gre/modules/Timer.jsm", {}).setTimeout;
 
 /**
  * Turn the error |aError| into a string, without fail.
  */
 exports.safeErrorString = function safeErrorString(aError) {
   try {
     let errorString = aError.toString();
     if (typeof errorString == "string") {
@@ -299,8 +297,24 @@ exports.isSafeJSObject = function isSafe
 
   let principal = Services.scriptSecurityManager.getObjectPrincipal(aObj);
   if (Services.scriptSecurityManager.isSystemPrincipal(principal)) {
     return true; // allow chrome objects
   }
 
   return Cu.isXrayWrapper(aObj);
 };
+
+exports.dumpn = function dumpn(str) {
+  if (exports.dumpn.wantLogging) {
+    dump("DBG-SERVER: " + str + "\n");
+  }
+}
+
+// We want wantLogging to be writable. The exports object is frozen by the
+// loader, so define it on dumpn instead.
+exports.dumpn.wantLogging = false;
+
+exports.dbg_assert = function dbg_assert(cond, e) {
+  if (!cond) {
+    return e;
+  }
+}
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -3,17 +3,28 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 /**
  * Manages the addon-sdk loader instance used to load the developer tools.
  */
 
-let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+let { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+// addDebuggerToGlobal only allows adding the Debugger object to a global. The
+// this object is not guaranteed to be a global (in particular on B2G, due to
+// compartment sharing), so add the Debugger object to a sandbox instead.
+let sandbox = Cu.Sandbox(CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')());
+Cu.evalInSandbox(
+  "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
+  "addDebuggerToGlobal(this);",
+  sandbox
+);
+let Debugger = sandbox.Debugger;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm");
@@ -29,33 +40,33 @@ this.EXPORTED_SYMBOLS = ["DevToolsLoader
 
 /**
  * Providers are different strategies for loading the devtools.
  */
 
 let loaderGlobals = {
   btoa: btoa,
   console: console,
-  hasChrome: true,
   promise: promise,
   _Iterator: Iterator,
   ChromeWorker: ChromeWorker,
   loader: {
     lazyGetter: XPCOMUtils.defineLazyGetter.bind(XPCOMUtils),
     lazyImporter: XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils),
     lazyServiceGetter: XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils)
   }
 };
 
 // Used when the tools should be loaded from the Firefox package itself (the default)
 function BuiltinProvider() {}
 BuiltinProvider.prototype = {
   load: function() {
     this.loader = new loader.Loader({
       modules: {
+        "Debugger": Debugger,
         "Services": Object.create(Services),
         "toolkit/loader": loader,
         "source-map": SourceMap,
       },
       paths: {
         // When you add a line to this mapping, don't forget to make a
         // corresponding addition to the SrcdirProvider mapping below as well.
         "": "resource://gre/modules/commonjs/",
@@ -123,16 +134,17 @@ SrcdirProvider.prototype = {
     let prettyFastURI = this.fileURI(OS.Path.join(toolkitDir), "pretty-fast.js");
     let asyncUtilsURI = this.fileURI(OS.Path.join(toolkitDir), "async-utils.js");
     let contentObserverURI = this.fileURI(OS.Path.join(toolkitDir), "content-observer.js");
     let gcliURI = this.fileURI(OS.Path.join(toolkitDir, "gcli", "source", "lib", "gcli"));
     let acornURI = this.fileURI(OS.Path.join(toolkitDir, "acorn"));
     let acornWalkURI = OS.Path.join(acornURI, "walk.js");
     this.loader = new loader.Loader({
       modules: {
+        "Debugger": Debugger,
         "Services": Object.create(Services),
         "toolkit/loader": loader,
         "source-map": SourceMap,
       },
       paths: {
         "": "resource://gre/modules/commonjs/",
         "main": mainURI,
         "devtools": devtoolsURI,
--- a/toolkit/devtools/server/actors/root.js
+++ b/toolkit/devtools/server/actors/root.js
@@ -354,16 +354,20 @@ RootActor.prototype = {
   onEcho: function (aRequest) {
     /*
      * Request packets are frozen. Copy aRequest, so that
      * DebuggerServerConnection.onPacket can attach a 'from' property.
      */
     return JSON.parse(JSON.stringify(aRequest));
   },
 
+  onProtocolDescription: function (aRequest) {
+    return protocol.dumpProtocolSpec()
+  },
+
   /* Support for DebuggerServer.addGlobalActor. */
   _createExtraActors: CommonCreateExtraActors,
   _appendExtraActors: CommonAppendExtraActors,
 
   /* ThreadActor hooks. */
 
   /**
    * Prepare to enter a nested event loop by disabling debuggee events.
@@ -394,10 +398,11 @@ RootActor.prototype = {
       windowUtils.suppressEventHandling(false);
     }
   }
 };
 
 RootActor.prototype.requestTypes = {
   "listTabs": RootActor.prototype.onListTabs,
   "listAddons": RootActor.prototype.onListAddons,
-  "echo": RootActor.prototype.onEcho
+  "echo": RootActor.prototype.onEcho,
+  "protocolDescription": RootActor.prototype.onProtocolDescription
 };
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -1108,16 +1108,17 @@ BrowserAddonList.prototype.onUninstalled
   this._onListChanged();
 };
 
 function BrowserAddonActor(aConnection, aAddon) {
   this.conn = aConnection;
   this._addon = aAddon;
   this._contextPool = null;
   this._threadActor = null;
+  this._global = null;
   AddonManager.addAddonListener(this);
 }
 
 BrowserAddonActor.prototype = {
   actorPrefix: "addon",
 
   get exited() {
     return !this._addon;
@@ -1130,42 +1131,63 @@ BrowserAddonActor.prototype = {
   get url() {
     return this._addon.sourceURI ? this._addon.sourceURI.spec : undefined;
   },
 
   get attached() {
     return this._threadActor;
   },
 
+  get global() {
+    return this._global;
+  },
+
   form: function BAA_form() {
     dbg_assert(this.actorID, "addon should have an actorID.");
 
     return {
       actor: this.actorID,
       id: this.id,
       name: this._addon.name,
-      url: this.url
+      url: this.url,
+      debuggable: this._addon.isDebuggable,
     };
   },
 
   disconnect: function BAA_disconnect() {
+    this._addon = null;
+    this._global = null;
     AddonManager.removeAddonListener(this);
   },
 
+  setOptions: function BAA_setOptions(aOptions) {
+    if ("global" in aOptions) {
+      this._global = aOptions.global;
+    }
+  },
+
+  onDisabled: function BAA_onDisabled(aAddon) {
+    if (aAddon != this._addon) {
+      return;
+    }
+
+    this._global = null;
+  },
+
   onUninstalled: function BAA_onUninstalled(aAddon) {
-    if (aAddon != this._addon)
+    if (aAddon != this._addon) {
       return;
+    }
 
     if (this.attached) {
       this.onDetach();
       this.conn.send({ from: this.actorID, type: "tabDetached" });
     }
 
-    this._addon = null;
-    AddonManager.removeAddonListener(this);
+    this.disconnect();
   },
 
   onAttach: function BAA_onAttach() {
     if (this.exited) {
       return { type: "exited" };
     }
 
     if (!this.attached) {
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -4,49 +4,55 @@
  * 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";
 /**
  * Toolkit glue for the remote debugging protocol, loaded into the
  * debugging global.
  */
+let { Ci, Cc, CC, Cu, Cr } = require("chrome");
+let Debugger = require("Debugger");
+let Services = require("Services");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils.js");
+let { dumpn, dbg_assert } = DevToolsUtils;
 let Services = require("Services");
+let EventEmitter = require("devtools/toolkit/event-emitter");
 
 // Until all Debugger server code is converted to SDK modules,
 // imports Components.* alias from chrome module.
 var { Ci, Cc, CC, Cu, Cr } = require("chrome");
 // On B2G, `this` != Global scope, so `Ci` won't be binded on `this`
 // (i.e. this.Ci is undefined) Then later, when using loadSubScript,
 // Ci,... won't be defined for sub scripts.
 this.Ci = Ci;
 this.Cc = Cc;
 this.CC = CC;
 this.Cu = Cu;
 this.Cr = Cr;
+this.Debugger = Debugger;
+this.Services = Services;
 this.DevToolsUtils = DevToolsUtils;
-this.Services = Services;
+this.dumpn = dumpn;
+this.dbg_assert = dbg_assert;
 
 // Overload `Components` to prevent SDK loader exception on Components
 // object usage
 Object.defineProperty(this, "Components", {
   get: function () require("chrome").components
 });
 
 const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
 
 const nsFile = CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
 Cu.import("resource://gre/modules/reflect.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
+dumpn.wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 
 Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
-Cu.import("resource://gre/modules/jsdebugger.jsm");
-addDebuggerToGlobal(this);
 
 function loadSubScript(aURL)
 {
   try {
     let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
       .getService(Ci.mozIJSSubScriptLoader);
     loader.loadSubScript(aURL, this);
   } catch(e) {
@@ -71,30 +77,16 @@ Cu.import("resource://gre/modules/devtoo
 
 XPCOMUtils.defineLazyModuleGetter(this, "console",
                                   "resource://gre/modules/devtools/Console.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "NetworkMonitorManager", () => {
   return require("devtools/toolkit/webconsole/network-monitor").NetworkMonitorManager;
 });
 
-function dumpn(str) {
-  if (wantLogging) {
-    dump("DBG-SERVER: " + str + "\n");
-  }
-}
-this.dumpn = dumpn;
-
-function dbg_assert(cond, e) {
-  if (!cond) {
-    return e;
-  }
-}
-this.dbg_assert = dbg_assert;
-
 loadSubScript.call(this, "resource://gre/modules/devtools/server/transport.js");
 
 // XPCOM constructors
 const ServerSocket = CC("@mozilla.org/network/server-socket;1",
                         "nsIServerSocket",
                         "initSpecialConnection");
 const UnixDomainServerSocket = CC("@mozilla.org/network/server-socket;1",
                                   "nsIServerSocket",
@@ -180,29 +172,16 @@ var DebuggerServer = {
   /**
    * The windowtype of the chrome window to use for actors that use the global
    * window (i.e the global style editor). Set this to your main window type,
    * for example "navigator:browser".
    */
   chromeWindowType: null,
 
   /**
-   * Set that to a function that will be called anytime a new connection
-   * is opened or one is closed.
-   */
-  onConnectionChange: null,
-
-  _fireConnectionChange: function(aWhat) {
-    if (this.onConnectionChange &&
-        typeof this.onConnectionChange === "function") {
-      this.onConnectionChange(aWhat);
-    }
-  },
-
-  /**
    * Prompt the user to accept or decline the incoming connection. This is the
    * default implementation that products embedding the debugger server may
    * choose to override.
    *
    * @return true if the connection should be permitted, false otherwise
    */
   _defaultAllowConnection: function DS__defaultAllowConnection() {
     let title = L10N.getStr("remoteIncomingPromptTitle");
@@ -239,16 +218,18 @@ var DebuggerServer = {
 
     this.xpcInspector = Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
     this.initTransport(aAllowConnectionCallback);
     this.addActors("resource://gre/modules/devtools/server/actors/root.js");
 
     this._initialized = true;
   },
 
+  protocol: require("devtools/server/protocol"),
+
   /**
    * Initialize the debugger server's transport variables.  This can be
    * in place of init() for cases where the jsdebugger isn't needed.
    *
    * @param function aAllowConnectionCallback
    *        The embedder-provider callback, that decides whether an incoming
    *        remote protocol conection should be allowed or refused.
    */
@@ -291,18 +272,16 @@ var DebuggerServer = {
 
     this.closeListener();
     this.globalActorFactories = {};
     this.tabActorFactories = {};
     this._allowConnection = null;
     this._transportInitialized = false;
     this._initialized = false;
 
-    this._fireConnectionChange("closed");
-
     dumpn("Debugger server is shut down.");
   },
 
   /**
    * Load a subscript into the debugging global.
    *
    * @param aURL string A url that will be loaded as a subscript into the
    *        debugging global.  The user must load at least one script
@@ -412,16 +391,40 @@ var DebuggerServer = {
     this.registerModule("devtools/server/actors/memory");
     this.registerModule("devtools/server/actors/eventlooplag");
     if ("nsIProfiler" in Ci) {
       this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
     }
   },
 
   /**
+   * Passes a set of options to the BrowserAddonActors for the given ID.
+   *
+   * @param aId string
+   *        The ID of the add-on to pass the options to
+   * @param aOptions object
+   *        The options.
+   * @return a promise that will be resolved when complete.
+   */
+  setAddonOptions: function DS_setAddonOptions(aId, aOptions) {
+    if (!this._initialized) {
+      return;
+    }
+
+    let promises = [];
+
+    // Pass to all connections
+    for (let connID of Object.getOwnPropertyNames(this._connections)) {
+      promises.push(this._connections[connID].setAddonOptions(aId, aOptions));
+    }
+
+    return all(promises);
+  },
+
+  /**
    * Listens on the given port or socket file for remote debugger connections.
    *
    * @param aPortOrPath int, string
    *        If given an integer, the port to listen on.
    *        Otherwise, the path to the unix socket domain file to listen on.
    */
   openListener: function DS_openListener(aPortOrPath) {
     if (!Services.prefs.getBoolPref("devtools.debugger.remote-enabled")) {
@@ -703,26 +706,26 @@ var DebuggerServer = {
         conn.rootActor.actorID = aForwardingPrefix + ":root";
       else
         conn.rootActor.actorID = "root";
       conn.addActor(conn.rootActor);
       aTransport.send(conn.rootActor.sayHello());
     }
     aTransport.ready();
 
-    this._fireConnectionChange("opened");
+    this.emit("connectionchange", "opened", conn);
     return conn;
   },
 
   /**
    * Remove the connection from the debugging server.
    */
   _connectionClosed: function DS_connectionClosed(aConnection) {
     delete this._connections[aConnection.prefix];
-    this._fireConnectionChange("closed");
+    this.emit("connectionchange", "closed", aConnection);
   },
 
   // DebuggerServer extension API.
 
   /**
    * Registers handlers for new tab-scoped request types defined dynamically.
    * This is used for example by add-ons to augment the functionality of the tab
    * actor. Note that the name or actorPrefix of the request type is not allowed
@@ -807,16 +810,18 @@ var DebuggerServer = {
       let handler = DebuggerServer.globalActorFactories[name];
       if (handler.name == aFunction.name) {
         delete DebuggerServer.globalActorFactories[name];
       }
     }
   }
 };
 
+EventEmitter.decorate(DebuggerServer);
+
 if (this.exports) {
   exports.DebuggerServer = DebuggerServer;
 }
 // Needed on B2G (See header note)
 this.DebuggerServer = DebuggerServer;
 
 /**
  * Construct an ActorPool.
@@ -1056,16 +1061,41 @@ DebuggerServerConnection.prototype = {
     Cu.reportError(errorString);
     dumpn(errorString);
     return {
       error: "unknownError",
       message: errorString
     };
   },
 
+  /**
+   * Passes a set of options to the BrowserAddonActors for the given ID.
+   *
+   * @param aId string
+   *        The ID of the add-on to pass the options to
+   * @param aOptions object
+   *        The options.
+   * @return a promise that will be resolved when complete.
+   */
+  setAddonOptions: function DSC_setAddonOptions(aId, aOptions) {
+    let addonList = this.rootActor._parameters.addonList;
+    if (!addonList) {
+      return resolve();
+    }
+    return addonList.getList().then((addonActors) => {
+      for (let actor of addonActors) {
+        if (actor.id != aId) {
+          continue;
+        }
+        actor.setOptions(aOptions);
+        return;
+      }
+    });
+  },
+
   /* Forwarding packets to other transports based on actor name prefixes. */
 
   /*
    * Arrange to forward packets to another server. This is how we
    * forward debugging connections to child processes.
    *
    * If we receive a packet for an actor whose name begins with |aPrefix|
    * followed by ':', then we will forward that packet to |aTransport|.
--- a/toolkit/devtools/server/protocol.js
+++ b/toolkit/devtools/server/protocol.js
@@ -173,16 +173,17 @@ types.addArrayType = function(subtype) {
 
   let name = "array:" + subtype.name;
 
   // Arrays of primitive types are primitive types themselves.
   if (subtype.primitive) {
     return types.addType(name);
   }
   return types.addType(name, {
+    category: "array",
     read: (v, ctx) => [subtype.read(i, ctx) for (i of v)],
     write: (v, ctx) => [subtype.write(i, ctx) for (i of v)]
   });
 };
 
 /**
  * Add a dict type to the type system.  This allows you to serialize
  * a JS object that contains non-primitive subtypes.
@@ -190,16 +191,18 @@ types.addArrayType = function(subtype) {
  * Properties of the value that aren't included in the specializations
  * will be serialized as primitive values.
  *
  * @param object specializations
  *    A dict of property names => type
  */
 types.addDictType = function(name, specializations) {
   return types.addType(name, {
+    category: "dict",
+    specializations: specializations,
     read: (v, ctx) => {
       let ret = {};
       for (let prop in v) {
         if (prop in specializations) {
           ret[prop] = types.getType(specializations[prop]).read(v[prop], ctx);
         } else {
           ret[prop] = v[prop];
         }
@@ -236,16 +239,17 @@ types.addDictType = function(name, speci
  * called during actor/front construction.
  *
  * @param string name
  *    The typestring to register.
  */
 types.addActorType = function(name) {
   let type = types.addType(name, {
     _actor: true,
+    category: "actor",
     read: (v, ctx, detail) => {
       // If we're reading a request on the server side, just
       // find the actor registered with this actorID.
       if (ctx instanceof Actor) {
         return ctx.conn.getActor(v);
       }
 
       // Reading a response on the client side, check for an
@@ -281,16 +285,17 @@ types.addActorType = function(name) {
     thawed: true
   });
   return type;
 }
 
 types.addNullableType = function(subtype) {
   subtype = types.getType(subtype);
   return types.addType("nullable:" + subtype.name, {
+    category: "nullable",
     read: (value, ctx) => {
       if (value == null) {
         return value;
       }
       return subtype.read(value, ctx);
     },
     write: (value, ctx) => {
       if (value == null) {
@@ -317,16 +322,17 @@ types.addNullableType = function(subtype
  */
 types.addActorDetail = function(name, actorType, detail) {
   actorType = types.getType(actorType);
   if (!actorType._actor) {
     throw Error("Details only apply to actor types, tried to add detail '" + detail + "'' to " + actorType.name + "\n");
   }
   return types.addType(name, {
     _actor: true,
+    category: "detail",
     read: (v, ctx) => actorType.read(v, ctx, detail),
     write: (v, ctx) => actorType.write(v, ctx, detail)
   });
 }
 
 /**
  * Register an actor lifetime.  This lets the type system find a parent
  * actor that differs from the actor fulfilling the request.
@@ -357,16 +363,17 @@ types.addLifetime = function(name, prop)
  */
 types.addLifetimeType = function(lifetime, subtype) {
   subtype = types.getType(subtype);
   if (!subtype._actor) {
     throw Error("Lifetimes only apply to actor types, tried to apply lifetime '" + lifetime + "'' to " + subtype.name);
   }
   let prop = registeredLifetimes.get(lifetime);
   return types.addType(lifetime + ":" + subtype.name, {
+    category: "lifetime",
     read: (value, ctx) => subtype.read(value, ctx[prop]),
     write: (value, ctx) => subtype.write(value, ctx[prop])
   })
 }
 
 // Add a few named primitive types.
 types.Primitive = types.addType("primitive");
 types.String = types.addType("string");
@@ -402,16 +409,23 @@ let Arg = Class({
   },
 
   write: function(arg, ctx) {
     return this.type.write(arg, ctx);
   },
 
   read: function(v, ctx, outArgs) {
     outArgs[this.index] = this.type.read(v, ctx);
+  },
+
+  describe: function() {
+    return {
+      _arg: this.index,
+      type: this.type.name,
+    }
   }
 });
 exports.Arg = Arg;
 
 /**
  * Placeholder for an options argument value that should be hoisted
  * into the packet.
  *
@@ -447,16 +461,23 @@ let Option = Class({
   read: function(v, ctx, outArgs, name) {
     if (outArgs[this.index] === undefined) {
       outArgs[this.index] = {};
     }
     if (v === undefined) {
       return;
     }
     outArgs[this.index][name] = this.type.read(v, ctx);
+  },
+
+  describe: function() {
+    return {
+      _option: this.index,
+      type: this.type.name,
+    }
   }
 });
 
 exports.Option = Option;
 
 /**
  * Placeholder for return values in a response template.
  *
@@ -469,16 +490,22 @@ let RetVal = Class({
   },
 
   write: function(v, ctx) {
     return this.type.write(v, ctx);
   },
 
   read: function(v, ctx) {
     return this.type.read(v, ctx);
+  },
+
+  describe: function() {
+    return {
+      _retval: this.type.name
+    }
   }
 });
 
 exports.RetVal = RetVal;
 
 /* Template handling functions */
 
 /**
@@ -513,16 +540,25 @@ function findPlaceholders(template, cons
     findPlaceholders(template[name], constructor, path, placeholders);
     path.pop();
   }
 
   return placeholders;
 }
 
 
+function describeTemplate(template) {
+  return JSON.parse(JSON.stringify(template, (key, value) => {
+    if (value.describe) {
+      return value.describe();
+    }
+    return value;
+  }));
+}
+
 /**
  * Manages a request template.
  *
  * @param object template
  *    The request template.
  * @construcor
  */
 let Request = Class({
@@ -565,16 +601,18 @@ let Request = Class({
     for (let templateArg of this.args) {
       let arg = templateArg.placeholder;
       let path = templateArg.path;
       let name = path[path.length - 1];
       arg.read(getPath(packet, path), ctx, fnArgs, name);
     }
     return fnArgs;
   },
+
+  describe: function() { return describeTemplate(this.template); }
 });
 
 /**
  * Manages a response template.
  *
  * @param object template
  *    The response template.
  * @construcor
@@ -619,17 +657,19 @@ let Response = Class({
    *    The object reading the response.
    */
   read: function(packet, ctx) {
     if (!this.retVal) {
       return undefined;
     }
     let v = getPath(packet, this.path);
     return this.retVal.read(v, ctx);
-  }
+  },
+
+  describe: function() { return describeTemplate(this.template); }
 });
 
 /**
  * Actor and Front implementations
  */
 
 /**
  * A protocol object that can manage the lifetime of other protocol
@@ -953,17 +993,20 @@ let actorProto = function(actorProto) {
 exports.ActorClass = function(proto) {
   if (!proto.typeName) {
     throw Error("Actor prototype must have a typeName member.");
   }
   proto.extends = Actor;
   if (!registeredTypes.has(proto.typeName)) {
     types.addActorType(proto.typeName);
   }
-  return Class(actorProto(proto));
+  let cls = Class(actorProto(proto));
+
+  registeredTypes.get(proto.typeName).actorSpec = proto._actorSpec;
+  return cls;
 };
 
 /**
  * Base class for client-side actor fronts.
  */
 let Front = Class({
   extends: Pool,
 
@@ -1234,8 +1277,62 @@ let frontProto = function(proto) {
  */
 exports.FrontClass = function(actorType, proto) {
   proto.actorType = actorType;
   proto.extends = Front;
   let cls = Class(frontProto(proto));
   registeredTypes.get(cls.prototype.typeName).frontClass = cls;
   return cls;
 }
+
+
+exports.dumpActorSpec = function(type) {
+  let actorSpec = type.actorSpec;
+  let ret = {
+    category: "actor",
+    typeName: type.name,
+    methods: [],
+    events: {}
+  };
+
+  for (let method of actorSpec.methods) {
+    ret.methods.push({
+      name: method.name,
+      release: method.release || undefined,
+      oneway: method.oneway || undefined,
+      request: method.request.describe(),
+      response: method.response.describe()
+    });
+  }
+
+  if (actorSpec.events) {
+    for (let [name, request] of actorSpec.events) {
+      ret.events[name] = request.describe();
+    }
+  }
+
+
+  JSON.stringify(ret);
+
+  return ret;
+}
+
+exports.dumpProtocolSpec = function() {
+  let ret = {
+    types: {},
+  };
+
+  for (let [name, type] of registeredTypes) {
+    // Force lazy instantiation if needed.
+    type = types.getType(name);
+    if (type.category === "dict") {
+      ret.types[name] = {
+        category: "dict",
+        typeName: name,
+        specializations: type.specializations
+      }
+    } else if (type.category === "actor") {
+      ret.types[name] = exports.dumpActorSpec(type);
+    }
+  }
+
+  return ret;
+}
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -30,16 +30,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
                                   "resource://gre/modules/PermissionsUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserToolboxProcess",
+                                  "resource:///modules/devtools/ToolboxProcess.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ChromeRegistry",
                                    "@mozilla.org/chrome/chrome-registry;1",
                                    "nsIChromeRegistry");
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ResProtocolHandler",
                                    "@mozilla.org/network/protocol;1?name=resource",
@@ -1865,16 +1867,24 @@ this.XPIProvider = {
       this.minCompatiblePlatformVersion = Prefs.getCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION,
                                                             null);
       this.enabledAddons = "";
 
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_APP_VERSION, this, false);
       Services.prefs.addObserver(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, this, false);
       Services.obs.addObserver(this, NOTIFICATION_FLUSH_PERMISSIONS, false);
 
+      try {
+        BrowserToolboxProcess.on("connectionchange",
+                                 this.onDebugConnectionChange.bind(this));
+      }
+      catch (e) {
+        // BrowserToolboxProcess is not available in all applications
+      }
+
       let flushCaches = this.checkForChanges(aAppChanged, aOldAppVersion,
                                              aOldPlatformVersion);
 
       // Changes to installed extensions may have changed which theme is selected
       this.applyThemeChange();
 
       // If the application has been upgraded and there are add-ons outside the
       // application directory then we may need to synchronize compatibility
@@ -3850,16 +3860,25 @@ this.XPIProvider = {
         logger.warn("Attempting to activate an already active default theme");
       }
     }
     else {
       logger.warn("Unable to activate the default theme");
     }
   },
 
+  onDebugConnectionChange: function(aEvent, aWhat, aConnection) {
+    if (aWhat != "opened")
+      return;
+
+    for (let id of Object.keys(this.bootstrapScopes)) {
+      aConnection.setAddonOptions(id, { global: this.bootstrapScopes[id] });
+    }
+  },
+
   /**
    * Notified when a preference we're interested in has changed.
    *
    * @see nsIObserver
    */
   observe: function XPI_observe(aSubject, aTopic, aData) {
     if (aTopic == NOTIFICATION_FLUSH_PERMISSIONS) {
       if (!aData || aData == XPI_PERMISSION) {
@@ -4114,30 +4133,44 @@ this.XPIProvider = {
       Components.utils.evalInSandbox(
         "Components.classes['@mozilla.org/moz/jssubscript-loader;1'] \
                    .createInstance(Components.interfaces.mozIJSSubScriptLoader) \
                    .loadSubScript(__SCRIPT_URI_SPEC__);", this.bootstrapScopes[aId], "ECMAv5");
     }
     catch (e) {
       logger.warn("Error loading bootstrap.js for " + aId, e);
     }
+
+    try {
+      BrowserToolboxProcess.setAddonOptions(aId, { global: this.bootstrapScopes[aId] });
+    }
+    catch (e) {
+      // BrowserToolboxProcess is not available in all applications
+    }
   },
 
   /**
    * Unloads a bootstrap scope by dropping all references to it and then
    * updating the list of active add-ons with the crash reporter.
    *
    * @param  aId
    *         The add-on's ID
    */
   unloadBootstrapScope: function XPI_unloadBootstrapScope(aId) {
     delete this.bootstrapScopes[aId];
     delete this.bootstrappedAddons[aId];
     this.persistBootstrappedAddons();
     this.addAddonsToCrashReporter();
+
+    try {
+      BrowserToolboxProcess.setAddonOptions(aId, { global: null });
+    }
+    catch (e) {
+      // BrowserToolboxProcess is not available in all applications
+    }
   },
 
   /**
    * Calls a bootstrap method for an add-on.
    *
    * @param  aId
    *         The ID of the add-on
    * @param  aVersion
--- a/toolkit/mozapps/extensions/test/browser/browser_experiments.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js
@@ -1,55 +1,71 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
+let {AddonTestUtils} = Components.utils.import("resource://testing-common/AddonManagerTesting.jsm", {});
+let {HttpServer} = Components.utils.import("resource://testing-common/httpd.js", {});
+
 let gManagerWindow;
 let gCategoryUtilities;
-let gInstalledAddons = [];
-let gContext = this;
+let gExperiments;
+let gHttpServer;
+
+function getExperimentAddons() {
+  let deferred = Promise.defer();
+  AddonManager.getAddonsByTypes(["experiment"], (addons) => {
+    deferred.resolve(addons);
+  });
+  return deferred.promise;
+}
 
 add_task(function* initializeState() {
   gManagerWindow = yield open_manager();
   gCategoryUtilities = new CategoryUtilities(gManagerWindow);
 
+  registerCleanupFunction(() => {
+    if (gHttpServer) {
+      gHttpServer.stop(() => {});
+    }
+  });
+
   // The Experiments Manager will interfere with us by preventing installs
   // of experiments it doesn't know about. We remove it from the equation
   // because here we are only concerned with core Addon Manager operation,
   // not the superset Experiments Manager has imposed.
   if ("@mozilla.org/browser/experiments-service;1" in Components.classes) {
-    Components.utils.import("resource:///modules/experiments/Experiments.jsm", gContext);
-
+    let tmp = {};
+    Components.utils.import("resource:///modules/experiments/Experiments.jsm", tmp);
     // There is a race condition between XPCOM service initialization and
     // this test running. We have to initialize the instance first, then
     // uninitialize it to prevent this.
-    let instance = gContext.Experiments.instance();
-    yield instance.uninit();
+    gExperiments = tmp.Experiments.instance();
+    yield gExperiments._mainTask;
+    yield gExperiments.uninit();
   }
 });
 
 // On an empty profile with no experiments, the experiment category
 // should be hidden.
 add_task(function* testInitialState() {
   Assert.ok(gCategoryUtilities.get("experiment", false), "Experiment tab is defined.");
-
   Assert.ok(!gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab hidden by default.");
 });
 
 add_task(function* testExperimentInfoNotVisible() {
   yield gCategoryUtilities.openType("extension");
   let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
   is_element_hidden(el, "Experiment info not visible on other types.");
 });
 
 // If we have an active experiment, we should see the experiments tab
 // and that tab should have some messages.
 add_task(function* testActiveExperiment() {
   let addon = yield install_addon("addons/browser_experiment1.xpi");
-  gInstalledAddons.push(addon);
 
   Assert.ok(addon.userDisabled, "Add-on is disabled upon initial install.");
   Assert.equal(addon.isActive, false, "Add-on is not active.");
 
   Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
 
   yield gCategoryUtilities.openType("experiment");
   let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
@@ -138,19 +154,132 @@ add_task(function* testButtonPresence() 
   // Corresponds to lack of disable permission.
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
   is_element_hidden(el, "Disable button not visible.");
   // Corresponds to lack of enable permission.
   el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
   is_element_hidden(el, "Enable button not visible.");
 });
 
+// Remove the add-on we've been testing with.
 add_task(function* testCleanup() {
-  for (let addon of gInstalledAddons) {
-    addon.uninstall();
+  yield AddonTestUtils.uninstallAddonByID("test-experiment1@experiments.mozilla.org");
+  // Verify some conditions, just in case.
+  let addons = yield getExperimentAddons();
+  Assert.equal(addons.length, 0, "No experiment add-ons are installed.");
+});
+
+// We need to initialize the experiments service for the following tests.
+add_task(function* initializeExperiments() {
+  if (!gExperiments) {
+    return;
+  }
+
+  // We need to remove the cache file to help ensure consistent state.
+  yield OS.File.remove(gExperiments._cacheFilePath);
+
+  info("Initializing experiments service.");
+  yield gExperiments.init();
+  info("Experiments service finished first run.");
+
+  // Check conditions, just to be sure.
+  let experiments = yield gExperiments.getExperiments();
+  Assert.equal(experiments.length, 0, "No experiments known to the service.");
+});
+
+// The following tests should ideally live in browser/experiments/. However,
+// they rely on some of the helper functions from head.js, which can't easily
+// be consumed from other directories. So, they live here.
+
+add_task(function* testActivateExperiment() {
+  if (!gExperiments) {
+    info("Skipping experiments test because that feature isn't available.");
+    return;
   }
 
+  gHttpServer = new HttpServer();
+  gHttpServer.start(-1);
+  let root = "http://localhost:" + gHttpServer.identity.primaryPort + "/";
+  gHttpServer.registerPathHandler("/manifest", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+    response.write(JSON.stringify({
+      "version": 1,
+      "experiments": [
+        {
+          id: "experiment-1",
+          xpiURL: TESTROOT + "addons/browser_experiment1.xpi",
+          xpiHash: "IRRELEVANT",
+          startTime: Date.now() / 1000 - 3600,
+          endTime: Date.now() / 1000 + 3600,
+          maxActiveSeconds: 600,
+          appName: [Services.appinfo.name],
+          channel: [gExperiments._policy.updatechannel()],
+        },
+      ],
+    }));
+    response.processAsync();
+    response.finish();
+  });
+
+  Services.prefs.setBoolPref("experiments.manifest.cert.checkAttributes", false);
+  Services.prefs.setCharPref("experiments.manifest.uri", root + "manifest");
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref("experiments.manifest.cert.checkAttributes");
+    Services.prefs.clearUserPref("experiments.manifest.uri");
+  });
+
+  // This makes testing easier.
+  gExperiments._policy.ignoreHashes = true;
+  registerCleanupFunction(() => { gExperiments._policy.ignoreHashes = false; });
+
+  info("Manually updating experiments manifest.");
+  yield gExperiments.updateManifest();
+  info("Experiments update complete.");
+
+  let deferred = Promise.defer();
+  gHttpServer.stop(() => {
+    gHttpServer = null;
+
+    info("getting experiment by ID");
+    AddonManager.getAddonByID("test-experiment1@experiments.mozilla.org", (addon) => {
+      Assert.ok(addon, "Add-on installed via Experiments manager.");
+
+      deferred.resolve();
+    });
+  });
+
+  yield deferred.promise;
+
+  Assert.ok(gCategoryUtilities.isTypeVisible, "experiment", "Experiment tab visible.");
+  yield gCategoryUtilities.openType("experiment");
+  let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
+  is_element_visible(el, "Experiment info is visible on experiment tab.");
+});
+
+add_task(function testDeactivateExperiment() {
+  if (!gExperiments) {
+    return;
+  }
+
+  yield gExperiments._updateExperiments({
+    "version": 1,
+    "experiments": [],
+  });
+
+  yield gExperiments.disableExperiment("testing");
+});
+
+add_task(function* testCleanup() {
+  if (gExperiments) {
+    // We perform the uninit/init cycle to purge any leftover state.
+    yield OS.File.remove(gExperiments._cacheFilePath);
+    yield gExperiments.uninit();
+    yield gExperiments.init();
+  }
+
+  // Check post-conditions.
+  let addons = yield getExperimentAddons();
+  Assert.equal(addons.length, 0, "No experiment add-ons are installed.");
+
   yield close_manager(gManagerWindow);
 
-  if ("@mozilla.org/browser/experiments-service;1" in Components.classes) {
-    yield gContext.Experiments.instance().init();
-  }
 });
+
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/locale-fonts.nsh
@@ -0,0 +1,681 @@
+# 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/.
+
+; Acholi
+!if "${AB_CD}" == "ach"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Afrikaans
+!if "${AB_CD}" == "af"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Akan
+!if "${AB_CD}" == "ak"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Aragonese
+!if "${AB_CD}" == "an"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Arabic
+!if "${AB_CD}" == "ar"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Assamese
+!if "${AB_CD}" == "as"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Asturian
+!if "${AB_CD}" == "ast"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Azerbaijani
+!if "${AB_CD}" == "az"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Belarusian
+!if "${AB_CD}" == "be"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Bulgarian
+!if "${AB_CD}" == "bg"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Bengali
+!if "${AB_CD}" == "bn-BD"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Bengali - India
+!if "${AB_CD}" == "bn-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Breton
+!if "${AB_CD}" == "br"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Bosnian
+!if "${AB_CD}" == "bs"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Catalan
+!if "${AB_CD}" == "ca"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Czech
+!if "${AB_CD}" == "cs"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Kashubian
+!if "${AB_CD}" == "csb"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Welsh
+!if "${AB_CD}" == "cy"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Danish
+!if "${AB_CD}" == "da"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; German
+!if "${AB_CD}" == "de"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Greek
+!if "${AB_CD}" == "el"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; English - Great Britain
+!if "${AB_CD}" == "en-GB"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; English - United States
+!if "${AB_CD}" == "en-US"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; English - South Africa
+!if "${AB_CD}" == "en-ZA"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Esperanto
+!if "${AB_CD}" == "eo"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish - Argentina
+!if "${AB_CD}" == "es-AR"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish - Chile
+!if "${AB_CD}" == "es-CL"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish
+!if "${AB_CD}" == "es-ES"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish - Mexico
+!if "${AB_CD}" == "es-MX"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Estonian
+!if "${AB_CD}" == "et"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Basque
+!if "${AB_CD}" == "eu"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Persian
+!if "${AB_CD}" == "fa"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Fulah
+!if "${AB_CD}" == "ff"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Finnish
+!if "${AB_CD}" == "fi"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; French
+!if "${AB_CD}" == "fr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Frisian
+!if "${AB_CD}" == "fy-NL"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Irish
+!if "${AB_CD}" == "ga-IE"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Scottish Gaelic
+!if "${AB_CD}" == "gd"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Galician
+!if "${AB_CD}" == "gl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Gujarati
+!if "${AB_CD}" == "gu-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Hawaiian
+!if "${AB_CD}" == "haw"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Hebrew
+!if "${AB_CD}" == "he"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; hindi
+!if "${AB_CD}" == "hi-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Croatian
+!if "${AB_CD}" == "hr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Upper Sorbian
+!if "${AB_CD}" == "hsb"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Hungarian
+!if "${AB_CD}" == "hu"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Armenian
+!if "${AB_CD}" == "hy-AM"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Indonesian
+!if "${AB_CD}" == "id"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Iloko
+!if "${AB_CD}" == "ilo"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Icelandic
+!if "${AB_CD}" == "is"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Italian
+!if "${AB_CD}" == "it"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Japanese
+!if "${AB_CD}" == "ja"
+!define FONT_NAME1 "Meiryo UI"
+!define FONT_FILE1 "meiryo.ttc"
+!endif
+
+; Georgian
+!if "${AB_CD}" == "ka"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Kazakh
+!if "${AB_CD}" == "kk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Khmer
+!if "${AB_CD}" == "km"
+!define FONT_NAME1 "Leelawadee UI"
+!define FONT_FILE1 "LeelawUI.ttf"
+!endif
+
+; Kannada
+!if "${AB_CD}" == "kn"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Korean
+!if "${AB_CD}" == "ko"
+!define FONT_NAME1 "Malgun Gothic"
+!define FONT_FILE1 "malgun.ttf"
+!endif
+
+; Kurdish
+!if "${AB_CD}" == "ku"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Luganda
+!if "${AB_CD}" == "lg"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Ligurian
+!if "${AB_CD}" == "lij"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Lithuanian
+!if "${AB_CD}" == "lt"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Latvian
+!if "${AB_CD}" == "lv"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Maithili
+!if "${AB_CD}" == "mai"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Macedonian
+!if "${AB_CD}" == "mk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Malayalam
+!if "${AB_CD}" == "ml"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Mongolian
+!if "${AB_CD}" == "mn"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Marathi
+!if "${AB_CD}" == "mr"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Malay
+!if "${AB_CD}" == "ms"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Burmese
+!if "${AB_CD}" == "my"
+!define FONT_NAME1 "Myanmar Text"
+!define FONT_FILE1 "mmrtext.ttf"
+!endif
+
+; Norwegian Bokmål
+!if "${AB_CD}" == "nb-NO"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Nepali Nepal
+!if "${AB_CD}" == "ne-NP"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Dutch
+!if "${AB_CD}" == "nl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Norwegian Nynorsk
+!if "${AB_CD}" == "nn-NO"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Southern Ndebele
+!if "${AB_CD}" == "nr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Northern Sotho
+!if "${AB_CD}" == "nso"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Occitan
+!if "${AB_CD}" == "oc"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Oriya
+!if "${AB_CD}" == "or"
+!define FONT_NAME1 "Kalinga"
+!define FONT_FILE1 "kalinga.ttf"
+!endif
+
+; Punjabi
+!if "${AB_CD}" == "pa-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Polish
+!if "${AB_CD}" == "pl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Portugese - Brazil
+!if "${AB_CD}" == "pt-BR"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Portugese
+!if "${AB_CD}" == "pt-PT"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Romansh
+!if "${AB_CD}" == "rm"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Romanian
+!if "${AB_CD}" == "ro"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Russian
+!if "${AB_CD}" == "ru"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Kinyarwanda
+!if "${AB_CD}" == "rw"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Sakha
+!if "${AB_CD}" == "sah"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Sinhala
+!if "${AB_CD}" == "si"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Slovak
+!if "${AB_CD}" == "sk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Slovene
+!if "${AB_CD}" == "sl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Songhay
+!if "${AB_CD}" == "son"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Albanian
+!if "${AB_CD}" == "sq"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Serbian
+!if "${AB_CD}" == "sr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Swazi
+!if "${AB_CD}" == "ss"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Southern Sotho
+!if "${AB_CD}" == "st"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Swedish
+!if "${AB_CD}" == "sv-Se"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Swahili
+!if "${AB_CD}" == "sw"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Tamil
+!if "${AB_CD}" == "ta"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Tamil - Sri Lanka
+!if "${AB_CD}" == "ta-LK"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Telugu
+!if "${AB_CD}" == "te"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Thai
+!if "${AB_CD}" == "th"
+!define FONT_NAME1 "Leelawadee UI"
+!define FONT_FILE1 "LeelawUI.ttf"
+!endif
+
+; Tswana
+!if "${AB_CD}" == "tn"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Turkish
+!if "${AB_CD}" == "tr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Tsonga
+!if "${AB_CD}" == "ts"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Uyghur
+!if "${AB_CD}" == "ug"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Ukrainian
+!if "${AB_CD}" == "uk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Urdu
+!if "${AB_CD}" == "ur"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Venda
+!if "${AB_CD}" == "ve"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Vietnamese
+!if "${AB_CD}" == "vi"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Wolof
+!if "${AB_CD}" == "wo"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Xhosa
+!if "${AB_CD}" == "xh"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Chinese (Simplified)
+!if "${AB_CD}" == "zh-CN"
+!define FONT_NAME1 "Microsoft YaHei UI"
+!define FONT_FILE1 "msyh.ttc"
+!endif
+
+; Chinese (Traditional)
+!if "${AB_CD}" == "zh-TW"
+!define FONT_NAME1 "Microsoft JhengHei UI"
+!define FONT_FILE1 "msjh.ttc"
+!endif
+
+; Zulu
+!if "${AB_CD}" == "zu"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
--- a/toolkit/mozapps/installer/windows/nsis/makensis.mk
+++ b/toolkit/mozapps/installer/windows/nsis/makensis.mk
@@ -10,16 +10,17 @@ include $(MOZILLA_DIR)/toolkit/mozapps/i
 
 ABS_CONFIG_DIR := $(abspath $(CONFIG_DIR))
 
 SFX_MODULE ?= $(error SFX_MODULE is not defined)
 
 TOOLKIT_NSIS_FILES = \
 	common.nsh \
 	locale.nlf \
+	locale-fonts.nsh \
 	locale-rtl.nlf \
 	locales.nsi \
 	overrides.nsh \
 	setup.ico \
 	$(NULL)
 
 CUSTOM_NSIS_PLUGINS = \
 	AccessControl.dll \