Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 24 Jan 2014 17:00:09 -0500
changeset 181136 a12c7d7ac590402510356d33fc6acd4f5e9c1b1a
parent 181105 fdc82b2c5584323dfd7deaaa0601e596d6725578 (diff)
parent 181135 166b7998065e295f4abf9f00bfc387291cfa1975 (current diff)
child 181168 765019ac7236420a6b26ecef7f8fb0fea8efc732
push id3343
push userffxbld
push dateMon, 17 Mar 2014 21:55:32 +0000
treeherdermozilla-beta@2f7d3415f79f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone29.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.
browser/components/sessionstore/src/Messenger.jsm
browser/components/sessionstore/src/SessionSaver.jsm
browser/components/sessionstore/test/browser_625257.js
browser/components/sessionstore/test/browser_pageshow.js
browser/components/sessionstore/test/browser_tabStateCache.js
browser/devtools/webconsole/test/head.js
browser/devtools/webconsole/webconsole.js
browser/extensions/pdfjs/content/build/pdf.worker.js
browser/extensions/pdfjs/extension-files
browser/extensions/pdfjs/icon.png
browser/extensions/pdfjs/icon64.png
toolkit/components/telemetry/Histograms.json
--- a/browser/devtools/webconsole/hudservice.js
+++ b/browser/devtools/webconsole/hudservice.js
@@ -6,19 +6,19 @@
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 
 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
 let Heritage = require("sdk/core/heritage");
 
-loader.lazyGetter(this, "promise", () => require("sdk/core/promise"));
 loader.lazyGetter(this, "Telemetry", () => require("devtools/shared/telemetry"));
 loader.lazyGetter(this, "WebConsoleFrame", () => require("devtools/webconsole/webconsole").WebConsoleFrame);
+loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
 loader.lazyImporter(this, "gDevTools", "resource:///modules/devtools/gDevTools.jsm");
 loader.lazyImporter(this, "devtools", "resource://gre/modules/devtools/Loader.jsm");
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
 loader.lazyImporter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm");
 loader.lazyImporter(this, "DebuggerClient", "resource://gre/modules/devtools/dbg-client.jsm");
 
 const STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
 let l10n = new WebConsoleUtils.l10n(STRINGS_URI);
@@ -105,16 +105,17 @@ HUD_SERVICE.prototype =
    *        The window of the browser console owner.
    * @return object
    *         A promise object for the opening of the new BrowserConsole instance.
    */
   openBrowserConsole:
   function HS_openBrowserConsole(aTarget, aIframeWindow, aChromeWindow)
   {
     let hud = new BrowserConsole(aTarget, aIframeWindow, aChromeWindow);
+    this._browserConsoleID = hud.hudId;
     this.consoles.set(hud.hudId, hud);
     return hud.init();
   },
 
   /**
    * Returns the Web Console object associated to a content window.
    *
    * @param nsIDOMWindow aContentWindow
@@ -236,17 +237,16 @@ HUD_SERVICE.prototype =
       });
 
       return deferred.promise;
     }
 
     connect().then(getTarget).then(openWindow).then((aWindow) => {
       this.openBrowserConsole(target, aWindow, aWindow)
         .then((aBrowserConsole) => {
-          this._browserConsoleID = aBrowserConsole.hudId;
           this._browserConsoleDefer.resolve(aBrowserConsole);
           this._browserConsoleDefer = null;
         })
     }, console.error);
 
     return this._browserConsoleDefer.promise;
   },
 
--- a/browser/devtools/webconsole/panel.js
+++ b/browser/devtools/webconsole/panel.js
@@ -1,17 +1,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const {Cc, Ci, Cu} = require("chrome");
 
-loader.lazyGetter(this, "promise", () => require("sdk/core/promise"));
+loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
 loader.lazyGetter(this, "HUDService", () => require("devtools/webconsole/hudservice"));
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
 
 /**
  * A DevToolPanel that controls the Web Console.
  */
 function WebConsolePanel(iframeWindow, toolbox)
 {
--- a/browser/devtools/webconsole/test/browser_console_dead_objects.js
+++ b/browser/devtools/webconsole/test/browser_console_dead_objects.js
@@ -44,17 +44,20 @@ function test()
           "dead object found");
 
     hud.jsterm.setInputValue("foobarzTezt");
 
     for (let c of ".hello") {
       EventUtils.synthesizeKey(c, {}, hud.iframeWindow);
     }
 
-    hud.jsterm.execute(null, onReadProperty.bind(null, msg));
+    hud.jsterm.execute(null, () => {
+      // executeSoon() is needed to get out of the execute() event loop.
+      executeSoon(onReadProperty.bind(null, msg));
+    });
   }
 
   function onReadProperty(deadObjectMessage)
   {
     isnot(hud.outputNode.textContent.indexOf("can't access dead object"), -1,
           "'cannot access dead object' message found");
 
     // Click the second execute output.
@@ -64,16 +67,17 @@ function test()
           "message text check");
 
     hud.jsterm.once("variablesview-fetched", onFetched);
     EventUtils.synthesizeMouse(clickable, 2, 2, {}, hud.iframeWindow);
   }
 
   function onFetched()
   {
+    ok(true, "variables view fetched");
     hud.jsterm.execute("delete window.foobarzTezt; 2013-26", onCalcResult);
   }
 
   function onCalcResult()
   {
     isnot(hud.outputNode.textContent.indexOf("1987"), -1, "result message found");
 
     // executeSoon() is needed to get out of the execute() event loop.
--- a/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
+++ b/browser/devtools/webconsole/test/browser_webconsole_bug_613642_maintain_scroll.js
@@ -90,47 +90,34 @@ function testGen() {
       // Wait for scroll to bottom.
       return;
     }
     scrollBox.onscroll = null;
     isnot(scrollBox.scrollTop, 0, "scroll location updated (moved to bottom)");
     testNext();
   };
   EventUtils.synthesizeKey("VK_END", {});
-  yield;
+  yield undefined;
 
   let oldScrollTop = scrollBox.scrollTop;
 
   content.console.log("test message 151");
 
-  waitForMessages({
-    webconsole: hud,
-    messages: [{
-      text: "test message 151",
-      category: CATEGORY_WEBDEV,
-      severity: SEVERITY_LOG,
-    }],
-  }).then(() => {
-    scrollBox.onscroll = () => {
-      if (scrollBox.scrollTop == oldScrollTop) {
-        // Wait for scroll to change.
-        return;
-      }
-      scrollBox.onscroll = null;
-      isnot(scrollBox.scrollTop, oldScrollTop, "scroll location updated (moved to bottom again)");
-      testNext();
-    };
-  });
+  scrollBox.onscroll = () => {
+    if (scrollBox.scrollTop == oldScrollTop) {
+      // Wait for scroll to change.
+      return;
+    }
+    scrollBox.onscroll = null;
+    isnot(scrollBox.scrollTop, oldScrollTop, "scroll location updated (moved to bottom again)");
+    hud = testDriver = null;
+    finishTest();
+  };
 
   yield undefined;
-
-  hud = testDriver = null;
-  finishTest();
-  
-  yield undefined;
 }
 
 function test() {
   addTab("data:text/html;charset=utf-8,Web Console test for bug 613642: remember scroll location");
   browser.addEventListener("load", function tabLoad(aEvent) {
     browser.removeEventListener(aEvent.type, tabLoad, true);
     openConsole(null, function(aHud) {
       hud = aHud;
--- a/browser/devtools/webconsole/test/head.js
+++ b/browser/devtools/webconsole/test/head.js
@@ -1,16 +1,16 @@
 /* vim:set ts=2 sw=2 sts=2 et: */
 /* 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/. */
 
 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
-let {Promise: promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let {require, TargetFactory} = devtools;
 let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils");
 let {Messages} = require("devtools/webconsole/console-output");
 
 // promise._reportErrors = true; // please never leave me.
 
--- a/browser/devtools/webconsole/webconsole.js
+++ b/browser/devtools/webconsole/webconsole.js
@@ -9,17 +9,17 @@
 const {Cc, Ci, Cu} = require("chrome");
 
 let WebConsoleUtils = require("devtools/toolkit/webconsole/utils").Utils;
 
 loader.lazyServiceGetter(this, "clipboardHelper",
                          "@mozilla.org/widget/clipboardhelper;1",
                          "nsIClipboardHelper");
 loader.lazyImporter(this, "Services", "resource://gre/modules/Services.jsm");
-loader.lazyGetter(this, "promise", () => require("sdk/core/promise"));
+loader.lazyImporter(this, "promise", "resource://gre/modules/Promise.jsm", "Promise");
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
 loader.lazyGetter(this, "AutocompletePopup",
                   () => require("devtools/shared/autocomplete-popup").AutocompletePopup);
 loader.lazyGetter(this, "ToolSidebar",
                   () => require("devtools/framework/sidebar").ToolSidebar);
 loader.lazyGetter(this, "NetworkPanel",
                   () => require("devtools/webconsole/network-panel").NetworkPanel);
 loader.lazyGetter(this, "ConsoleOutput",
--- a/browser/installer/windows/nsis/installer.nsi
+++ b/browser/installer/windows/nsis/installer.nsi
@@ -1043,16 +1043,20 @@ Function .onInit
   System::Call 'kernel32::SetDllDirectoryW(w "")'
 
   StrCpy $PageName ""
   StrCpy $LANGUAGE 0
   ${SetBrandNameVars} "$EXEDIR\core\distribution\setup.ini"
 
   ${InstallOnInitCommon} "$(WARN_MIN_SUPPORTED_OS_MSG)"
 
+  ${If} ${AtLeastWinVista}
+    System::Call 'user32::SetProcessDPIAware()'
+  ${EndIf}
+
   !insertmacro InitInstallOptionsFile "options.ini"
   !insertmacro InitInstallOptionsFile "shortcuts.ini"
   !insertmacro InitInstallOptionsFile "components.ini"
   !insertmacro InitInstallOptionsFile "summary.ini"
 
   WriteINIStr "$PLUGINSDIR\options.ini" "Settings" NumFields "5"
 
   WriteINIStr "$PLUGINSDIR\options.ini" "Field 1" Type   "label"
--- a/browser/installer/windows/nsis/maintenanceservice_installer.nsi
+++ b/browser/installer/windows/nsis/maintenanceservice_installer.nsi
@@ -127,16 +127,20 @@ Function .onInit
   ${EndUnless}
 FunctionEnd
 
 Function un.onInit
   ; Remove the current exe directory from the search order.
   ; This only effects LoadLibrary calls and not implicitly loaded DLLs.
   System::Call 'kernel32::SetDllDirectoryW(w "")'
 
+  ${If} ${AtLeastWinVista}
+    System::Call 'user32::SetProcessDPIAware()'
+  ${EndIf}
+
   StrCpy $BrandFullNameDA "${MaintFullName}"
   StrCpy $BrandFullName "${MaintFullName}"
 FunctionEnd
 
 Section "MaintenanceService"
   AllowSkipFiles off
 
   CreateDirectory $INSTDIR
--- a/browser/installer/windows/nsis/stub.nsi
+++ b/browser/installer/windows/nsis/stub.nsi
@@ -331,16 +331,20 @@ Function .onInit
       Quit
     ${EndIf}
   ${EndUnless}
 !endif
 
   ; Require elevation if the user can elevate
   ${ElevateUAC}
 
+  ${If} ${AtLeastWinVista}
+    System::Call 'user32::SetProcessDPIAware()'
+  ${EndIf}
+
   ; Create a mutex to prevent multiple launches of the same stub installer in
   ; the same location on the file system. This intentionally won't handle the
   ; case where someone runs multiple copies of the stub on the file system but
   ; it does handle the important case which is a user launching the same stub
   ; multiple times.
   StrCpy $1 "$EXEPATH"
   ; Backslashes are illegal in a mutex name so replace all occurences of a
   ; backslash with a forward slash.
--- a/browser/installer/windows/nsis/uninstaller.nsi
+++ b/browser/installer/windows/nsis/uninstaller.nsi
@@ -680,16 +680,20 @@ Function un.onInit
   ; Remove the current exe directory from the search order.
   ; This only effects LoadLibrary calls and not implicitly loaded DLLs.
   System::Call 'kernel32::SetDllDirectoryW(w "")'
 
   StrCpy $LANGUAGE 0
 
   ${un.UninstallUnOnInitCommon}
 
+  ${If} ${AtLeastWinVista}
+    System::Call 'user32::SetProcessDPIAware()'
+  ${EndIf}
+
   !insertmacro InitInstallOptionsFile "unconfirm.ini"
 FunctionEnd
 
 Function .onGUIEnd
   ${OnEndCommon}
 FunctionEnd
 
 Function un.onGUIEnd
--- a/content/base/src/CSPUtils.jsm
+++ b/content/base/src/CSPUtils.jsm
@@ -381,42 +381,27 @@ CSPRep.fromString = function(aStr, self,
       for (let i in uriStrings) {
         var uri = null;
         try {
           // Relative URIs are okay, but to ensure we send the reports to the
           // right spot, the relative URIs are expanded here during parsing.
           // The resulting CSPRep instance will have only absolute URIs.
           uri = gIoService.newURI(uriStrings[i],null,selfUri);
 
-          // if there's no host, don't do the ETLD+ check.  This will throw
-          // NS_ERROR_FAILURE if the URI doesn't have a host, causing a parse
-          // failure.
+          // if there's no host, this will throw NS_ERROR_FAILURE, causing a
+          // parse failure.
           uri.host;
 
-          // Verify that each report URI is in the same etld + 1 and that the
-          // scheme and port match "self" if "self" is defined, and just that
-          // it's valid otherwise.
-          if (self) {
-            if (gETLDService.getBaseDomain(uri) !==
-                gETLDService.getBaseDomain(selfUri)) {
-              cspWarn(aCSPR,
-                      CSPLocalizer.getFormatStr("reportURInotETLDPlus1",
-                                                [gETLDService.getBaseDomain(uri)]));
-              continue;
-            }
-            if (!uri.schemeIs(selfUri.scheme)) {
-              cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotSameSchemeAsSelf",
-                                                       [uri.asciiSpec]));
-              continue;
-            }
-            if (uri.port && uri.port !== selfUri.port) {
-              cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotSamePortAsSelf",
-                                                       [uri.asciiSpec]));
-              continue;
-            }
+          // warn about, but do not prohibit non-http and non-https schemes for
+          // reporting URIs.  The spec allows unrestricted URIs resolved
+          // relative to "self", but we should let devs know if the scheme is
+          // abnormal and may fail a POST.
+          if (!uri.schemeIs("http") && !uri.schemeIs("https")) {
+            cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotHttpsOrHttp",
+                                                     [uri.asciiSpec]));
           }
         } catch(e) {
           switch (e.result) {
             case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS:
             case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS:
               if (uri.host !== selfUri.host) {
                 cspWarn(aCSPR,
                         CSPLocalizer.getFormatStr("pageCannotSendReportsTo",
@@ -426,17 +411,17 @@ CSPRep.fromString = function(aStr, self,
               break;
 
             default:
               cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotParseReportURI",
                                                        [uriStrings[i]]));
               continue;
           }
         }
-        // all verification passed: same ETLD+1, scheme, and port.
+        // all verification passed
         okUriStrings.push(uri.asciiSpec);
       }
       aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' ');
       continue directive;
     }
 
     // POLICY URI //////////////////////////////////////////////////////////
     if (dirname === UD.POLICY_URI) {
@@ -644,44 +629,27 @@ CSPRep.fromStringSpecCompliant = functio
       for (let i in uriStrings) {
         var uri = null;
         try {
           // Relative URIs are okay, but to ensure we send the reports to the
           // right spot, the relative URIs are expanded here during parsing.
           // The resulting CSPRep instance will have only absolute URIs.
           uri = gIoService.newURI(uriStrings[i],null,selfUri);
 
-          // if there's no host, don't do the ETLD+ check.  This will throw
-          // NS_ERROR_FAILURE if the URI doesn't have a host, causing a parse
-          // failure.
+          // if there's no host, this will throw NS_ERROR_FAILURE, causing a
+          // parse failure.
           uri.host;
 
-          // Verify that each report URI is in the same etld + 1 and that the
-          // scheme and port match "self" if "self" is defined, and just that
-          // it's valid otherwise.
-          if (self) {
-            if (gETLDService.getBaseDomain(uri) !==
-                gETLDService.getBaseDomain(selfUri)) {
-              cspWarn(aCSPR, 
-                      CSPLocalizer.getFormatStr("reportURInotETLDPlus1",
-                                                [gETLDService.getBaseDomain(uri)]));
-              continue;
-            }
-            if (!uri.schemeIs(selfUri.scheme)) {
-              cspWarn(aCSPR,
-                      CSPLocalizer.getFormatStr("reportURInotSameSchemeAsSelf",
-                                                [uri.asciiSpec]));
-              continue;
-            }
-            if (uri.port && uri.port !== selfUri.port) {
-              cspWarn(aCSPR,
-                      CSPLocalizer.getFormatStr("reportURInotSamePortAsSelf",
-                                                [uri.asciiSpec]));
-              continue;
-            }
+          // warn about, but do not prohibit non-http and non-https schemes for
+          // reporting URIs.  The spec allows unrestricted URIs resolved
+          // relative to "self", but we should let devs know if the scheme is
+          // abnormal and may fail a POST.
+          if (!uri.schemeIs("http") && !uri.schemeIs("https")) {
+            cspWarn(aCSPR, CSPLocalizer.getFormatStr("reportURInotHttpsOrHttp",
+                                                     [uri.asciiSpec]));
           }
         } catch(e) {
           switch (e.result) {
             case Components.results.NS_ERROR_INSUFFICIENT_DOMAIN_LEVELS:
             case Components.results.NS_ERROR_HOST_IS_IP_ADDRESS:
               if (uri.host !== selfUri.host) {
                 cspWarn(aCSPR, CSPLocalizer.getFormatStr("pageCannotSendReportsTo",
                                                          [selfUri.host, uri.host]));
@@ -690,17 +658,17 @@ CSPRep.fromStringSpecCompliant = functio
               break;
 
             default:
               cspWarn(aCSPR, CSPLocalizer.getFormatStr("couldNotParseReportURI", 
                                                        [uriStrings[i]]));
               continue;
           }
         }
-        // all verification passed: same ETLD+1, scheme, and port.
+        // all verification passed.
        okUriStrings.push(uri.asciiSpec);
       }
       aCSPR._directives[UD.REPORT_URI] = okUriStrings.join(' ');
       continue directive;
     }
 
     // POLICY URI //////////////////////////////////////////////////////////
     if (dirname === UD.POLICY_URI) {
--- a/content/base/test/unit/test_csputils.js
+++ b/content/base/test/unit/test_csputils.js
@@ -660,71 +660,88 @@ test(function test_FrameAncestor_ignores
       do_check_true(testPermits(URI("http://user1:pass1@self.com/foo"),
                                 URI("http://username:password@self.com/bar")));
       do_check_true(testPermits(URI("http://self.com/foo"),
                                 URI("http://username:password@self.com/bar")));
      });
 
 test(function test_CSP_ReportURI_parsing() {
       var cspr;
-      var SD = CSPRep.SRC_DIRECTIVES_OLD;
+      var SD = CSPRep.SRC_DIRECTIVES_NEW;
       var self = "http://self.com:34";
       var parsedURIs = [];
 
       var uri_valid_absolute = self + "/report.py";
-      var uri_invalid_host_absolute = "http://foo.org:34/report.py";
+      var uri_other_host_absolute = "http://foo.org:34/report.py";
       var uri_valid_relative = "/report.py";
       var uri_valid_relative_expanded = self + uri_valid_relative;
       var uri_valid_relative2 = "foo/bar/report.py";
       var uri_valid_relative2_expanded = self + "/" + uri_valid_relative2;
       var uri_invalid_relative = "javascript:alert(1)";
+      var uri_other_scheme_absolute = "https://self.com/report.py";
+      var uri_other_scheme_and_host_absolute = "https://foo.com/report.py";
 
-      cspr = CSPRep.fromString("allow *; report-uri " + uri_valid_absolute, URI(self));
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_valid_absolute, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
       do_check_in_array(parsedURIs, uri_valid_absolute);
       do_check_eq(parsedURIs.length, 1);
 
-      cspr = CSPRep.fromString("allow *; report-uri " + uri_invalid_host_absolute, URI(self));
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_other_host_absolute, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
-      do_check_in_array(parsedURIs, "");
+      do_check_in_array(parsedURIs, uri_other_host_absolute);
       do_check_eq(parsedURIs.length, 1); // the empty string is in there.
 
-      cspr = CSPRep.fromString("allow *; report-uri " + uri_invalid_relative, URI(self));
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_invalid_relative, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
       do_check_in_array(parsedURIs, "");
       do_check_eq(parsedURIs.length, 1);
 
-      cspr = CSPRep.fromString("allow *; report-uri " + uri_valid_relative, URI(self));
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_valid_relative, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
       do_check_in_array(parsedURIs, uri_valid_relative_expanded);
       do_check_eq(parsedURIs.length, 1);
 
-      cspr = CSPRep.fromString("allow *; report-uri " + uri_valid_relative2, URI(self));
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_valid_relative2, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
       dump(parsedURIs.length);
       do_check_in_array(parsedURIs, uri_valid_relative2_expanded);
       do_check_eq(parsedURIs.length, 1);
 
+      // make sure cross-scheme reporting works
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_other_scheme_absolute, URI(self));
+      parsedURIs = cspr.getReportURIs().split(/\s+/);
+      dump(parsedURIs.length);
+      do_check_in_array(parsedURIs, uri_other_scheme_absolute);
+      do_check_eq(parsedURIs.length, 1);
+
+      // make sure cross-scheme, cross-host reporting works
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " + uri_other_scheme_and_host_absolute, URI(self));
+      parsedURIs = cspr.getReportURIs().split(/\s+/);
+      dump(parsedURIs.length);
+      do_check_in_array(parsedURIs, uri_other_scheme_and_host_absolute);
+      do_check_eq(parsedURIs.length, 1);
+
       // combination!
-      cspr = CSPRep.fromString("allow *; report-uri " +
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " +
                                uri_valid_relative2 + " " +
                                uri_valid_absolute, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
       do_check_in_array(parsedURIs, uri_valid_relative2_expanded);
       do_check_in_array(parsedURIs, uri_valid_absolute);
       do_check_eq(parsedURIs.length, 2);
 
-      cspr = CSPRep.fromString("allow *; report-uri " +
+      cspr = CSPRep.fromStringSpecCompliant("default-src *; report-uri " +
                                uri_valid_relative2 + " " +
-                               uri_invalid_host_absolute + " " +
+                               uri_other_host_absolute + " " +
                                uri_valid_absolute, URI(self));
       parsedURIs = cspr.getReportURIs().split(/\s+/);
       do_check_in_array(parsedURIs, uri_valid_relative2_expanded);
+      do_check_in_array(parsedURIs, uri_other_host_absolute);
       do_check_in_array(parsedURIs, uri_valid_absolute);
-      do_check_eq(parsedURIs.length, 2);
+      do_check_eq(parsedURIs.length, 3);
     });
 
 test(
      function test_bug634778_duplicateDirective_Detection() {
       var cspr;
       var SD = CSPRep.SRC_DIRECTIVES_OLD;
       var self = "http://self.com:34";
       var firstDomain = "http://first.com";
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -1138,20 +1138,22 @@ Navigator::RequestWakeLock(const nsAStri
     aRv.Throw(NS_ERROR_UNEXPECTED);
     return nullptr;
   }
 
   nsRefPtr<power::PowerManagerService> pmService =
     power::PowerManagerService::GetInstance();
   // Maybe it went away for some reason... Or maybe we're just called
   // from our XPCOM method.
-  NS_ENSURE_TRUE(pmService, nullptr);
+  if (!pmService) {
+    aRv.Throw(NS_ERROR_UNEXPECTED);
+    return nullptr;
+  }
 
-  ErrorResult rv;
-  return pmService->NewWakeLock(aTopic, mWindow, rv);
+  return pmService->NewWakeLock(aTopic, mWindow, aRv);
 }
 
 nsIDOMMozMobileMessageManager*
 Navigator::GetMozMobileMessage()
 {
   if (!mMobileMessageManager) {
     // Check that our window has not gone away
     NS_ENSURE_TRUE(mWindow, nullptr);
--- a/dom/base/nsDOMClassInfo.cpp
+++ b/dom/base/nsDOMClassInfo.cpp
@@ -2776,17 +2776,18 @@ ResolvePrototype(nsIXPConnect *aXPConnec
       JSAutoCompartment ac(cx, winobj);
 
       JS::Rooted<JS::Value> val(cx);
       if (!JS_LookupProperty(cx, winobj, CutPrefix(class_parent_name), &val)) {
         return NS_ERROR_UNEXPECTED;
       }
 
       if (val.isObject()) {
-        if (!JS_LookupProperty(cx, &val.toObject(), "prototype", &val)) {
+        JS::Rooted<JSObject*> obj(cx, &val.toObject());
+        if (!JS_LookupProperty(cx, obj, "prototype", &val)) {
           return NS_ERROR_UNEXPECTED;
         }
 
         if (val.isObject()) {
           proto = &val.toObject();
         }
       }
     }
--- a/dom/bindings/Codegen.py
+++ b/dom/bindings/Codegen.py
@@ -6055,17 +6055,17 @@ class CGEnumerateHook(CGAbstractBindingM
                 "}\n"
                 "return true;"))
 
 class CppKeywords():
     """
     A class for checking if method names declared in webidl
     are not in conflict with C++ keywords.
     """
-    keywords = frozenset(['alignas', 'alignof', 'and', 'and_eq', 'asm', 'auto', 'bitand', 'bitor', 'bool',
+    keywords = frozenset(['alignas', 'alignof', 'and', 'and_eq', 'asm', 'assert', 'auto', 'bitand', 'bitor', 'bool',
     'break', 'case', 'catch', 'char', 'char16_t', 'char32_t', 'class', 'compl', 'const', 'constexpr',
     'const_cast', 'continue', 'decltype', 'default', 'delete', 'do', 'double', 'dynamic_cast', 'else', 'enum',
     'explicit', 'export', 'extern', 'false', 'final', 'float', 'for', 'friend', 'goto', 'if', 'inline',
     'int', 'long', 'mutable', 'namespace', 'new', 'noexcept', 'not', 'not_eq', 'nullptr', 'operator',
     'or', 'or_eq', 'override', 'private', 'protected', 'public', 'register', 'reinterpret_cast', 'return',
     'short', 'signed', 'sizeof', 'static', 'static_assert', 'static_cast', 'struct', 'switch', 'template',
     'this', 'thread_local', 'throw', 'true', 'try', 'typedef', 'typeid', 'typename', 'union', 'unsigned',
     'using', 'virtual', 'void', 'volatile', 'wchar_t', 'while', 'xor', 'xor_eq'])
--- a/dom/events/nsEventStateManager.cpp
+++ b/dom/events/nsEventStateManager.cpp
@@ -2159,16 +2159,25 @@ nsEventStateManager::GenerateDragGesture
       WidgetDragEvent* event = &startEvent;
       if (status != nsEventStatus_eConsumeNoDefault) {
         status = nsEventStatus_eIgnore;
         nsEventDispatcher::Dispatch(targetContent, aPresContext, &gestureEvent, nullptr,
                                     &status);
         event = &gestureEvent;
       }
 
+      nsCOMPtr<nsIObserverService> observerService =
+        mozilla::services::GetObserverService();
+      // Emit observer event to allow addons to modify the DataTransfer object.
+      if (observerService) {
+        observerService->NotifyObservers(dataTransfer,
+                                         "on-datatransfer-available",
+                                         nullptr);
+      }
+
       // now that the dataTransfer has been updated in the dragstart and
       // draggesture events, make it read only so that the data doesn't
       // change during the drag.
       dataTransfer->SetReadOnly();
 
       if (status != nsEventStatus_eConsumeNoDefault) {
         bool dragStarted = DoDefaultDragStart(aPresContext, event, dataTransfer,
                                               targetContent, selection);
--- a/dom/locales/en-US/chrome/security/csp.properties
+++ b/dom/locales/en-US/chrome/security/csp.properties
@@ -20,23 +20,19 @@ errorWas = error was: "%1$S"
 # %1$S is the report URI that could not be parsed
 couldNotParseReportURI = couldn't parse report URI: %1$S
 # LOCALIZATION NOTE (couldNotProcessUnknownDirective):
 # %1$S is the unknown directive
 couldNotProcessUnknownDirective = Couldn't process unknown directive '%1$S'
 # LOCALIZATION NOTE (ignoringUnknownOption):
 # %1$S is the option that could not be understood
 ignoringUnknownOption = Ignoring unknown option %1$S
-# LOCALIZATION NOTE (reportURInotETLDPlus1):
-# %1$S is the ETLD of the report URI that could not be used
-reportURInotETLDPlus1 = The report URI (%1$S) must be from the same eTLD+1.
-# LOCALIZATION NOTE (reportURInotSameSchemeAsSelf, reportURInotSamePortAsSelf):
-# %1$S is the report URI that could not be used
-reportURInotSameSchemeAsSelf = The report URI (%1$S) must use the same scheme as the originating document.
-reportURInotSamePortAsSelf = The report URI (%1$S) must use the same port as the originating document.
+# LOCALIZATION NOTE (reportURInotHttpsOrHttp):
+# %1$S is the ETLD of the report URI that is not HTTP or HTTPS
+reportURInotHttpsOrHttp = The report URI (%1$) should be an HTTP or HTTPS URI.
 # LOCALIZATION NOTE (pageCannotSendReportsTo):
 # %1$S is the URI of the page with the policy
 # %2$S is the report URI that could not be used
 pageCannotSendReportsTo = page on %1$S cannot send reports to %2$S
 allowOrDefaultSrcRequired = 'allow' or 'default-src' directive required but not present.  Reverting to "default-src 'none'"
 # LOCALIZATION NOTE (failedToParseUnrecognizedSource):
 # %1$S is the CSP Source that could not be parsed
 failedToParseUnrecognizedSource = Failed to parse unrecognized source %1$S
--- a/dom/media/tests/mochitest/pc.js
+++ b/dom/media/tests/mochitest/pc.js
@@ -1560,23 +1560,27 @@ PeerConnectionWrapper.prototype = {
       return n;
     }
 
     // Use spec way of enumerating stats
     var counters = {};
     for (var key in stats) {
       if (stats.hasOwnProperty(key)) {
         var res = stats[key];
-        counters[res.type] = toNum(counters[res.type]) + 1;
+        if (!res.isRemote) {
+          counters[res.type] = toNum(counters[res.type]) + 1;
+        }
       }
     }
     // Use MapClass way of enumerating stats
     var counters2 = {};
     stats.forEach(function(res) {
-        counters2[res.type] = toNum(counters2[res.type]) + 1;
+        if (!res.isRemote) {
+          counters2[res.type] = toNum(counters2[res.type]) + 1;
+        }
       });
     is(JSON.stringify(counters), JSON.stringify(counters2),
        "Spec and MapClass variant of RTCStatsReport enumeration agree");
     var nin = numTracks(this._pc.getRemoteStreams());
     var nout = numTracks(this._pc.getLocalStreams());
 
     // TODO(Bug 957145): Restore stronger inboundrtp test once Bug 948249 is fixed
     //is(toNum(counters["inboundrtp"]), nin, "Have " + nin + " inboundrtp stat(s)");
--- a/dom/power/test/mochitest.ini
+++ b/dom/power/test/mochitest.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 
+[test_bug957893.html]
 [test_bug957899.html]
 [test_power_basics.html]
 [test_power_set_cpusleepallowed.html]
 [test_power_set_screen_brightness.html]
 [test_power_set_screen_enabled.html]
new file mode 100644
--- /dev/null
+++ b/dom/power/test/test_bug957893.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test bug 957893 - Crash in WakeLock</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+ try {
+   var wl = navigator.requestWakeLock('');
+   ok(false, "RequestWakeLock throws an exception!");
+ } catch(e) {
+   ok(true, "RequestWakeLock throws an exception!");
+ }
+
+ info("Still alive!");
+
+ </script>
+</body>
+</html>
--- a/dom/system/NetworkGeolocationProvider.js
+++ b/dom/system/NetworkGeolocationProvider.js
@@ -145,19 +145,19 @@ WifiGeoPositionProvider.prototype = {
 
     Cc["@mozilla.org/geolocation/service;1"].getService(Ci.nsIGeolocationUpdate)
         .locationUpdatePending();
 
     let url = Services.urlFormatter.formatURLPref("geo.wifi.uri");
 
     function isPublic(ap) {
         let mask = "_nomap"
-        let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length) == -1;
+        let result = ap.ssid.indexOf(mask, ap.ssid.length - mask.length);
         if (result != -1) {
-            LOG("Filtering out " + ap.ssid);
+            LOG("Filtering out " + ap.ssid + " " + result);
         }
         return result;
     };
 
     function sort(a, b) {
       return b.signal - a.signal;
     };
 
--- a/gfx/layers/ImageContainer.h
+++ b/gfx/layers/ImageContainer.h
@@ -955,18 +955,16 @@ public:
     nsRefPtr<gfxASurface> surface = mDeprecatedSurface.get();
     return surface.forget();
   }
 
   gfx::IntSize GetSize() { return mSize; }
 
   CairoImage() : Image(nullptr, CAIRO_SURFACE) {}
 
-private:
-
   nsCountedRef<nsMainThreadSurfaceRef> mDeprecatedSurface;
   gfx::IntSize mSize;
 
   // mSourceSurface wraps mDeprrecatedSurface's data, therefore it should not
   // outlive mDeprecatedSurface
   nsCountedRef<nsMainThreadSourceSurfaceRef> mSourceSurface;
 };
 
--- a/gfx/layers/client/ContentClient.cpp
+++ b/gfx/layers/client/ContentClient.cpp
@@ -143,17 +143,19 @@ ContentClientRemoteBuffer::BeginPaint()
 void
 ContentClientRemoteBuffer::EndPaint()
 {
   // XXX: We might still not have a texture client if PaintThebes
   // decided we didn't need one yet because the region to draw was empty.
   SetBufferProvider(nullptr);
   SetBufferProviderOnWhite(nullptr);
   for (unsigned i = 0; i< mOldTextures.Length(); ++i) {
-    mOldTextures[i]->Unlock();
+    if (mOldTextures[i]->IsLocked()) {
+      mOldTextures[i]->Unlock();
+    }
   }
   mOldTextures.Clear();
 
   if (mTextureClient) {
     mTextureClient->Unlock();
   }
   if (mTextureClientOnWhite) {
     mTextureClientOnWhite->Unlock();
@@ -552,16 +554,25 @@ ContentClientDoubleBuffered::SwapBuffers
   ContentClientRemoteBuffer::SwapBuffers(aFrontUpdatedRegion);
 }
 
 void
 ContentClientDoubleBuffered::PrepareFrame()
 {
   mIsNewBuffer = false;
 
+  if (mTextureClient) {
+    DebugOnly<bool> locked = mTextureClient->Lock(OPEN_READ_WRITE);
+    MOZ_ASSERT(locked);
+  }
+  if (mTextureClientOnWhite) {
+    DebugOnly<bool> locked = mTextureClientOnWhite->Lock(OPEN_READ_WRITE);
+    MOZ_ASSERT(locked);
+  }
+
   if (!mFrontAndBackBufferDiffer) {
     return;
   }
 
   if (mDidSelfCopy) {
     // We can't easily draw our front buffer into us, since we're going to be
     // copying stuff around anyway it's easiest if we just move our situation
     // to non-rotated while we're at it. If this situation occurs we'll have
--- a/gfx/layers/client/TextureClient.cpp
+++ b/gfx/layers/client/TextureClient.cpp
@@ -559,17 +559,17 @@ BufferTextureClient::GetAsDrawTarget()
 
 bool
 BufferTextureClient::Lock(OpenMode aMode)
 {
   // XXX - Turn this into a fatal assertion as soon as Bug 952507 is fixed
   NS_WARN_IF_FALSE(!mLocked, "The TextureClient is already Locked!");
   mOpenMode = aMode;
   mLocked = true;
-  return true;
+  return IsValid() && IsAllocated();
 }
 
 void
 BufferTextureClient::Unlock()
 {
   // XXX - Turn this into a fatal assertion as soon as Bug 952507 is fixed
   NS_WARN_IF_FALSE(mLocked, "The TextureClient is already Unlocked!");
   mLocked = false;
--- a/gfx/layers/client/TextureClient.h
+++ b/gfx/layers/client/TextureClient.h
@@ -167,16 +167,18 @@ public:
    *
    * Please always lock/unlock when accessing the shared data.
    * If Lock() returns false, you should not attempt to access the shared data.
    */
   virtual bool Lock(OpenMode aMode) { return IsValid(); }
 
   virtual void Unlock() {}
 
+  virtual bool IsLocked() const = 0;
+
   /**
    * Returns true if this texture has a lock/unlock mechanism.
    * Textures that do not implement locking should be immutable or should
    * use immediate uploads (see TextureFlags in CompositorTypes.h)
    */
   virtual bool ImplementsLocking() const { return false; }
 
   /**
@@ -308,16 +310,18 @@ public:
   virtual uint8_t* GetBuffer() const = 0;
 
   virtual gfx::IntSize GetSize() const { return mSize; }
 
   virtual bool Lock(OpenMode aMode) MOZ_OVERRIDE;
 
   virtual void Unlock() MOZ_OVERRIDE;
 
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mLocked; }
+
   // TextureClientSurface
 
   virtual TextureClientSurface* AsTextureClientSurface() MOZ_OVERRIDE { return this; }
 
   virtual bool UpdateSurface(gfxASurface* aSurface) MOZ_OVERRIDE;
 
   virtual already_AddRefed<gfxASurface> GetAsSurface() MOZ_OVERRIDE;
 
--- a/gfx/layers/d3d10/ImageLayerD3D10.cpp
+++ b/gfx/layers/d3d10/ImageLayerD3D10.cpp
@@ -208,24 +208,24 @@ ImageLayerD3D10::RenderLayer()
   }
 
   IntSize size = image->GetSize();
 
   SetEffectTransformAndOpacity();
 
   ID3D10EffectTechnique *technique;
   nsRefPtr<IDXGIKeyedMutex> keyedMutex;
-  nsRefPtr<gfxASurface> surf = image->DeprecatedGetAsSurface();
 
   if (image->GetFormat() == ImageFormat::CAIRO_SURFACE ||
       image->GetFormat() == ImageFormat::REMOTE_IMAGE_BITMAP ||
       image->GetFormat() == ImageFormat::REMOTE_IMAGE_DXGI_TEXTURE ||
       image->GetFormat() == ImageFormat::D3D9_RGB32_TEXTURE) {
     NS_ASSERTION(image->GetFormat() != ImageFormat::CAIRO_SURFACE ||
-                 !surf || surf->GetContentType() != gfxContentType::ALPHA,
+                 !static_cast<CairoImage*>(image)->mDeprecatedSurface ||
+                 static_cast<CairoImage*>(image)->mDeprecatedSurface->GetContentType() != gfxContentType::ALPHA,
                  "Image layer has alpha image");
     bool hasAlpha = false;
 
     nsRefPtr<ID3D10ShaderResourceView> srView = GetImageSRView(image, hasAlpha, getter_AddRefs(keyedMutex));
     if (!srView) {
       return;
     }
 
--- a/gfx/layers/d3d11/TextureD3D11.cpp
+++ b/gfx/layers/d3d11/TextureD3D11.cpp
@@ -149,16 +149,19 @@ TextureClientD3D11::TextureClientD3D11(g
 {}
 
 TextureClientD3D11::~TextureClientD3D11()
 {}
 
 bool
 TextureClientD3D11::Lock(OpenMode aMode)
 {
+  if (!IsValid() || !IsAllocated()) {
+    return false;
+  }
   MOZ_ASSERT(!mIsLocked, "The Texture is already locked!");
   LockD3DTexture(mTexture.get());
   mIsLocked = true;
   return true;
 }
 
 void
 TextureClientD3D11::Unlock()
--- a/gfx/layers/d3d11/TextureD3D11.h
+++ b/gfx/layers/d3d11/TextureD3D11.h
@@ -36,16 +36,18 @@ public:
   // TextureClient
 
   virtual bool IsAllocated() const MOZ_OVERRIDE { return !!mTexture; }
 
   virtual bool Lock(OpenMode aOpenMode) MOZ_OVERRIDE;
 
   virtual void Unlock() MOZ_OVERRIDE;
 
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mIsLocked; }
+
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
   virtual gfx::IntSize GetSize() const MOZ_OVERRIDE { return mSize; }
 
   virtual TextureClientData* DropTextureData() MOZ_OVERRIDE { return nullptr; }
 
   // TextureClientDrawTarget
 
--- a/gfx/layers/d3d9/ImageLayerD3D9.cpp
+++ b/gfx/layers/d3d9/ImageLayerD3D9.cpp
@@ -409,19 +409,19 @@ ImageLayerD3D9::RenderLayer()
   SetShaderTransformAndOpacity();
 
   gfx::IntSize size = image->GetSize();
 
   if (image->GetFormat() == CAIRO_SURFACE ||
       image->GetFormat() == REMOTE_IMAGE_BITMAP ||
       image->GetFormat() == D3D9_RGB32_TEXTURE)
   {
-    nsRefPtr<gfxASurface> surf = image->DeprecatedGetAsSurface();
     NS_ASSERTION(image->GetFormat() != CAIRO_SURFACE ||
-                 !surf || surf->GetContentType() != gfxContentType::ALPHA,
+                 !static_cast<CairoImage*>(image)->mDeprecatedSurface ||
+                 static_cast<CairoImage*>(image)->mDeprecatedSurface->GetContentType() != gfxContentType::ALPHA,
                  "Image layer has alpha image");
 
     bool hasAlpha = false;
     nsRefPtr<IDirect3DTexture9> texture = GetTexture(image, hasAlpha);
 
     device()->SetVertexShaderConstantF(CBvLayerQuad,
                                        ShaderConstantRect(0,
                                                           0,
--- a/gfx/layers/d3d9/TextureD3D9.cpp
+++ b/gfx/layers/d3d9/TextureD3D9.cpp
@@ -1261,17 +1261,17 @@ CairoTextureClientD3D9::~CairoTextureCli
 {
   MOZ_COUNT_DTOR(CairoTextureClientD3D9);
 }
 
 bool
 CairoTextureClientD3D9::Lock(OpenMode)
 {
   MOZ_ASSERT(!mIsLocked);
-  if (!IsValid()) {
+  if (!IsValid() || !IsAllocated()) {
     return false;
   }
   mIsLocked = true;
   return true;
 }
 
 void
 CairoTextureClientD3D9::Unlock()
--- a/gfx/layers/d3d9/TextureD3D9.h
+++ b/gfx/layers/d3d9/TextureD3D9.h
@@ -189,16 +189,18 @@ public:
   // TextureClient
 
   virtual bool IsAllocated() const MOZ_OVERRIDE { return !!mTexture; }
 
   virtual bool Lock(OpenMode aOpenMode) MOZ_OVERRIDE;
 
   virtual void Unlock() MOZ_OVERRIDE;
 
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mIsLocked; }
+
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
   virtual gfx::IntSize GetSize() const { return mSize; }
 
   virtual gfx::SurfaceFormat GetFormat() const { return mFormat; }
 
   virtual TextureClientData* DropTextureData() MOZ_OVERRIDE;
 
@@ -237,16 +239,18 @@ public:
   // TextureClient
 
   virtual bool IsAllocated() const MOZ_OVERRIDE { return !!mSurface; }
 
   virtual bool Lock(OpenMode aOpenMode) MOZ_OVERRIDE;
 
   virtual void Unlock() MOZ_OVERRIDE;
 
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mIsLocked; }
+
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
   virtual gfx::IntSize GetSize() const { return mSize; }
 
   virtual gfx::SurfaceFormat GetFormat() const { return mFormat; }
 
   virtual TextureClientData* DropTextureData() MOZ_OVERRIDE;
 
@@ -282,16 +286,18 @@ public:
   // TextureClient
 
   virtual bool IsAllocated() const MOZ_OVERRIDE { return !!mTexture; }
 
   virtual bool Lock(OpenMode aOpenMode) MOZ_OVERRIDE;
 
   virtual void Unlock() MOZ_OVERRIDE;
 
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mIsLocked; }
+
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
   void InitWith(IDirect3DTexture9* aTexture, HANDLE aSharedHandle, D3DSURFACE_DESC aDesc)
   {
     MOZ_ASSERT(!mTexture);
     mTexture = aTexture;
     mHandle = aSharedHandle;
     mDesc = aDesc;
--- a/gfx/layers/ipc/CompositorChild.h
+++ b/gfx/layers/ipc/CompositorChild.h
@@ -95,17 +95,16 @@ private:
     // the shared FrameMetrics
     mozilla::ipc::SharedMemoryBasic* mBuffer;
     CrossProcessMutex* mMutex;
     // Unique ID of the APZC that is sharing the FrameMetrics
     uint32_t mAPZCId;
   };
 
   nsRefPtr<ClientLayerManager> mLayerManager;
-  nsCOMPtr<nsIObserver> mMemoryPressureObserver;
 
   // The ViewID of the FrameMetrics is used as the key for this hash table.
   // While this should be safe to use since the ViewID is unique
   nsClassHashtable<nsUint64HashKey, SharedFrameMetricsData> mFrameMetricsTable;
 
   // When we're in a child process, this is the process-global
   // compositor that we use to forward transactions directly to the
   // compositor context in another process.
--- a/gfx/layers/opengl/GrallocTextureClient.cpp
+++ b/gfx/layers/opengl/GrallocTextureClient.cpp
@@ -169,16 +169,19 @@ GrallocTextureClientOGL::ToSurfaceDescri
   aOutDescriptor = NewSurfaceDescriptorGralloc(nullptr, mGrallocActor, mSize);
   return true;
 }
 
 bool
 GrallocTextureClientOGL::Lock(OpenMode aMode)
 {
   MOZ_ASSERT(IsValid());
+  if (!IsValid() || !IsAllocated()) {
+    return false;
+  }
   // XXX- it would be cleaner to take the openMode into account or to check
   // that aMode is coherent with mGrallocFlags (which carries more information
   // than OpenMode).
   int32_t rv = mGraphicBuffer->lock(mGrallocFlags, reinterpret_cast<void**>(&mMappedBuffer));
   if (rv) {
     NS_WARNING("Couldn't lock graphic buffer");
     return false;
   }
--- a/gfx/layers/opengl/GrallocTextureHost.cpp
+++ b/gfx/layers/opengl/GrallocTextureHost.cpp
@@ -117,16 +117,20 @@ void GrallocTextureSourceOGL::BindTextur
   MOZ_ASSERT(gl());
   gl()->MakeCurrent();
 
   GLuint tex = GetGLTexture();
   GLuint textureTarget = GetTextureTarget();
 
   gl()->fActiveTexture(aTextureUnit);
   gl()->fBindTexture(textureTarget, tex);
+  if (!mEGLImage) {
+    mEGLImage = EGLImageCreateFromNativeBuffer(gl(), mGraphicBuffer->getNativeBuffer());
+  }
+  gl()->fEGLImageTargetTexture2D(textureTarget, mEGLImage);
   gl()->fActiveTexture(LOCAL_GL_TEXTURE0);
 }
 
 bool
 GrallocTextureSourceOGL::IsValid() const
 {
   return !!gl() && !!mGraphicBuffer.get();
 }
@@ -168,16 +172,23 @@ GrallocTextureSourceOGL::GetFormat() con
 void
 GrallocTextureSourceOGL::SetCompositableBackendSpecificData(CompositableBackendSpecificData* aBackendData)
 {
   if (mCompositableBackendData != aBackendData) {
     mNeedsReset = true;
   }
 
   if (!mNeedsReset) {
+    // Update binding to the EGLImage
+    gl()->MakeCurrent();
+    GLuint tex = GetGLTexture();
+    GLuint textureTarget = GetTextureTarget();
+    gl()->fActiveTexture(LOCAL_GL_TEXTURE0);
+    gl()->fBindTexture(textureTarget, tex);
+    gl()->fEGLImageTargetTexture2D(textureTarget, mEGLImage);
     return;
   }
 
   mCompositableBackendData = aBackendData;
 
   if (!mCompositor) {
     return;
   }
--- a/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.cpp
+++ b/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.cpp
@@ -6,30 +6,52 @@
 #include "MacIOSurfaceTextureClientOGL.h"
 #include "mozilla/gfx/MacIOSurface.h"
 
 namespace mozilla {
 namespace layers {
 
 MacIOSurfaceTextureClientOGL::MacIOSurfaceTextureClientOGL(TextureFlags aFlags)
   : TextureClient(aFlags)
+  , mIsLocked(false)
 {}
 
 MacIOSurfaceTextureClientOGL::~MacIOSurfaceTextureClientOGL()
 {}
 
 void
 MacIOSurfaceTextureClientOGL::InitWith(MacIOSurface* aSurface)
 {
   MOZ_ASSERT(IsValid());
   MOZ_ASSERT(!IsAllocated());
   mSurface = aSurface;
 }
 
 bool
+MacIOSurfaceTextureClientOGL::Lock(OpenMode aMode)
+{
+  MOZ_ASSERT(!mIsLocked);
+  mIsLocked = true;
+  return IsValid() && IsAllocated();
+}
+
+void
+MacIOSurfaceTextureClientOGL::Unlock()
+{
+  MOZ_ASSERT(mIsLocked);
+  mIsLocked = false;
+}
+
+bool
+MacIOSurfaceTextureClientOGL::IsLocked() const
+{
+  return mIsLocked;
+}
+
+bool
 MacIOSurfaceTextureClientOGL::ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor)
 {
   MOZ_ASSERT(IsValid());
   if (!IsAllocated()) {
     return false;
   }
   aOutDescriptor = SurfaceDescriptorMacIOSurface(mSurface->GetIOSurfaceID(),
                                                  mSurface->GetContentsScaleFactor(),
--- a/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.h
+++ b/gfx/layers/opengl/MacIOSurfaceTextureClientOGL.h
@@ -17,24 +17,31 @@ class MacIOSurfaceTextureClientOGL : pub
 {
 public:
   MacIOSurfaceTextureClientOGL(TextureFlags aFlags);
 
   virtual ~MacIOSurfaceTextureClientOGL();
 
   void InitWith(MacIOSurface* aSurface);
 
+  virtual bool Lock(OpenMode aMode) MOZ_OVERRIDE;
+
+  virtual void Unlock() MOZ_OVERRIDE;
+
+  virtual bool IsLocked() const MOZ_OVERRIDE;
+
   virtual bool IsAllocated() const MOZ_OVERRIDE { return !!mSurface; }
 
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
   virtual gfx::IntSize GetSize() const;
 
   virtual TextureClientData* DropTextureData() MOZ_OVERRIDE;
 
 protected:
   RefPtr<MacIOSurface> mSurface;
+  bool mIsLocked;
 };
 
 }
 }
 
 #endif // MOZILLA_GFX_MACIOSURFACETEXTURECLIENTOGL_H
\ No newline at end of file
--- a/gfx/layers/opengl/TextureClientOGL.cpp
+++ b/gfx/layers/opengl/TextureClientOGL.cpp
@@ -56,33 +56,70 @@ SharedTextureClientOGL::InitWith(gl::Sha
   mShareType = aShareType;
   mInverted = aInverted;
   if (mInverted) {
     AddFlags(TEXTURE_NEEDS_Y_FLIP);
   }
 }
 
 bool
+SharedTextureClientOGL::Lock(OpenMode mode)
+{
+  MOZ_ASSERT(!mIsLocked);
+  if (!IsValid() || !IsAllocated()) {
+    return false;
+  }
+  mIsLocked = true;
+  return true;
+}
+
+void
+SharedTextureClientOGL::Unlock()
+{
+  MOZ_ASSERT(mIsLocked);
+  mIsLocked = false;
+}
+
+bool
 SharedTextureClientOGL::IsAllocated() const
 {
   return mHandle != 0;
 }
 
 StreamTextureClientOGL::StreamTextureClientOGL(TextureFlags aFlags)
   : TextureClient(aFlags)
   , mStream(0)
+  , mIsLocked(false)
 {
 }
 
 StreamTextureClientOGL::~StreamTextureClientOGL()
 {
   // the data is owned externally.
 }
 
 bool
+StreamTextureClientOGL::Lock(OpenMode mode)
+{
+  MOZ_ASSERT(!mIsLocked);
+  if (!IsValid() || !IsAllocated()) {
+    return false;
+  }
+  mIsLocked = true;
+  return true;
+}
+
+void
+StreamTextureClientOGL::Unlock()
+{
+  MOZ_ASSERT(mIsLocked);
+  mIsLocked = false;
+}
+
+bool
 StreamTextureClientOGL::ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor)
 {
   if (!IsAllocated()) {
     return false;
   }
 
   gfx::SurfaceStreamHandle handle = mStream->GetShareHandle();
   aOutDescriptor = SurfaceStreamDescriptor(handle, false);
--- a/gfx/layers/opengl/TextureClientOGL.h
+++ b/gfx/layers/opengl/TextureClientOGL.h
@@ -35,16 +35,22 @@ public:
   SharedTextureClientOGL(TextureFlags aFlags);
 
   ~SharedTextureClientOGL();
 
   virtual bool IsAllocated() const MOZ_OVERRIDE;
 
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
+  virtual bool Lock(OpenMode mode) MOZ_OVERRIDE;
+
+  virtual void Unlock() MOZ_OVERRIDE;
+
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mIsLocked; }
+
   void InitWith(gl::SharedTextureHandle aHandle,
                 gfx::IntSize aSize,
                 gl::SharedTextureShareType aShareType,
                 bool aInverted = false);
 
   virtual gfx::IntSize GetSize() const { return mSize; }
 
   virtual TextureClientData* DropTextureData() MOZ_OVERRIDE
@@ -56,40 +62,48 @@ public:
     return nullptr;
   }
 
 protected:
   gl::SharedTextureHandle mHandle;
   gfx::IntSize mSize;
   gl::SharedTextureShareType mShareType;
   bool mInverted;
+  bool mIsLocked;
 };
 
 /**
  * A TextureClient implementation to share SurfaceStream.
  */
 class StreamTextureClientOGL : public TextureClient
 {
 public:
   StreamTextureClientOGL(TextureFlags aFlags);
 
   ~StreamTextureClientOGL();
 
   virtual bool IsAllocated() const MOZ_OVERRIDE;
 
+  virtual bool Lock(OpenMode mode) MOZ_OVERRIDE;
+
+  virtual void Unlock() MOZ_OVERRIDE;
+
+  virtual bool IsLocked() const MOZ_OVERRIDE { return mIsLocked; }
+
   virtual bool ToSurfaceDescriptor(SurfaceDescriptor& aOutDescriptor) MOZ_OVERRIDE;
 
   virtual TextureClientData* DropTextureData() MOZ_OVERRIDE { return nullptr; }
 
   void InitWith(gfx::SurfaceStream* aStream);
 
   virtual gfx::IntSize GetSize() const { return gfx::IntSize(); }
 
 protected:
   gfx::SurfaceStream* mStream;
+  bool mIsLocked;
 };
 
 class DeprecatedTextureClientSharedOGL : public DeprecatedTextureClient
 {
 public:
   DeprecatedTextureClientSharedOGL(CompositableForwarder* aForwarder, const TextureInfo& aTextureInfo);
   ~DeprecatedTextureClientSharedOGL() { ReleaseResources(); }
 
--- a/gfx/layers/opengl/TextureHostOGL.cpp
+++ b/gfx/layers/opengl/TextureHostOGL.cpp
@@ -220,16 +220,17 @@ GLuint CompositableDataGonkOGL::GetTextu
 
 void
 CompositableDataGonkOGL::DeleteTextureIfPresent()
 {
   if (mTexture) {
     if (gl()->MakeCurrent()) {
       gl()->fDeleteTextures(1, &mTexture);
     }
+    mTexture = 0;
   }
 }
 
 bool
 TextureImageTextureSourceOGL::Update(gfx::DataSourceSurface* aSurface,
                                      nsIntRegion* aDestRegion,
                                      gfx::IntPoint* aSrcOffset)
 {
--- a/js/public/HashTable.h
+++ b/js/public/HashTable.h
@@ -154,29 +154,29 @@ class HashMap
         return impl.relookupOrAdd(p, e.key(), mozilla::Move(e));
     }
 
     // |all()| returns a Range containing |count()| elements. E.g.:
     //
     //   typedef HashMap<int,char> HM;
     //   HM h;
     //   for (HM::Range r = h.all(); !r.empty(); r.popFront())
-    //     char c = r.front().value;
+    //     char c = r.front().value();
     //
     // Also see the definition of Range in HashTable above (with T = Entry).
     typedef typename Impl::Range Range;
     Range all() const                                 { return impl.all(); }
 
     // Typedef for the enumeration class. An Enum may be used to examine and
     // remove table entries:
     //
     //   typedef HashMap<int,char> HM;
     //   HM s;
     //   for (HM::Enum e(s); !e.empty(); e.popFront())
-    //     if (e.front().value == 'l')
+    //     if (e.front().value() == 'l')
     //       e.removeFront();
     //
     // Table resize may occur in Enum's destructor. Also see the definition of
     // Enum in HashTable above (with T = Entry).
     typedef typename Impl::Enum Enum;
 
     // Remove all entries. This does not shrink the table. For that consider
     // using the finish() method.
--- a/js/public/MemoryMetrics.h
+++ b/js/public/MemoryMetrics.h
@@ -204,16 +204,41 @@ struct CodeSizes
     {}
 
     FOR_EACH_SIZE(DECL_SIZE)
     int dummy;  // present just to absorb the trailing comma from FOR_EACH_SIZE(ZERO_SIZE)
 
 #undef FOR_EACH_SIZE
 };
 
+// Data for tracking GC memory usage.
+struct GCSizes
+{
+#define FOR_EACH_SIZE(macro) \
+    macro(_, _, marker) \
+    macro(_, _, nursery) \
+    macro(_, _, storeBufferVals) \
+    macro(_, _, storeBufferCells) \
+    macro(_, _, storeBufferSlots) \
+    macro(_, _, storeBufferWholeCells) \
+    macro(_, _, storeBufferRelocVals) \
+    macro(_, _, storeBufferRelocCells) \
+    macro(_, _, storeBufferGenerics)
+
+    GCSizes()
+      : FOR_EACH_SIZE(ZERO_SIZE)
+        dummy()
+    {}
+
+    FOR_EACH_SIZE(DECL_SIZE)
+    int dummy;  // present just to absorb the trailing comma from FOR_EACH_SIZE(ZERO_SIZE)
+
+#undef FOR_EACH_SIZE
+};
+
 // This class holds information about the memory taken up by identical copies of
 // a particular string.  Multiple JSStrings may have their sizes aggregated
 // together into one StringInfo object.  Note that two strings with identical
 // chars will not be aggregated together if one is a short string and the other
 // is not.
 struct StringInfo
 {
     StringInfo()
@@ -289,28 +314,30 @@ struct RuntimeSizes
 #define FOR_EACH_SIZE(macro) \
     macro(_, _, object) \
     macro(_, _, atomsTable) \
     macro(_, _, contexts) \
     macro(_, _, dtoa) \
     macro(_, _, temporary) \
     macro(_, _, regexpData) \
     macro(_, _, interpreterStack) \
-    macro(_, _, gcMarker) \
     macro(_, _, mathCache) \
+    macro(_, _, sourceDataCache) \
     macro(_, _, scriptData) \
     macro(_, _, scriptSources)
 
     RuntimeSizes()
       : FOR_EACH_SIZE(ZERO_SIZE)
-        code()
+        code(),
+        gc()
     {}
 
     FOR_EACH_SIZE(DECL_SIZE)
     CodeSizes code;
+    GCSizes   gc;
 
 #undef FOR_EACH_SIZE
 };
 
 struct ZoneStats : js::ZoneStatsPod
 {
     ZoneStats()
       : strings(nullptr)
--- a/js/src/assembler/wtf/Platform.h
+++ b/js/src/assembler/wtf/Platform.h
@@ -205,16 +205,23 @@
 #endif
 
 /* WTF_CPU_S390 - S390 32-bit */
 #if defined(__s390__)
 #define WTF_CPU_S390 1
 #define WTF_CPU_BIG_ENDIAN 1
 #endif
 
+#if defined(__aarch64__)
+#define WTF_CPU_AARCH64 1
+#if defined(__AARCH64EB__)
+#define WTF_CPU_BIG_ENDIAN 1
+#endif
+#endif
+
 /* WTF_CPU_X86 - i386 / x86 32-bit */
 #if   defined(__i386__) \
     || defined(i386)     \
     || defined(_M_IX86)  \
     || defined(_X86_)    \
     || defined(__THW_INTEL)
 #define WTF_CPU_X86 1
 #endif
--- a/js/src/gc/Nursery.h
+++ b/js/src/gc/Nursery.h
@@ -118,16 +118,18 @@ class Nursery
      * returns false and leaves |*ref| unset.
      */
     template <typename T>
     JS_ALWAYS_INLINE bool getForwardedPointer(T **ref);
 
     /* Forward a slots/elements pointer stored in an Ion frame. */
     void forwardBufferPointer(HeapSlot **pSlotsElems);
 
+    size_t sizeOfHeap() { return start() ? NurserySize : 0; }
+
 #ifdef JS_GC_ZEAL
     /*
      * In debug and zeal builds, these bytes indicate the state of an unused
      * segment of nursery-allocated memory.
      */
     static const uint8_t FreshNursery = 0x2a;
     static const uint8_t SweptNursery = 0x2b;
     static const uint8_t AllocatedThing = 0x2c;
--- a/js/src/gc/StoreBuffer.cpp
+++ b/js/src/gc/StoreBuffer.cpp
@@ -300,16 +300,29 @@ StoreBuffer::setAboutToOverflow()
 }
 
 bool
 StoreBuffer::inParallelSection() const
 {
     return InParallelSection();
 }
 
+void
+StoreBuffer::addSizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf, JS::GCSizes
+*sizes)
+{
+    sizes->storeBufferVals       += bufferVal.sizeOfExcludingThis(mallocSizeOf);
+    sizes->storeBufferCells      += bufferCell.sizeOfExcludingThis(mallocSizeOf);
+    sizes->storeBufferSlots      += bufferSlot.sizeOfExcludingThis(mallocSizeOf);
+    sizes->storeBufferWholeCells += bufferWholeCell.sizeOfExcludingThis(mallocSizeOf);
+    sizes->storeBufferRelocVals  += bufferRelocVal.sizeOfExcludingThis(mallocSizeOf);
+    sizes->storeBufferRelocCells += bufferRelocCell.sizeOfExcludingThis(mallocSizeOf);
+    sizes->storeBufferGenerics   += bufferGeneric.sizeOfExcludingThis(mallocSizeOf);
+}
+
 JS_PUBLIC_API(void)
 JS::HeapCellPostBarrier(js::gc::Cell **cellp)
 {
     JS_ASSERT(*cellp);
     JSRuntime *runtime = (*cellp)->runtimeFromMainThread();
     runtime->gcStoreBuffer.putRelocatableCell(cellp);
 }
 
--- a/js/src/gc/StoreBuffer.h
+++ b/js/src/gc/StoreBuffer.h
@@ -15,16 +15,17 @@
 
 #include "mozilla/DebugOnly.h"
 #include "mozilla/ReentrancyGuard.h"
 
 #include "jsalloc.h"
 
 #include "ds/LifoAlloc.h"
 #include "gc/Nursery.h"
+#include "js/MemoryMetrics.h"
 #include "js/Tracer.h"
 
 namespace js {
 namespace gc {
 
 extern void
 CrashAtUnhandlableOOM(const char *);
 
@@ -134,16 +135,20 @@ class StoreBuffer
                 if (isAboutToOverflow())
                     owner->setAboutToOverflow();
             }
         }
 
         /* Mark the source of all edges in the store buffer. */
         void mark(StoreBuffer *owner, JSTracer *trc);
 
+        size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) {
+            return storage_ ? storage_->sizeOfIncludingThis(mallocSizeOf) : 0;
+        }
+
       private:
         MonoTypeBuffer &operator=(const MonoTypeBuffer& other) MOZ_DELETE;
     };
 
     /*
      * Overrides the MonoTypeBuffer to support pointers that may be moved in
      * memory outside of the GC's control.
      */
@@ -204,16 +209,20 @@ class StoreBuffer
             T *tp = storage_->new_<T>(t);
             if (!tp)
                 CrashAtUnhandlableOOM("Failed to allocate for GenericBuffer::put.");
 
             if (isAboutToOverflow())
                 owner->setAboutToOverflow();
         }
 
+        size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) {
+            return storage_ ? storage_->sizeOfIncludingThis(mallocSizeOf) : 0;
+        }
+
       private:
         GenericBuffer &operator=(const GenericBuffer& other) MOZ_DELETE;
     };
 
     struct CellPtrEdge
     {
         Cell **edge;
 
@@ -431,17 +440,18 @@ class StoreBuffer
     /* Mark the source of all edges in the store buffer. */
     void mark(JSTracer *trc);
 
     /* We cannot call InParallelSection directly because of a circular dependency. */
     bool inParallelSection() const;
 
     /* For use by our owned buffers and for testing. */
     void setAboutToOverflow();
-    void setOverflowed();
+
+    void addSizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf, JS::GCSizes *sizes);
 };
 
 } /* namespace gc */
 } /* namespace js */
 
 #endif /* JSGC_GENERATIONAL */
 
 #endif /* gc_StoreBuffer_h */
--- a/js/src/jsapi.cpp
+++ b/js/src/jsapi.cpp
@@ -2745,53 +2745,58 @@ LookupResult(JSContext *cx, HandleObject
     }
 
     /* XXX bad API: no way to return "defined but value unknown" */
     vp.setBoolean(true);
     return true;
 }
 
 JS_PUBLIC_API(bool)
-JS_LookupPropertyById(JSContext *cx, JSObject *objArg, jsid idArg, MutableHandleValue vp)
-{
-    RootedId id(cx, idArg);
-    RootedObject obj(cx, objArg);
+JS_LookupPropertyById(JSContext *cx, HandleObject obj, HandleId id, MutableHandleValue vp)
+{
     RootedObject obj2(cx);
     RootedShape prop(cx);
 
     return LookupPropertyById(cx, obj, id, 0, &obj2, &prop) &&
            LookupResult(cx, obj, obj2, id, prop, vp);
 }
 
 JS_PUBLIC_API(bool)
-JS_LookupElement(JSContext *cx, JSObject *objArg, uint32_t index, MutableHandleValue vp)
-{
-    RootedObject obj(cx, objArg);
+JS_LookupElement(JSContext *cx, HandleObject obj, uint32_t index, MutableHandleValue vp)
+{
     CHECK_REQUEST(cx);
     RootedId id(cx);
     if (!IndexToId(cx, index, &id))
         return false;
     return JS_LookupPropertyById(cx, obj, id, vp);
 }
 
 JS_PUBLIC_API(bool)
-JS_LookupProperty(JSContext *cx, JSObject *objArg, const char *name, MutableHandleValue vp)
+JS_LookupProperty(JSContext *cx, HandleObject objArg, const char *name, MutableHandleValue vp)
 {
     RootedObject obj(cx, objArg);
     JSAtom *atom = Atomize(cx, name, strlen(name));
-    return atom && JS_LookupPropertyById(cx, obj, AtomToId(atom), vp);
+    if (!atom)
+        return false;
+
+    RootedId id(cx, AtomToId(atom));
+    return JS_LookupPropertyById(cx, obj, id, vp);
 }
 
 JS_PUBLIC_API(bool)
-JS_LookupUCProperty(JSContext *cx, JSObject *objArg, const jschar *name, size_t namelen,
+JS_LookupUCProperty(JSContext *cx, HandleObject objArg, const jschar *name, size_t namelen,
                     MutableHandleValue vp)
 {
     RootedObject obj(cx, objArg);
     JSAtom *atom = AtomizeChars(cx, name, AUTO_NAMELEN(name, namelen));
-    return atom && JS_LookupPropertyById(cx, obj, AtomToId(atom), vp);
+    if (!atom)
+        return false;
+
+    RootedId id(cx, AtomToId(atom));
+    return JS_LookupPropertyById(cx, obj, id, vp);
 }
 
 JS_PUBLIC_API(bool)
 JS_LookupPropertyWithFlagsById(JSContext *cx, HandleObject obj, HandleId id, unsigned flags,
                                MutableHandleObject objp, MutableHandleValue vp)
 {
     RootedShape prop(cx);
 
--- a/js/src/jsapi.h
+++ b/js/src/jsapi.h
@@ -2285,17 +2285,16 @@ class AutoIdArray : private AutoGCRooter
         if (idArray)
             JS_DestroyIdArray(context, idArray);
     }
     bool operator!() {
         return !idArray;
     }
     jsid operator[](size_t i) const {
         JS_ASSERT(idArray);
-        JS_ASSERT(i < length());
         return JS_IdArrayGet(context, idArray, i);
     }
     size_t length() const {
         return JS_IdArrayLength(context, idArray);
     }
 
     friend void AutoGCRooter::trace(JSTracer *trc);
 
@@ -2825,20 +2824,21 @@ JS_AlreadyHasOwnPropertyById(JSContext *
 
 extern JS_PUBLIC_API(bool)
 JS_HasProperty(JSContext *cx, JS::HandleObject obj, const char *name, bool *foundp);
 
 extern JS_PUBLIC_API(bool)
 JS_HasPropertyById(JSContext *cx, JS::HandleObject obj, JS::HandleId id, bool *foundp);
 
 extern JS_PUBLIC_API(bool)
-JS_LookupProperty(JSContext *cx, JSObject *obj, const char *name, JS::MutableHandleValue vp);
+JS_LookupProperty(JSContext *cx, JS::HandleObject obj, const char *name, JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(bool)
-JS_LookupPropertyById(JSContext *cx, JSObject *obj, jsid id, JS::MutableHandleValue vp);
+JS_LookupPropertyById(JSContext *cx, JS::HandleObject obj, JS::HandleId id,
+                      JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(bool)
 JS_LookupPropertyWithFlags(JSContext *cx, JS::HandleObject obj, const char *name,
                            unsigned flags, JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(bool)
 JS_LookupPropertyWithFlagsById(JSContext *cx, JS::HandleObject obj, JS::HandleId id,
                                unsigned flags, JS::MutableHandleObject objp, JS::MutableHandleValue vp);
@@ -3059,17 +3059,17 @@ JS_AlreadyHasOwnUCProperty(JSContext *cx
                            size_t namelen, bool *foundp);
 
 extern JS_PUBLIC_API(bool)
 JS_HasUCProperty(JSContext *cx, JS::HandleObject obj,
                  const jschar *name, size_t namelen,
                  bool *vp);
 
 extern JS_PUBLIC_API(bool)
-JS_LookupUCProperty(JSContext *cx, JSObject *obj,
+JS_LookupUCProperty(JSContext *cx, JS::HandleObject obj,
                     const jschar *name, size_t namelen,
                     JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(bool)
 JS_GetUCProperty(JSContext *cx, JSObject *obj,
                  const jschar *name, size_t namelen,
                  JS::MutableHandleValue vp);
 
@@ -3100,17 +3100,17 @@ JS_DefineElement(JSContext *cx, JSObject
 
 extern JS_PUBLIC_API(bool)
 JS_AlreadyHasOwnElement(JSContext *cx, JS::HandleObject obj, uint32_t index, bool *foundp);
 
 extern JS_PUBLIC_API(bool)
 JS_HasElement(JSContext *cx, JS::HandleObject obj, uint32_t index, bool *foundp);
 
 extern JS_PUBLIC_API(bool)
-JS_LookupElement(JSContext *cx, JSObject *obj, uint32_t index, JS::MutableHandleValue vp);
+JS_LookupElement(JSContext *cx, JS::HandleObject obj, uint32_t index, JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(bool)
 JS_GetElement(JSContext *cx, JSObject *obj, uint32_t index, JS::MutableHandleValue vp);
 
 extern JS_PUBLIC_API(bool)
 JS_ForwardGetElementTo(JSContext *cx, JSObject *obj, uint32_t index, JSObject *onBehalfOf,
                        JS::MutableHandleValue vp);
 
--- a/js/src/jsfun.cpp
+++ b/js/src/jsfun.cpp
@@ -1050,16 +1050,22 @@ js_fun_apply(JSContext *cx, unsigned arg
 
         if (!args.init(length))
             return false;
 
         /* Push fval, obj, and aobj's elements as args. */
         args.setCallee(fval);
         args.setThis(vp[2]);
 
+        // Make sure the function is delazified before querying its arguments.
+        if (args.callee().is<JSFunction>()) {
+            JSFunction *fun = &args.callee().as<JSFunction>();
+            if (fun->isInterpreted() && !fun->getOrCreateScript(cx))
+                return false;
+        }
         /* Steps 7-8. */
         if (!GetElements(cx, aobj, length, args.array()))
             return false;
     }
 
     /* Step 9. */
     if (!Invoke(cx, args))
         return false;
--- a/js/src/jsscript.cpp
+++ b/js/src/jsscript.cpp
@@ -1202,16 +1202,30 @@ SourceDataCache::purge()
 
     for (Map::Range r = map_->all(); !r.empty(); r.popFront())
         js_delete(const_cast<jschar*>(r.front().value()));
 
     js_delete(map_);
     map_ = nullptr;
 }
 
+size_t
+SourceDataCache::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf)
+{
+    size_t n = 0;
+    if (map_ && !map_->empty()) {
+        n += map_->sizeOfIncludingThis(mallocSizeOf);
+        for (Map::Range r = map_->all(); !r.empty(); r.popFront()) {
+            const jschar *v = r.front().value();
+            n += mallocSizeOf(v);
+        }
+    }
+    return n;
+}
+
 const jschar *
 ScriptSource::chars(JSContext *cx, const SourceDataCache::AutoSuppressPurge &asp)
 {
     if (const jschar *chars = getOffThreadCompressionChars(cx))
         return chars;
     JS_ASSERT(ready());
 
 #ifdef USE_ZLIB
--- a/js/src/jsscript.h
+++ b/js/src/jsscript.h
@@ -348,16 +348,18 @@ class SourceDataCache
         ~AutoSuppressPurge();
         SourceDataCache &cache() const { return cache_; }
     };
 
     const jschar *lookup(ScriptSource *ss, const AutoSuppressPurge &asp);
     bool put(ScriptSource *ss, const jschar *chars, const AutoSuppressPurge &asp);
 
     void purge();
+
+    size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf);
 };
 
 class ScriptSource
 {
     friend class SourceCompressionTask;
 
     union {
         // Before setSourceCopy or setSource are successfully called, this union
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -4495,17 +4495,18 @@ Help(JSContext *cx, unsigned argc, jsval
     if (argc == 0) {
         RootedObject global(cx, JS::CurrentGlobalOrNull(cx));
         AutoIdArray ida(cx, JS_Enumerate(cx, global));
         if (!ida)
             return false;
 
         for (size_t i = 0; i < ida.length(); i++) {
             RootedValue v(cx);
-            if (!JS_LookupPropertyById(cx, global, ida[i], &v))
+            RootedId id(cx, ida[i]);
+            if (!JS_LookupPropertyById(cx, global, id, &v))
                 return false;
             if (JSVAL_IS_PRIMITIVE(v)) {
                 JS_ReportError(cx, "primitive arg");
                 return false;
             }
             obj = JSVAL_TO_OBJECT(v);
             if (!PrintHelp(cx, obj))
                 return false;
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -593,40 +593,45 @@ JSRuntime::addSizeOfIncludingThis(mozill
 
     for (ContextIter acx(this); !acx.done(); acx.next())
         rtSizes->contexts += acx->sizeOfIncludingThis(mallocSizeOf);
 
     rtSizes->dtoa += mallocSizeOf(mainThread.dtoaState);
 
     rtSizes->temporary += tempLifoAlloc.sizeOfExcludingThis(mallocSizeOf);
 
+    rtSizes->regexpData += bumpAlloc_ ? bumpAlloc_->sizeOfNonHeapData() : 0;
+
+    rtSizes->interpreterStack += interpreterStack_.sizeOfExcludingThis(mallocSizeOf);
+
+    rtSizes->mathCache += mathCache_ ? mathCache_->sizeOfIncludingThis(mallocSizeOf) : 0;
+
+    rtSizes->sourceDataCache += sourceDataCache.sizeOfExcludingThis(mallocSizeOf);
+
+    rtSizes->scriptData += scriptDataTable().sizeOfExcludingThis(mallocSizeOf);
+    for (ScriptDataTable::Range r = scriptDataTable().all(); !r.empty(); r.popFront())
+        rtSizes->scriptData += mallocSizeOf(r.front());
+
     if (execAlloc_)
         execAlloc_->addSizeOfCode(&rtSizes->code);
-
 #ifdef JS_ION
     {
         AutoLockForOperationCallback lock(this);
         if (jitRuntime()) {
             if (JSC::ExecutableAllocator *ionAlloc = jitRuntime()->ionAlloc(this))
                 ionAlloc->addSizeOfCode(&rtSizes->code);
         }
     }
 #endif
 
-    rtSizes->regexpData += bumpAlloc_ ? bumpAlloc_->sizeOfNonHeapData() : 0;
-
-    rtSizes->interpreterStack += interpreterStack_.sizeOfExcludingThis(mallocSizeOf);
-
-    rtSizes->gcMarker += gcMarker.sizeOfExcludingThis(mallocSizeOf);
-
-    rtSizes->mathCache += mathCache_ ? mathCache_->sizeOfIncludingThis(mallocSizeOf) : 0;
-
-    rtSizes->scriptData += scriptDataTable().sizeOfExcludingThis(mallocSizeOf);
-    for (ScriptDataTable::Range r = scriptDataTable().all(); !r.empty(); r.popFront())
-        rtSizes->scriptData += mallocSizeOf(r.front());
+    rtSizes->gc.marker += gcMarker.sizeOfExcludingThis(mallocSizeOf);
+#ifdef JSGC_GENERATIONAL
+    rtSizes->gc.nursery += gcNursery.sizeOfHeap();
+    gcStoreBuffer.addSizeOfExcludingThis(mallocSizeOf, &rtSizes->gc);
+#endif
 }
 
 static bool
 SignalBasedTriggersDisabled()
 {
   // Don't bother trying to cache the getenv lookup; this should be called
   // infrequently.
   return !!getenv("JS_DISABLE_SLOW_SCRIPT_SIGNALS");
--- a/js/xpconnect/src/XPCInlines.h
+++ b/js/xpconnect/src/XPCInlines.h
@@ -540,19 +540,20 @@ XPCWrappedNative::SweepTearOffs()
             }
         }
     }
 }
 
 /***************************************************************************/
 
 inline bool
-xpc_ForcePropertyResolve(JSContext* cx, JSObject* obj, jsid id)
+xpc_ForcePropertyResolve(JSContext* cx, JS::HandleObject obj, jsid idArg)
 {
     JS::RootedValue prop(cx);
+    JS::RootedId id(cx, idArg);
 
     if (!JS_LookupPropertyById(cx, obj, id, &prop))
         return false;
     return true;
 }
 
 inline jsid
 GetRTIdByIndex(JSContext *cx, unsigned index)
--- a/js/xpconnect/src/XPCJSRuntime.cpp
+++ b/js/xpconnect/src/XPCJSRuntime.cpp
@@ -2259,24 +2259,25 @@ ReportJSRuntimeExplicitTreeStats(const J
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/regexp-data"),
                   KIND_NONHEAP, rtStats.runtime.regexpData,
                   "Memory used by the regexp JIT to hold data.");
 
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/interpreter-stack"),
                   KIND_HEAP, rtStats.runtime.interpreterStack,
                   "Memory used for JS interpreter frames.");
 
-    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc-marker"),
-                  KIND_HEAP, rtStats.runtime.gcMarker,
-                  "Memory used for the GC mark stack and gray roots.");
-
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/math-cache"),
                   KIND_HEAP, rtStats.runtime.mathCache,
                   "Memory used for the math cache.");
 
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/source-data-cache"),
+                  KIND_HEAP, rtStats.runtime.sourceDataCache,
+                  "Memory used for the source data cache, which holds "
+                  "decompressed script source code.");
+
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/script-data"),
                   KIND_HEAP, rtStats.runtime.scriptData,
                   "Memory used for the table holding script data shared in "
                   "the runtime.");
 
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/script-sources"),
                   KIND_HEAP, rtStats.runtime.scriptSources,
                   "Memory use for storing JavaScript source code and filenames.");
@@ -2298,16 +2299,52 @@ ReportJSRuntimeExplicitTreeStats(const J
                   "Memory used by the JITs to hold generated code for "
                   "wrappers and trampolines.");
 
     RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/code/unused"),
                   KIND_NONHEAP, rtStats.runtime.code.unused,
                   "Memory allocated by one of the JITs to hold code, "
                   "but which is currently unused.");
 
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/marker"),
+                  KIND_HEAP, rtStats.runtime.gc.marker,
+                  "Memory used for the GC mark stack and gray roots.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/nursery"),
+                  KIND_NONHEAP, rtStats.runtime.gc.nursery,
+                  "Memory used for the GC nursery.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/vals"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferVals,
+                  "Memory used for values in the store buffer.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/cells"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferCells,
+                  "Memory used for cells in the store buffer.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/slots"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferSlots,
+                  "Memory used for slots in the store buffer.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/whole-cells"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferWholeCells,
+                  "Memory used for whole cells in the store buffer.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/reloc-vals"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferRelocVals,
+                  "Memory used for relocatable values in the store buffer.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/reloc-cells"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferRelocCells,
+                  "Memory used for relocatable cells in the store buffer.");
+
+    RREPORT_BYTES(rtPath + NS_LITERAL_CSTRING("runtime/gc/store-buffer/generics"),
+                  KIND_HEAP, rtStats.runtime.gc.storeBufferGenerics,
+                  "Memory used for generic things in the store buffer.");
+
     if (rtTotalOut)
         *rtTotalOut = rtTotal;
 
     // Report GC numbers that don't belong to a compartment.
 
     // We don't want to report decommitted memory in "explicit", so we just
     // change the leading "explicit/" to "decommitted/".
     nsCString rtPath2(rtPath);
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3494,17 +3494,17 @@ ExportFunction(JSContext *cx, JS::Handle
 
 } /* namespace xpc */
 
 
 /***************************************************************************/
 // Inlined utilities.
 
 inline bool
-xpc_ForcePropertyResolve(JSContext* cx, JSObject* obj, jsid id);
+xpc_ForcePropertyResolve(JSContext* cx, JS::HandleObject obj, jsid id);
 
 inline jsid
 GetRTIdByIndex(JSContext *cx, unsigned index);
 
 namespace xpc {
 
 class CompartmentPrivate
 {
--- a/layout/base/tests/bug106855-1-ref.html
+++ b/layout/base/tests/bug106855-1-ref.html
@@ -1,14 +1,14 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
 x<br>
-<textarea id="t" rows="4">
+<textarea id="t" rows="4" spellcheck="false">
 A
 
 
 </textarea><br>
 y
 <script>
   // Position the caret at the last line
   var sel = window.getSelection();
--- a/layout/base/tests/bug106855-1.html
+++ b/layout/base/tests/bug106855-1.html
@@ -1,14 +1,14 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
 x<br>
-<textarea id="t" rows="4">
+<textarea id="t" rows="4" spellcheck="false">
 A
 
 
 </textarea><br>
 y
 <script>
   // Position the caret at the last line
   var sel = window.getSelection();
--- a/layout/base/tests/bug106855-2.html
+++ b/layout/base/tests/bug106855-2.html
@@ -1,14 +1,14 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
 x<br>
-<textarea id="t" rows="4">
+<textarea id="t" rows="4" spellcheck="false">
 A
 
 
 </textarea><br>
 y
 <script>
   // Position the caret at the last line
   var sel = window.getSelection();
--- a/layout/base/tests/bug482484-ref.html
+++ b/layout/base/tests/bug482484-ref.html
@@ -1,11 +1,11 @@
 <!DOCTYPE HTML><html><head></head>
 <body>
-<div contentEditable="true" id="div"><p id="p">ABC</p></div>
+<div contentEditable="true" id="div" spellcheck="false"><p id="p">ABC</p></div>
 <script>
   // Position the caret after the "A"
   var div = document.getElementById('div');
   var p = document.getElementById('p');
   div.focus();
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
--- a/layout/base/tests/bug482484.html
+++ b/layout/base/tests/bug482484.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-<div contentEditable="true" id="div"><p id="p">BC</p></div>
+<div contentEditable="true" id="div" spellcheck="false"><p id="p">BC</p></div>
 <script>
   // Position the caret before the "B"
   var div = document.getElementById('div');
   div.focus();
   var p = document.getElementById('p');
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
--- a/layout/base/tests/bug512295-1-ref.html
+++ b/layout/base/tests/bug512295-1-ref.html
@@ -1,24 +1,28 @@
-<!DOCTYPE HTML><html><head>
+<!DOCTYPE HTML><html class="reftest-wait"><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
 <div contenteditable="true">
 <p id="p">A B CD EFG<br>
   1234567890</p>
 </div>
 x
 <script>
   // Position the caret at the end of the P element
   var p = document.getElementById('p');
   var div = p.parentNode;
   div.focus();
-  var sel = window.getSelection();
-  sel.removeAllRanges();
-  var range = document.createRange();
-  range.setStart(p, p.childNodes.length);
-  range.setEnd(p, p.childNodes.length);
-  sel.addRange(range);
+  SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window);
+  onSpellCheck(div, function () {
+    var sel = window.getSelection();
+    sel.removeAllRanges();
+    var range = document.createRange();
+    range.setStart(p, p.childNodes.length);
+    range.setEnd(p, p.childNodes.length);
+    sel.addRange(range);
+    document.documentElement.classList.remove("reftest-wait");
+  });
 </script>
 
 </body>
 </html>
--- a/layout/base/tests/bug512295-1.html
+++ b/layout/base/tests/bug512295-1.html
@@ -1,9 +1,9 @@
-<!DOCTYPE HTML><html><head>
+<!DOCTYPE HTML><html class="reftest-wait"><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
 <div contenteditable="true">
 <p id="p">A B CD EFG<br>
   1234567890</p>
 </div>
 x
@@ -14,17 +14,21 @@ x
   var range = document.createRange();
   var p = document.getElementById('p');
   var t = p.firstChild;
   range.setStart(t, 1);
   range.setEnd(t, 1);
   sel.addRange(range);
   p.parentNode.focus();
 
-  sendKey('DOWN');  // now after "1"
-  sendKey('DOWN');  // now make sure we get to the end
-  sendKey('DOWN');  // now make sure we get to the end
-  sendKey('DOWN');  // now make sure we get to the end
-  sendKey('DOWN');  // now make sure we get to the end
-  sendKey('DOWN');  // now make sure we get to the end
+  SpecialPowers.Cu.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm", window);
+  onSpellCheck(p.parentNode, function () {
+    sendKey('DOWN');  // now after "1"
+    sendKey('DOWN');  // now make sure we get to the end
+    sendKey('DOWN');  // now make sure we get to the end
+    sendKey('DOWN');  // now make sure we get to the end
+    sendKey('DOWN');  // now make sure we get to the end
+    sendKey('DOWN');  // now make sure we get to the end
+    document.documentElement.classList.remove("reftest-wait");
+  });
 </script>
 </body>
 </html>
--- a/layout/base/tests/bug597519-1-ref.html
+++ b/layout/base/tests/bug597519-1-ref.html
@@ -1,12 +1,12 @@
 <!DOCTYPE HTML><html><head>
 </head>
 <body>
-<textarea>ab
+<textarea spellcheck="false">ab
 </textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
   t.selectionStart = t.selectionEnd = t.value.length;
 </script>
 </body>
 </html>
--- a/layout/base/tests/bug597519-1.html
+++ b/layout/base/tests/bug597519-1.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-<textarea maxlength="3"></textarea>
+<textarea maxlength="3" spellcheck="false"></textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
 
   synthesizeKey("a", {});
   synthesizeKey("b", {});
   synthesizeKey("VK_ENTER", {});
   synthesizeKey("c", {});
--- a/layout/base/tests/bug602141-1-ref.html
+++ b/layout/base/tests/bug602141-1-ref.html
@@ -1,12 +1,12 @@
 <!DOCTYPE HTML><html><head>
 </head>
 <body>
-<span contenteditable="true">navigable__</span><span id="x" contenteditable="true">navigable|unnavigable</span><br />
+<span contenteditable="true" spellcheck="false">navigable__</span><span id="x" contenteditable="true" spellcheck="false">navigable|unnavigable</span><br />
 <script>
   // Position the caret after "u"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 11);
--- a/layout/base/tests/bug602141-1.html
+++ b/layout/base/tests/bug602141-1.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-<span contenteditable="true">navigable__</span><span id="x" contenteditable="true">navigable|unnavigable</span><br />
+<span contenteditable="true" spellcheck="false">navigable__</span><span id="x" contenteditable="true" spellcheck="false">navigable|unnavigable</span><br />
 <script>
   // Position the caret after "|"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 10);
--- a/layout/base/tests/bug602141-2-ref.html
+++ b/layout/base/tests/bug602141-2-ref.html
@@ -1,12 +1,12 @@
 <!DOCTYPE HTML><html><head>
 </head>
 <body>
-<span id="x" contenteditable="true">navigable__|unnavigable</span><br />
+<span id="x" contenteditable="true" spellcheck="false">navigable__|unnavigable</span><br />
 <script>
   // Position the caret after "u"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 13);
--- a/layout/base/tests/bug602141-2.html
+++ b/layout/base/tests/bug602141-2.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-<span id="x" contenteditable="true">navigable__|</span><br />
+<span id="x" contenteditable="true" spellcheck="false">navigable__|</span><br />
 <script>
   document.getElementById('x').appendChild(document.createTextNode('unnavigable'));
 
   // Position the caret after "|"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
--- a/layout/base/tests/bug602141-3-ref.html
+++ b/layout/base/tests/bug602141-3-ref.html
@@ -1,12 +1,12 @@
 <!DOCTYPE HTML><html><head>
 </head>
 <body>
-noteditable<span id="x" contenteditable="true">navigable|unnavigable</span><br />
+noteditable<span id="x" contenteditable="true" spellcheck="false">navigable|unnavigable</span><br />
 <script>
   // Position the caret after "u"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 11);
--- a/layout/base/tests/bug602141-3.html
+++ b/layout/base/tests/bug602141-3.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-noteditable<span id="x" contenteditable="true">navigable|unnavigable</span><br />
+noteditable<span id="x" contenteditable="true" spellcheck="false">navigable|unnavigable</span><br />
 <script>
   // Position the caret after "|"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 10);
--- a/layout/base/tests/bug602141-4-ref.html
+++ b/layout/base/tests/bug602141-4-ref.html
@@ -1,12 +1,12 @@
 <!DOCTYPE HTML><html><head>
 </head>
 <body>
-<span>not editable</span><span id="x" contenteditable="true">navigable|unnavigable</span>
+<span>not editable</span><span id="x" contenteditable="true" spellcheck="false">navigable|unnavigable</span>
 <script>
   // Position the caret after "u"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 11);
--- a/layout/base/tests/bug602141-4.html
+++ b/layout/base/tests/bug602141-4.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-<span>not editable</span><span id="x" contenteditable="true">navigable|unnavigable</span>
+<span>not editable</span><span id="x" contenteditable="true" spellcheck="false">navigable|unnavigable</span>
 <script>
   // Position the caret after "|"
   var sel = window.getSelection();
   sel.removeAllRanges();
   var range = document.createRange();
   var x = document.getElementById('x');
   var t = x.firstChild;
   range.setStart(t, 10);
--- a/layout/base/tests/bug612271-1.html
+++ b/layout/base/tests/bug612271-1.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
- <textarea id="target" style="height: 100px"
+ <textarea id="target" style="height: 100px" spellcheck="false"
     onkeydown="this.style.display='block';this.style.height='200px';">foo</textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
   t.selectionStart = t.selectionEnd = t.value.length;
   sendKey('ENTER');
   document.body.appendChild(document.createTextNode(t.selectionStart + " - " + t.selectionEnd));
 </script>
--- a/layout/base/tests/bug612271-2.html
+++ b/layout/base/tests/bug612271-2.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-  <textarea id="target" style="height: 100px"
+  <textarea id="target" style="height: 100px" spellcheck="false"
     onkeypress="this.style.display='block';this.style.height='200px';">foo</textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
   t.selectionStart = t.selectionEnd = t.value.length;
   sendKey('ENTER');
   document.body.appendChild(document.createTextNode(t.selectionStart + " - " + t.selectionEnd));
 </script>
--- a/layout/base/tests/bug612271-3.html
+++ b/layout/base/tests/bug612271-3.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-  <textarea id="target" style="height: 100px"
+  <textarea id="target" style="height: 100px" spellcheck="false"
     onkeyup="this.style.display='block';this.style.height='200px';">foo</textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
   t.selectionStart = t.selectionEnd = t.value.length;
   sendKey('ENTER');
   document.body.appendChild(document.createTextNode(t.selectionStart + " - " + t.selectionEnd));
 </script>
--- a/layout/base/tests/bug612271-ref.html
+++ b/layout/base/tests/bug612271-ref.html
@@ -4,14 +4,14 @@
     function loaded() {
       var t = document.querySelector("textarea");
       t.focus();
       t.selectionStart = t.selectionEnd = 4;
     }
   </script>
 </head>
 <body onload="loaded()">
-  <textarea style="height: 200px; display: block;"
+  <textarea style="height: 200px; display: block;" spellcheck="false"
     >foo
 </textarea>
   4 - 4
 </body>
 </html>
--- a/layout/base/tests/bug632215-1.html
+++ b/layout/base/tests/bug632215-1.html
@@ -1,15 +1,15 @@
 <!DOCTYPE html>
 <html class="reftest-wait">
   <head>
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body>
-    <iframe src="data:text/html,<body></body>"></iframe>
+    <iframe src="data:text/html,<body spellcheck=false></body>"></iframe>
     <script>
       onload = function() {
         var i = document.querySelector("iframe");
         var d = i.contentDocument;
         var w = i.contentWindow;
         var s = w.getSelection();
         i.focus();
         d.body.contentEditable = true;
--- a/layout/base/tests/bug632215-2.html
+++ b/layout/base/tests/bug632215-2.html
@@ -1,15 +1,15 @@
 <!DOCTYPE html>
 <html class="reftest-wait">
   <head>
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body>
-    <iframe src="data:text/html,<body contenteditable></body>"></iframe>
+    <iframe src="data:text/html,<body contenteditable spellcheck=false></body>"></iframe>
     <script>
       onload = function() {
         var i = document.querySelector("iframe");
         var d = i.contentDocument;
         var w = i.contentWindow;
         var s = w.getSelection();
         i.focus();
         d.body.contentEditable = false;
--- a/layout/base/tests/bug632215-ref.html
+++ b/layout/base/tests/bug632215-ref.html
@@ -1,12 +1,12 @@
 <!DOCTYPE html>
 <html>
   <body>
-    <iframe src="data:text/html,<body>xx</body>"></iframe>
+    <iframe src="data:text/html,<body spellcheck=false>xx</body>"></iframe>
     <script>
       onload = function() {
         var i = document.querySelector("iframe");
         var d = i.contentDocument;
         var w = i.contentWindow;
         d.designMode = "on";
         i.focus();
         d.body.focus();
--- a/layout/base/tests/bug634406-1-ref.html
+++ b/layout/base/tests/bug634406-1-ref.html
@@ -1,10 +1,10 @@
 <!DOCTYPE HTML><html>
 <body>
-<textarea>ab</textarea>
+<textarea spellcheck="false">ab</textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
   t.selectionStart = t.selectionEnd = t.value.length;
 </script>
 </body>
 </html>
--- a/layout/base/tests/bug634406-1.html
+++ b/layout/base/tests/bug634406-1.html
@@ -1,13 +1,13 @@
 <!DOCTYPE HTML><html><head>
   <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
 </head>
 <body>
-<textarea></textarea>
+<textarea spellcheck="false"></textarea>
 <script>
   var t = document.querySelector("textarea");
   t.focus();
 
   synthesizeKey("a", {});
   synthesizeKey("A", {accelKey: true});
   synthesizeKey("VK_RIGHT", {});
   synthesizeKey("b", {});
--- a/layout/base/tests/bug664087-1-ref.html
+++ b/layout/base/tests/bug664087-1-ref.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <textarea onfocus="done()">אב
+    <textarea onfocus="done()" spellcheck="false">אב
 ג</textarea>
     <script>
       var textarea = document.querySelector("textarea");
       function start() {
         textarea.focus();
       }
       function done() {
         synthesizeKey("VK_LEFT", {});
--- a/layout/base/tests/bug664087-1.html
+++ b/layout/base/tests/bug664087-1.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <textarea onfocus="typeIntoMe()"></textarea>
+    <textarea onfocus="typeIntoMe()" spellcheck="false"></textarea>
     <script>
       function start() {
         document.querySelector("textarea").focus();
       }
       function typeIntoMe() {
         setTimeout(function() {
           synthesizeKey("א", {});
           synthesizeKey("VK_ENTER", {});
--- a/layout/base/tests/bug664087-2-ref.html
+++ b/layout/base/tests/bug664087-2-ref.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <textarea dir="rtl" onfocus="done()">ab
+    <textarea dir="rtl" onfocus="done()" spellcheck="false">ab
 c</textarea>
     <script>
       var textarea = document.querySelector("textarea");
       function start() {
         textarea.focus();
       }
       function done() {
         synthesizeKey("VK_RIGHT", {});
--- a/layout/base/tests/bug664087-2.html
+++ b/layout/base/tests/bug664087-2.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <textarea dir="rtl" onfocus="typeIntoMe()"></textarea>
+    <textarea dir="rtl" onfocus="typeIntoMe()" spellcheck="false"></textarea>
     <script>
       function start() {
         document.querySelector("textarea").focus();
       }
       function typeIntoMe() {
         setTimeout(function() {
           synthesizeKey("a", {});
           synthesizeKey("VK_ENTER", {});
--- a/layout/base/tests/bug682712-1-ref.html
+++ b/layout/base/tests/bug682712-1-ref.html
@@ -1,14 +1,14 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
   </head>
   <body onload="start()">
-    <iframe src="data:text/html,<body contenteditable>foo bar"></iframe>
+    <iframe src="data:text/html,<body contenteditable spellcheck=false>foo bar"></iframe>
     <script>
       function start() {
         var iframe = document.querySelector("iframe");
         var win = iframe.contentWindow;
         var doc = iframe.contentDocument;
 
         setTimeout(function() {
           doc.body.focus();
--- a/layout/base/tests/bug682712-1.html
+++ b/layout/base/tests/bug682712-1.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <iframe src="data:text/html,<body contenteditable>foo bar"></iframe>
+    <iframe src="data:text/html,<body contenteditable spellcheck=false>foo bar"></iframe>
     <script>
       function start() {
         var iframe = document.querySelector("iframe");
         var win = iframe.contentWindow;
         var doc = iframe.contentDocument;
 
         // Reframe the iframe
         iframe.style.display = "none";
--- a/layout/base/tests/bug746993-1-ref.html
+++ b/layout/base/tests/bug746993-1-ref.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <iframe src="data:text/html,<body contenteditable>Here's some text.<br /><br /><div></div></body>"></iframe>
+    <iframe src="data:text/html,<body contenteditable spellcheck=false>Here's some text.<br /><br /><div></div></body>"></iframe>
     <script>
       function start() {
         var iframe = document.querySelector("iframe");
         var win = iframe.contentWindow;
         var doc = iframe.contentDocument;
         iframe.focus();
         doc.body.focus();
         win.getSelection().collapse(doc.body, 3);
--- a/layout/base/tests/bug746993-1.html
+++ b/layout/base/tests/bug746993-1.html
@@ -1,15 +1,15 @@
 <html class="reftest-wait">
   <head>
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
     <script type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
   </head>
   <body onload="start()">
-    <iframe src="data:text/html,<body contenteditable><br /><div></div></body>"></iframe>
+    <iframe src="data:text/html,<body contenteditable spellcheck=false><br /><div></div></body>"></iframe>
     <script>
       function start() {
         var iframe = document.querySelector("iframe");
         var win = iframe.contentWindow;
         var doc = iframe.contentDocument;
         iframe.focus();
         doc.body.focus();
         win.getSelection().collapse(doc.body, 0);
--- a/layout/reftests/reftest-sanity/reftest.list
+++ b/layout/reftests/reftest-sanity/reftest.list
@@ -39,17 +39,17 @@ HTTP != blank.html default.html
 skip-if(B2G) HTTP(..) == filter-1.xhtml filter-1-ref.xhtml
 skip-if(B2G) HTTP(..) == filter-2.xhtml filter-2-ref.xhtml # bug 773482
 
 # test that the MozReftestInvalidate event fires
 == invalidation.html about:blank
 == zoom-invalidation.html zoom-invalidation-ref.html # bug 773482
 
 # test that xulRuntime.OS works
-skip-if(B2G) fails-if(xulRuntime.OS!="Linux"&&!Android) == data:text/html,<body>Linux data:text/html,<script>document.write(navigator.platform.substr(0,5))</script>
+skip-if(B2G||B2GDT) fails-if(xulRuntime.OS!="Linux"&&!Android) == data:text/html,<body>Linux data:text/html,<script>document.write(navigator.platform.substr(0,5))</script>
 fails-if(xulRuntime.OS!="WINNT") == data:text/html,<body>Win data:text/html,<script>document.write(navigator.platform.substr(0,3))</script>
 fails-if(xulRuntime.OS!="Darwin") == data:text/html,<body>Mac data:text/html,<script>document.write(navigator.platform.substr(0,3))</script>
 
 # test parsing of asserts() expressions
 asserts(0) load about:blank
 asserts(0-5) load about:blank
 asserts-if(true,0) load about:blank
 asserts-if(false,7) load about:blank
@@ -71,19 +71,19 @@ include default-preferences-tests.list
 # test that all corners are visible
 != corners-1.html corners-1-ref.html
 != corners-2.html corners-2-ref.html
 != corners-3.html corners-3-ref.html
 != corners-4.html corners-4-ref.html
 
 # Test that the harness gives the correct page dimensions.
 != page-width-3.9in.html page-width-4in.html
-skip-if(B2G) == page-width-4.1in.html page-width-4in.html   # bug 774396
-skip-if(B2G) == page-width-auto.html page-width-4in.html    # bug 774396
-skip-if(B2G) != page-height-2in.html page-height-2.1in.html # bug 774396
+skip-if(B2G||B2GDT) == page-width-4.1in.html page-width-4in.html   # bug 774396
+skip-if(B2G||B2GDT) == page-width-auto.html page-width-4in.html    # bug 774396
+skip-if(B2G||B2GDT) != page-height-2in.html page-height-2.1in.html # bug 774396
 == page-height-2in.html page-height-nobreak.html
 == page-height-2.1in.html page-height-forcebreak.html
 
 # Check that tests that need focus are skipped when it's not available
 needs-focus load needs-focus.html
 
 # Bug 632636
 fails == data:text/plain,HELLO about:blank
--- a/layout/tools/reftest/Makefile.in
+++ b/layout/tools/reftest/Makefile.in
@@ -40,16 +40,17 @@ make-xpi:
 copy-harness: make-xpi
 libs:: copy-harness
 endif
 
 _HARNESS_FILES = \
   $(srcdir)/runreftest.py \
   $(srcdir)/remotereftest.py \
   $(srcdir)/runreftestb2g.py \
+  $(srcdir)/b2g_desktop.py \
   $(srcdir)/b2g_start_script.js \
   automation.py \
   $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanager.py \
   $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanagerADB.py \
   $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/devicemanagerSUT.py \
   $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/droid.py \
   $(topsrcdir)/testing/mozbase/mozdevice/mozdevice/Zeroconf.py \
   $(topsrcdir)/build/mobile/b2gautomation.py \
new file mode 100644
--- /dev/null
+++ b/layout/tools/reftest/b2g_desktop.py
@@ -0,0 +1,170 @@
+# 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/.
+from __future__ import print_function, unicode_literals
+
+import json
+import os
+import signal
+import sys
+import threading
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+from runreftest import RefTest, ReftestOptions
+
+from marionette import Marionette
+from mozprocess import ProcessHandler
+from mozrunner import FirefoxRunner
+import mozinfo
+import mozlog
+
+log = mozlog.getLogger('REFTEST')
+
+class B2GDesktopReftest(RefTest):
+    def __init__(self, marionette):
+        RefTest.__init__(self)
+        self.last_test = os.path.basename(__file__)
+        self.marionette = marionette
+        self.profile = None
+        self.runner = None
+        self.test_script = os.path.join(here, 'b2g_start_script.js')
+        self.timeout = None
+
+    def run_marionette_script(self):
+        assert(self.marionette.wait_for_port())
+        self.marionette.start_session()
+        self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+
+        if os.path.isfile(self.test_script):
+            f = open(self.test_script, 'r')
+            self.test_script = f.read()
+            f.close()
+        self.marionette.execute_script(self.test_script)
+
+    def run_tests(self, test_path, options):
+        reftestlist = self.getManifestPath(test_path)
+        if not reftestlist.startswith('file://'):
+            reftestlist = 'file://%s' % reftestlist
+
+        self.profile = self.create_profile(options, reftestlist,
+                                           profile_to_clone=options.profile)
+        env = self.buildBrowserEnv(options, self.profile.profile)
+        kp_kwargs = { 'processOutputLine': [self._on_output],
+                      'onTimeout': [self._on_timeout],
+                      'kill_on_timeout': False }
+
+        if not options.debugger:
+            if not options.timeout:
+                if mozinfo.info['debug']:
+                    options.timeout = 420
+                else:
+                    options.timeout = 300
+            self.timeout = options.timeout + 30.0
+
+        log.info("%s | Running tests: start.", os.path.basename(__file__))
+        cmd, args = self.build_command_line(options.app,
+                            ignore_window_size=options.ignoreWindowSize)
+        self.runner = FirefoxRunner(profile=self.profile,
+                                    binary=cmd,
+                                    cmdargs=args,
+                                    env=env,
+                                    process_class=ProcessHandler,
+                                    symbols_path=options.symbolsPath,
+                                    kp_kwargs=kp_kwargs)
+
+        status = 0
+        try:
+            self.runner.start(outputTimeout=self.timeout)
+            log.info("%s | Application pid: %d",
+                     os.path.basename(__file__),
+                     self.runner.process_handler.pid)
+
+            # kick starts the reftest harness
+            self.run_marionette_script()
+            status = self.runner.wait()
+        finally:
+            self.runner.check_for_crashes(test_name=self.last_test)
+            self.runner.cleanup()
+
+        if status > 0:
+            log.testFail("%s | application terminated with exit code %s",
+                         self.last_test, status)
+        elif status < 0:
+            log.info("%s | application killed with signal %s",
+                         self.last_test, -status)
+
+        log.info("%s | Running tests: end.", os.path.basename(__file__))
+        return status
+
+    def create_profile(self, options, reftestlist, profile_to_clone=None):
+        profile = RefTest.createReftestProfile(self, options, reftestlist,
+                                               profile_to_clone=profile_to_clone)
+
+        prefs = {}
+        # Turn off the locale picker screen
+        prefs["browser.firstrun.show.localepicker"] = False
+        prefs["browser.homescreenURL"] = "app://test-container.gaiamobile.org/index.html"
+        prefs["browser.manifestURL"] = "app://test-container.gaiamobile.org/manifest.webapp"
+        prefs["browser.tabs.remote"] = False
+        prefs["dom.ipc.tabs.disabled"] = False
+        prefs["dom.mozBrowserFramesEnabled"] = True
+        prefs["font.size.inflation.emPerLine"] = 0
+        prefs["font.size.inflation.minTwips"] = 0
+        prefs["network.dns.localDomains"] = "app://test-container.gaiamobile.org"
+        prefs["reftest.browser.iframe.enabled"] = False
+        prefs["reftest.remote"] = False
+        prefs["reftest.uri"] = "%s" % reftestlist
+        # Set a future policy version to avoid the telemetry prompt.
+        prefs["toolkit.telemetry.prompted"] = 999
+        prefs["toolkit.telemetry.notifiedOptOut"] = 999
+
+        # Set the extra prefs.
+        profile.set_preferences(prefs)
+        return profile
+
+    def build_command_line(self, app, ignore_window_size=False):
+        cmd = os.path.abspath(app)
+        args = []
+
+        if not ignore_window_size:
+            args.extend(['--screen', '800x1000'])
+        return cmd, args
+
+    def _on_output(self, line):
+        print(line)
+        # TODO use structured logging
+        if "TEST-START" in line and "|" in line:
+            self.last_test = line.split("|")[1].strip()
+
+    def _on_timeout(self):
+        msg = "%s | application timed out after %s seconds with no output"
+        log.testFail(msg % (self.last_test, self.timeout))
+
+        # kill process to get a stack
+        self.runner.stop(sig=signal.SIGABRT)
+
+
+def run_desktop_reftests(parser, options, args):
+    kwargs = {}
+    if options.marionette:
+        host, port = options.marionette.split(':')
+        kwargs['host'] = host
+        kwargs['port'] = int(port)
+    marionette = Marionette.getMarionetteOrExit(**kwargs)
+
+    reftest = B2GDesktopReftest(marionette)
+
+    options = ReftestOptions.verifyCommonOptions(parser, options, reftest)
+    if options == None:
+        sys.exit(1)
+
+    # add a -bin suffix if b2g-bin exists, but just b2g was specified
+    if options.app[-4:] != '-bin':
+        if os.path.isfile("%s-bin" % options.app):
+            options.app = "%s-bin" % options.app
+
+    if options.desktop and not options.profile:
+        raise Exception("must specify --profile when specifying --desktop")
+
+    sys.exit(reftest.run_tests(args[0], options))
--- a/layout/tools/reftest/b2g_start_script.js
+++ b/layout/tools/reftest/b2g_start_script.js
@@ -1,15 +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/. */
 
-let serverAddr = __marionetteParams[0];
-let serverPort = __marionetteParams[1];
-
 function setDefaultPrefs() {
     // This code sets the preferences for extension-based reftest; for
     // command-line based reftest they are set in function handler_handle in
     // reftest-cmdline.js.  These two locations should stay in sync.
     //
     // FIXME: These should be in only one place.
     var prefs = Cc["@mozilla.org/preferences-service;1"].
                 getService(Ci.nsIPrefService);
@@ -39,16 +36,22 @@ function setDefaultPrefs() {
     // Checking whether two files are the same is slow on Windows.
     // Setting this pref makes tests run much faster there.
     branch.setBoolPref("security.fileuri.strict_origin_policy", false);
     // Disable the thumbnailing service
     branch.setBoolPref("browser.pagethumbnails.capturing_disabled", true);
 }
 
 function setPermissions() {
+  if (__marionetteParams.length < 2) {
+    return;
+  }
+
+  let serverAddr = __marionetteParams[0];
+  let serverPort = __marionetteParams[1];
   let perms = Cc["@mozilla.org/permissionmanager;1"]
               .getService(Ci.nsIPermissionManager);
   let ioService = Cc["@mozilla.org/network/io-service;1"]
                   .getService(Ci.nsIIOService);
   let uri = ioService.newURI("http://" + serverAddr + ":" + serverPort, null, null);
   perms.add(uri, "allowXULXBL", Ci.nsIPermissionManager.ALLOW_ACTION);
 }
 
--- a/layout/tools/reftest/reftest.js
+++ b/layout/tools/reftest/reftest.js
@@ -389,16 +389,25 @@ function InitAndStartRefTests()
     gIOService = CC[IO_SERVICE_CONTRACTID].getService(CI.nsIIOService);
     gDebug = CC[DEBUG_CONTRACTID].getService(CI.nsIDebug2);
 
     RegisterProcessCrashObservers();
 
     if (gRemote) {
         gServer = null;
     } else {
+        // not all gecko applications autoregister xpcom components
+        if (CC["@mozilla.org/server/jshttp;1"] === undefined) {
+            var file = CC["@mozilla.org/file/directory_service;1"].
+                        getService(CI.nsIProperties).get("ProfD", CI.nsIFile);
+            file.appendRelativePath("extensions/reftest@mozilla.org/chrome.manifest");
+
+            registrar = Components.manager.QueryInterface(CI.nsIComponentRegistrar);
+            registrar.autoRegister(file);
+        }
         gServer = CC["@mozilla.org/server/jshttp;1"].
                       createInstance(CI.nsIHttpServer);
     }
     try {
         if (gServer)
             StartHTTPServer();
     } catch (ex) {
         //gBrowser.loadURI('data:text/plain,' + ex);
--- a/layout/tools/reftest/remotereftest.py
+++ b/layout/tools/reftest/remotereftest.py
@@ -103,17 +103,17 @@ class RemoteOptions(ReftestOptions):
         self.set_defaults(**defaults)
 
     def verifyRemoteOptions(self, options):
         if options.runTestsInParallel:
             self.error("Cannot run parallel tests here")
 
         # Ensure our defaults are set properly for everything we can infer
         if not options.remoteTestRoot:
-            options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + '/reftest'
+            options.remoteTestRoot = self.automation._devicemanager.getDeviceRoot() + '/reftest'
         options.remoteProfile = options.remoteTestRoot + "/profile"
 
         # Verify that our remotewebserver is set properly
         if (options.remoteWebServer == None or
             options.remoteWebServer == '127.0.0.1'):
             print "ERROR: Either you specified the loopback for the remote webserver or ",
             print "your local IP cannot be detected.  Please provide the local ip in --remote-webserver"
             return None
@@ -161,70 +161,70 @@ class RemoteOptions(ReftestOptions):
 
         # httpd-path is specified by standard makefile targets and may be specified
         # on the command line to select a particular version of httpd.js. If not
         # specified, try to select the one from hostutils.zip, as required in bug 882932.
         if not options.httpdPath:
             options.httpdPath = os.path.join(options.utilityPath, "components")
 
         # TODO: Copied from main, but I think these are no longer used in a post xulrunner world
-        #options.xrePath = options.remoteTestRoot + self._automation._product + '/xulrunner'
-        #options.utilityPath = options.testRoot + self._automation._product + '/bin'
+        #options.xrePath = options.remoteTestRoot + self.automation._product + '/xulrunner'
+        #options.utilityPath = options.testRoot + self.automation._product + '/bin'
         return options
 
 class ReftestServer:
     """ Web server used to serve Reftests, for closer fidelity to the real web.
         It is virtually identical to the server used in mochitest and will only
         be used for running reftests remotely.
         Bug 581257 has been filed to refactor this wrapper around httpd.js into
         it's own class and use it in both remote and non-remote testing. """
 
     def __init__(self, automation, options, scriptDir):
-        self._automation = automation
+        self.automation = automation
         self._utilityPath = options.utilityPath
         self._xrePath = options.xrePath
         self._profileDir = options.serverProfilePath
         self.webServer = options.remoteWebServer
         self.httpPort = options.httpPort
         self.scriptDir = scriptDir
         self.pidFile = options.pidFile
         self._httpdPath = os.path.abspath(options.httpdPath)
         self.shutdownURL = "http://%(server)s:%(port)s/server/shutdown" % { "server" : self.webServer, "port" : self.httpPort }
 
     def start(self):
         "Run the Refest server, returning the process ID of the server."
 
-        env = self._automation.environment(xrePath = self._xrePath)
+        env = self.automation.environment(xrePath = self._xrePath)
         env["XPCOM_DEBUG_BREAK"] = "warn"
-        if self._automation.IS_WIN32:
+        if self.automation.IS_WIN32:
             env["PATH"] = env["PATH"] + ";" + self._xrePath
 
         args = ["-g", self._xrePath,
                 "-v", "170",
                 "-f", os.path.join(self._httpdPath, "httpd.js"),
                 "-e", "const _PROFILE_PATH = '%(profile)s';const _SERVER_PORT = '%(port)s'; const _SERVER_ADDR ='%(server)s';" %
                        {"profile" : self._profileDir.replace('\\', '\\\\'), "port" : self.httpPort, "server" : self.webServer },
                 "-f", os.path.join(self.scriptDir, "server.js")]
 
         xpcshell = os.path.join(self._utilityPath,
-                                "xpcshell" + self._automation.BIN_SUFFIX)
+                                "xpcshell" + self.automation.BIN_SUFFIX)
 
         if not os.access(xpcshell, os.F_OK):
             raise Exception('xpcshell not found at %s' % xpcshell)
-        if self._automation.elf_arm(xpcshell):
+        if self.automation.elf_arm(xpcshell):
             raise Exception('xpcshell at %s is an ARM binary; please use '
                             'the --utility-path argument to specify the path '
                             'to a desktop version.' % xpcshell)
 
-        self._process = self._automation.Process([xpcshell] + args, env = env)
+        self._process = self.automation.Process([xpcshell] + args, env = env)
         pid = self._process.pid
         if pid < 0:
             print "TEST-UNEXPECTED-FAIL | remotereftests.py | Error starting server."
             return 2
-        self._automation.log.info("INFO | remotereftests.py | Server pid: %d", pid)
+        self.automation.log.info("INFO | remotereftests.py | Server pid: %d", pid)
 
         if (self.pidFile != ""):
             f = open(self.pidFile + ".xpcshell.pid", 'w')
             f.write("%s" % pid)
             f.close()
 
     def ensureReady(self, timeout):
         assert timeout >= 0
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -1,32 +1,37 @@
 # 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/.
 
 """
 Runs the reftest test harness.
 """
 
-import re
-import sys
-import shutil
+from optparse import OptionParser
+import collections
+import json
+import multiprocessing
 import os
-import threading
+import re
+import shutil
 import subprocess
-import collections
-import multiprocessing
+import sys
+import threading
+
 SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
 sys.path.insert(0, SCRIPT_DIRECTORY)
 
 from automation import Automation
-from automationutils import *
-from optparse import OptionParser
-from tempfile import mkdtemp
-
+from automationutils import (
+        addCommonOptions,
+        getDebuggerInfo,
+        isURL,
+        processLeakLog
+)
 import mozprofile
 
 def categoriesToRegex(categoryList):
   return "\\(" + ', '.join(["(?P<%s>\\d+) %s" % c for c in categoryList]) + "\\)"
 summaryLines = [('Successful', [('pass', 'pass'), ('loadOnly', 'load only')]),
                 ('Unexpected', [('fail', 'unexpected fail'),
                                 ('pass', 'unexpected pass'),
                                 ('asserts', 'unexpected asserts'),
@@ -124,21 +129,21 @@ class RefTest(object):
         if os.path.exists(defaultManifestPath):
           path = defaultManifestPath
     return path
 
   def makeJSString(self, s):
     return '"%s"' % re.sub(r'([\\"])', r'\\\1', s)
 
   def createReftestProfile(self, options, manifest, server='localhost',
-                           special_powers=True):
+                           special_powers=True, profile_to_clone=None):
     """
       Sets up a profile for reftest.
       'manifest' is the path to the reftest.list file we want to test with.  This is used in
-      the remote subclass in remotereftest.py so we can write it to a preference for the 
+      the remote subclass in remotereftest.py so we can write it to a preference for the
       bootstrap extension.
     """
 
     locations = mozprofile.permissions.ServerLocations()
     locations.add_host(server, port=0)
     locations.add_host('<file>', port=0)
 
     # Set preferences for communication between our command line arguments
@@ -179,34 +184,37 @@ class RefTest(object):
     if os.path.isdir(distExtDir):
       for f in os.listdir(distExtDir):
         addons.append(os.path.join(distExtDir, f))
 
     # Install custom extensions.
     for f in options.extensionsToInstall:
       addons.append(self.getFullPath(f))
 
-    profile = mozprofile.profile.Profile(
-        addons=addons,
-        preferences=prefs,
-        locations=locations,
-    )
+    kwargs = { 'addons': addons,
+               'preferences': prefs,
+               'locations': locations }
+    if profile_to_clone:
+        profile = mozprofile.Profile.clone(profile_to_clone, **kwargs)
+    else:
+        profile = mozprofile.Profile(**kwargs)
+
     self.copyExtraFilesToProfile(options, profile)
     return profile
 
   def buildBrowserEnv(self, options, profileDir):
     browserEnv = self.automation.environment(xrePath = options.xrePath)
     browserEnv["XPCOM_DEBUG_BREAK"] = "stack"
 
     for v in options.environment:
       ix = v.find("=")
       if ix <= 0:
         print "Error: syntax error in --setenv=" + v
         return None
-      browserEnv[v[:ix]] = v[ix + 1:]    
+      browserEnv[v[:ix]] = v[ix + 1:]
 
     # Enable leaks detection to its own log file.
     self.leakLogFile = os.path.join(profileDir, "runreftest_leaks.log")
     browserEnv["XPCOM_MEM_BLOAT_LOG"] = self.leakLogFile
     return browserEnv
 
   def cleanup(self, profileDir):
     if profileDir:
@@ -333,68 +341,68 @@ class RefTest(object):
         shutil.copytree(abspath, dest)
       else:
         self.automation.log.warning("WARNING | runreftest.py | Failed to copy %s to profile", abspath)
         continue
 
 
 class ReftestOptions(OptionParser):
 
-  def __init__(self, automation):
-    self._automation = automation
+  def __init__(self, automation=None):
+    self.automation = automation or Automation()
     OptionParser.__init__(self)
     defaults = {}
 
     # we want to pass down everything from automation.__all__
-    addCommonOptions(self, 
-                     defaults=dict(zip(self._automation.__all__, 
-                            [getattr(self._automation, x) for x in self._automation.__all__])))
-    self._automation.addCommonOptions(self)
+    addCommonOptions(self,
+                     defaults=dict(zip(self.automation.__all__,
+                            [getattr(self.automation, x) for x in self.automation.__all__])))
+    self.automation.addCommonOptions(self)
     self.add_option("--appname",
                     action = "store", type = "string", dest = "app",
                     default = os.path.join(SCRIPT_DIRECTORY, automation.DEFAULT_APP),
                     help = "absolute path to application, overriding default")
     self.add_option("--extra-profile-file",
                     action = "append", dest = "extraProfileFiles",
                     default = [],
                     help = "copy specified files/dirs to testing profile")
-    self.add_option("--timeout",              
-                    action = "store", dest = "timeout", type = "int", 
+    self.add_option("--timeout",
+                    action = "store", dest = "timeout", type = "int",
                     default = 5 * 60, # 5 minutes per bug 479518
                     help = "reftest will timeout in specified number of seconds. [default %default s].")
     self.add_option("--leak-threshold",
                     action = "store", type = "int", dest = "leakThreshold",
                     default = 0,
                     help = "fail if the number of bytes leaked through "
                            "refcounted objects (or bytes in classes with "
                            "MOZ_COUNT_CTOR and MOZ_COUNT_DTOR) is greater "
                            "than the given number")
     self.add_option("--utility-path",
                     action = "store", type = "string", dest = "utilityPath",
-                    default = self._automation.DIST_BIN,
+                    default = self.automation.DIST_BIN,
                     help = "absolute path to directory containing utility "
                            "programs (xpcshell, ssltunnel, certutil)")
-    defaults["utilityPath"] = self._automation.DIST_BIN
+    defaults["utilityPath"] = self.automation.DIST_BIN
 
     self.add_option("--total-chunks",
                     type = "int", dest = "totalChunks",
                     help = "how many chunks to split the tests up into")
     defaults["totalChunks"] = None
 
     self.add_option("--this-chunk",
                     type = "int", dest = "thisChunk",
                     help = "which chunk to run between 1 and --total-chunks")
     defaults["thisChunk"] = None
 
     self.add_option("--log-file",
                     action = "store", type = "string", dest = "logFile",
                     default = None,
                     help = "file to log output to in addition to stdout")
     defaults["logFile"] = None
- 
+
     self.add_option("--skip-slow-tests",
                     dest = "skipSlowTests", action = "store_true",
                     help = "skip tests marked as slow when running")
     defaults["skipSlowTests"] = False
 
     self.add_option("--ignore-window-size",
                     dest = "ignoreWindowSize", action = "store_true",
                     help = "ignore the window size, which may cause spurious failures and passes")
--- a/layout/tools/reftest/runreftestb2g.py
+++ b/layout/tools/reftest/runreftestb2g.py
@@ -4,29 +4,28 @@
 
 import ConfigParser
 import os
 import sys
 import tempfile
 import traceback
 
 # We need to know our current directory so that we can serve our test files from it.
-SCRIPT_DIRECTORY = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
-sys.path.insert(0, SCRIPT_DIRECTORY)
+here = os.path.abspath(os.path.dirname(__file__))
 
 from automation import Automation
 from b2gautomation import B2GRemoteAutomation
+from b2g_desktop import run_desktop_reftests
 from runreftest import RefTest
 from runreftest import ReftestOptions
 from remotereftest import ReftestServer
 
 from mozdevice import DeviceManagerADB, DMError
 from marionette import Marionette
 
-
 class B2GOptions(ReftestOptions):
 
     def __init__(self, automation=None, **kwargs):
         defaults = {}
         if not automation:
             automation = B2GRemoteAutomation(None, "fennec", context_chrome=True)
 
         ReftestOptions.__init__(self, automation)
@@ -106,40 +105,49 @@ class B2GOptions(ReftestOptions):
         self.add_option('--busybox', action='store',
                         type='string', dest='busybox',
                         help="Path to busybox binary to install on device")
         defaults['busybox'] = None
         self.add_option("--httpd-path", action = "store",
                     type = "string", dest = "httpdPath",
                     help = "path to the httpd.js file")
         defaults["httpdPath"] = None
+        self.add_option("--profile", action="store",
+                    type="string", dest="profile",
+                    help="for desktop testing, the path to the "
+                         "gaia profile to use")
+        defaults["profile"] = None
+        self.add_option("--desktop", action="store_true",
+                        dest="desktop",
+                        help="Run the tests on a B2G desktop build")
+        defaults["desktop"] = False
         defaults["remoteTestRoot"] = "/data/local/tests"
         defaults["logFile"] = "reftest.log"
         defaults["autorun"] = True
         defaults["closeWhenDone"] = True
         defaults["testPath"] = ""
         defaults["runTestsInParallel"] = False
 
         self.set_defaults(**defaults)
 
     def verifyRemoteOptions(self, options):
         if options.runTestsInParallel:
             self.error("Cannot run parallel tests here")
 
         if not options.remoteTestRoot:
-            options.remoteTestRoot = self._automation._devicemanager.getDeviceRoot() + "/reftest"
+            options.remoteTestRoot = self.automation._devicemanager.getDeviceRoot() + "/reftest"
         options.remoteProfile = options.remoteTestRoot + "/profile"
 
-        productRoot = options.remoteTestRoot + "/" + self._automation._product
-        if options.utilityPath == self._automation.DIST_BIN:
+        productRoot = options.remoteTestRoot + "/" + self.automation._product
+        if options.utilityPath == self.automation.DIST_BIN:
             options.utilityPath = productRoot + "/bin"
 
         if options.remoteWebServer == None:
             if os.name != "nt":
-                options.remoteWebServer = self._automation.getLanIp()
+                options.remoteWebServer = self.automation.getLanIp()
             else:
                 print "ERROR: you must specify a --remote-webserver=<ip address>\n"
                 return None
 
         options.webServer = options.remoteWebServer
 
         if options.geckoPath and not options.emulator:
             self.error("You must specify --emulator if you specify --gecko-path")
@@ -204,43 +212,40 @@ class ProfileConfigParser(ConfigParser.R
             for (key, value) in self._sections[section].items():
                 if key == "__name__":
                     continue
                 if (value is not None) or (self._optcre == self.OPTCRE):
                     key = "=".join((key, str(value).replace('\n', '\n\t')))
                 fp.write("%s\n" % (key))
             fp.write("\n")
 
+class B2GRemoteReftest(RefTest):
 
-class B2GReftest(RefTest):
-
-    _automation = None
     _devicemanager = None
     localProfile = None
     remoteApp = ''
     profile = None
 
     def __init__(self, automation, devicemanager, options, scriptDir):
-        self._automation = automation
-        RefTest.__init__(self, self._automation)
+        RefTest.__init__(self, automation)
         self._devicemanager = devicemanager
         self.runSSLTunnel = False
         self.remoteTestRoot = options.remoteTestRoot
         self.remoteProfile = options.remoteProfile
-        self._automation.setRemoteProfile(self.remoteProfile)
+        self.automation.setRemoteProfile(self.remoteProfile)
         self.localLogName = options.localLogName
         self.remoteLogFile = options.remoteLogFile
         self.bundlesDir = '/system/b2g/distribution/bundles'
         self.userJS = '/data/local/user.js'
         self.remoteMozillaPath = '/data/b2g/mozilla'
         self.remoteProfilesIniPath = os.path.join(self.remoteMozillaPath, 'profiles.ini')
         self.originalProfilesIni = None
         self.scriptDir = scriptDir
         self.SERVER_STARTUP_TIMEOUT = 90
-        if self._automation.IS_DEBUG_BUILD:
+        if self.automation.IS_DEBUG_BUILD:
             self.SERVER_STARTUP_TIMEOUT = 180
 
     def cleanup(self, profileDir):
         # Pull results back from device
         if (self.remoteLogFile):
             try:
                 self._devicemanager.getFile(self.remoteLogFile, self.localLogName)
             except:
@@ -254,34 +259,34 @@ class B2GReftest(RefTest):
                 self._devicemanager._checkCmdAs(['shell', 'rm', '-rf',
                                                  os.path.join(self.bundlesDir, filename)])
             except DMError:
                 pass
 
         # Restore the original profiles.ini.
         if self.originalProfilesIni:
             try:
-                if not self._automation._is_emulator:
+                if not self.automation._is_emulator:
                     self.restoreProfilesIni()
                 os.remove(self.originalProfilesIni)
             except:
                 pass
 
-        if not self._automation._is_emulator:
+        if not self.automation._is_emulator:
             self._devicemanager.removeFile(self.remoteLogFile)
             self._devicemanager.removeDir(self.remoteProfile)
             self._devicemanager.removeDir(self.remoteTestRoot)
 
             # Restore the original user.js.
             self._devicemanager._checkCmdAs(['shell', 'rm', '-f', self.userJS])
             self._devicemanager._checkCmdAs(['shell', 'dd', 'if=%s.orig' % self.userJS, 'of=%s' % self.userJS])
 
             # We've restored the original profile, so reboot the device so that
             # it gets picked up.
-            self._automation.rebootDevice()
+            self.automation.rebootDevice()
 
         RefTest.cleanup(self, profileDir)
         if getattr(self, 'pidFile', '') != '':
             try:
                 os.remove(self.pidFile)
                 os.remove(self.pidFile + ".xpcshell.pid")
             except:
                 print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % self.pidFile
@@ -312,18 +317,18 @@ class B2GReftest(RefTest):
             localAutomation.IS_LINUX = True
             localAutomation.UNIXISH = True
         elif hostos in ['win32', 'win64']:
             localAutomation.BIN_SUFFIX = ".exe"
             localAutomation.IS_WIN32 = True
 
         paths = [options.xrePath,
                  localAutomation.DIST_BIN,
-                 self._automation._product,
-                 os.path.join('..', self._automation._product)]
+                 self.automation._product,
+                 os.path.join('..', self.automation._product)]
         options.xrePath = self.findPath(paths)
         if options.xrePath == None:
             print "ERROR: unable to find xulrunner path for %s, please specify with --xre-path" % (os.name)
             sys.exit(1)
         paths.append("bin")
         paths.append(os.path.join("..", "bin"))
 
         xpcshell = "xpcshell"
@@ -333,17 +338,17 @@ class B2GReftest(RefTest):
         if (options.utilityPath):
             paths.insert(0, options.utilityPath)
         options.utilityPath = self.findPath(paths, xpcshell)
         if options.utilityPath == None:
             print "ERROR: unable to find utility path for %s, please specify with --utility-path" % (os.name)
             sys.exit(1)
 
         xpcshell = os.path.join(options.utilityPath, xpcshell)
-        if self._automation.elf_arm(xpcshell):
+        if self.automation.elf_arm(xpcshell):
             raise Exception('xpcshell at %s is an ARM binary; please use '
                             'the --utility-path argument to specify the path '
                             'to a desktop version.' % xpcshell)
 
         options.serverProfilePath = tempfile.mkdtemp()
         self.server = ReftestServer(localAutomation, options, self.scriptDir)
         retVal = self.server.start()
         if retVal:
@@ -362,17 +367,16 @@ class B2GReftest(RefTest):
         options.utilityPath = remoteUtilityPath
         options.profilePath = remoteProfilePath
         return 0
 
     def stopWebServer(self, options):
         if hasattr(self, 'server'):
             self.server.stop()
 
-
     def restoreProfilesIni(self):
         # restore profiles.ini on the device to its previous state
         if not self.originalProfilesIni or not os.access(self.originalProfilesIni, os.F_OK):
             raise DMError('Unable to install original profiles.ini; file not found: %s',
                           self.originalProfilesIni)
 
         self._devicemanager.pushFile(self.originalProfilesIni, self.remoteProfilesIniPath)
 
@@ -465,16 +469,17 @@ class B2GReftest(RefTest):
             self._devicemanager.pushDir(profileDir, options.remoteProfile)
         except DMError:
             print "Automation Error: Failed to copy extra files to device"
             raise
 
     def getManifestPath(self, path):
         return path
 
+
 def run_remote_reftests(parser, options, args):
     auto = B2GRemoteAutomation(None, "fennec", context_chrome=True)
 
     # create our Marionette instance
     kwargs = {}
     if options.emulator:
         kwargs['emulator'] = options.emulator
         auto.setEmulator(True)
@@ -520,37 +525,37 @@ def run_remote_reftests(parser, options,
         parts = dm.getInfo('screen')['screen'][0].split()
         width = int(parts[0].split(':')[1])
         height = int(parts[1].split(':')[1])
         if (width < 1366 or height < 1050):
             print "ERROR: Invalid screen resolution %sx%s, please adjust to 1366x1050 or higher" % (width, height)
             return 1
 
     auto.setProduct("b2g")
-    auto.test_script = os.path.join(SCRIPT_DIRECTORY, 'b2g_start_script.js')
+    auto.test_script = os.path.join(here, 'b2g_start_script.js')
     auto.test_script_args = [options.remoteWebServer, options.httpPort]
     auto.logFinish = "REFTEST TEST-START | Shutdown"
 
-    reftest = B2GReftest(auto, dm, options, SCRIPT_DIRECTORY)
+    reftest = B2GRemoteReftest(auto, dm, options, here)
     options = parser.verifyCommonOptions(options, reftest)
 
     logParent = os.path.dirname(options.remoteLogFile)
     dm.mkDir(logParent);
     auto.setRemoteLog(options.remoteLogFile)
     auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
 
     # Hack in a symbolic link for jsreftest
-    os.system("ln -s %s %s" % (os.path.join('..', 'jsreftest'), os.path.join(SCRIPT_DIRECTORY, 'jsreftest')))
+    os.system("ln -s %s %s" % (os.path.join('..', 'jsreftest'), os.path.join(here, 'jsreftest')))
 
     # Dynamically build the reftest URL if possible, beware that args[0] should exist 'inside' the webroot
     manifest = args[0]
-    if os.path.exists(os.path.join(SCRIPT_DIRECTORY, args[0])):
+    if os.path.exists(os.path.join(here, args[0])):
         manifest = "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, args[0])
     elif os.path.exists(args[0]):
-        manifestPath = os.path.abspath(args[0]).split(SCRIPT_DIRECTORY)[1].strip('/')
+        manifestPath = os.path.abspath(args[0]).split(here)[1].strip('/')
         manifest = "http://%s:%s/%s" % (options.remoteWebServer, options.httpPort, manifestPath)
     else:
         print "ERROR: Could not find test manifest '%s'" % manifest
         return 1
 
     # Start the webserver
     retVal = 1
     try:
@@ -577,14 +582,17 @@ def run_remote_reftests(parser, options,
         return 1
 
     reftest.stopWebServer(options)
     return retVal
 
 def main(args=sys.argv[1:]):
     parser = B2GOptions()
     options, args = parser.parse_args(args)
+
+    if options.desktop:
+        return run_desktop_reftests(parser, options, args)
     return run_remote_reftests(parser, options, args)
 
 
 if __name__ == "__main__":
     sys.exit(main())
 
--- a/media/mtransport/third_party/nICEr/src/ice/ice_socket.c
+++ b/media/mtransport/third_party/nICEr/src/ice/ice_socket.c
@@ -76,17 +76,17 @@ static void nr_ice_socket_readable_cb(NR
     if (len_s > (size_t)INT_MAX)
       return;
 
     len = (int)len_s;
 
 #ifdef USE_TURN
   re_process:
 #endif /* USE_TURN */
-    r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Read %d bytes",sock->ctx->label,len);
+    r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Read %d bytes from %s",sock->ctx->label,len,addr.as_string);
 
     /* First question: is this STUN or not? */
     is_stun=nr_is_stun_message(buf,len);
 
     if(is_stun){
       is_req=nr_is_stun_request_message(buf,len);
       is_ind=is_req?0:nr_is_stun_indication_message(buf,len);
 
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
@@ -18,16 +18,17 @@
 #include "nsIPrefService.h"
 #include "nsIPrefBranch.h"
 #include "nsThreadUtils.h"
 #ifdef MOZILLA_INTERNAL_API
 #include "Latency.h"
 #endif
 
 #include "webrtc/voice_engine/include/voe_errors.h"
+#include "webrtc/system_wrappers/interface/clock.h"
 
 #ifdef MOZ_WIDGET_ANDROID
 #include "AndroidJNIWrapper.h"
 #endif
 
 namespace mozilla {
 
 static const char* logTag ="WebrtcAudioSessionConduit";
@@ -140,23 +141,73 @@ WebrtcAudioConduit::~WebrtcAudioConduit(
 bool WebrtcAudioConduit::GetLocalSSRC(unsigned int* ssrc) {
   return !mPtrRTP->GetLocalSSRC(mChannel, *ssrc);
 }
 
 bool WebrtcAudioConduit::GetRemoteSSRC(unsigned int* ssrc) {
   return !mPtrRTP->GetRemoteSSRC(mChannel, *ssrc);
 }
 
-bool WebrtcAudioConduit::GetReceivedJitter(unsigned int* jitterMs) {
+bool WebrtcAudioConduit::GetRTPJitter(unsigned int* jitterMs) {
   unsigned int maxJitterMs;
   unsigned int discardedPackets;
   return !mPtrRTP->GetRTPStatistics(mChannel, *jitterMs, maxJitterMs,
                                     discardedPackets);
 }
 
+DOMHighResTimeStamp
+NTPtoDOMHighResTimeStamp(uint32_t ntpHigh, uint32_t ntpLow) {
+  return (uint32_t(ntpHigh - webrtc::kNtpJan1970) +
+          double(ntpLow) / webrtc::kMagicNtpFractionalUnit) * 1000;
+}
+
+bool WebrtcAudioConduit::GetRTCPReceiverReport(DOMHighResTimeStamp* timestamp,
+                                               unsigned int* jitterMs,
+                                               unsigned int* packetsReceived,
+                                               uint64_t* bytesReceived) {
+  unsigned int ntpHigh, ntpLow;
+  unsigned int rtpTimestamp, playoutTimestamp;
+  unsigned int packetsSent;
+  unsigned int bytesSent32;
+  unsigned short fractionLost;
+  unsigned int cumulativeLost;
+  bool result = !mPtrRTP->GetRemoteRTCPData(mChannel, ntpHigh, ntpLow,
+                                            rtpTimestamp, playoutTimestamp,
+                                            packetsSent, bytesSent32,
+                                            jitterMs,
+                                            &fractionLost, &cumulativeLost);
+  if (result) {
+    *timestamp = NTPtoDOMHighResTimeStamp(ntpHigh, ntpLow);
+    *packetsReceived = (packetsSent >= cumulativeLost) ?
+                       (packetsSent - cumulativeLost) : 0;
+    *bytesReceived = (packetsSent ?
+                      (bytesSent32 / packetsSent) : 0) * (*packetsReceived);
+  }
+  return result;
+}
+
+bool WebrtcAudioConduit::GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
+                                             unsigned int* packetsSent,
+                                             uint64_t* bytesSent) {
+  unsigned int ntpHigh, ntpLow;
+  unsigned int rtpTimestamp, playoutTimestamp;
+  unsigned int bytesSent32;
+  unsigned int jitterMs;
+  unsigned short fractionLost;
+  bool result = !mPtrRTP->GetRemoteRTCPData(mChannel, ntpHigh, ntpLow,
+                                            rtpTimestamp, playoutTimestamp,
+                                            *packetsSent, bytesSent32,
+                                            &jitterMs, &fractionLost);
+  if (result) {
+    *timestamp = NTPtoDOMHighResTimeStamp(ntpHigh, ntpLow);
+    *bytesSent = bytesSent32;
+  }
+  return result;
+}
+
 /*
  * WebRTCAudioConduit Implementation
  */
 MediaConduitErrorCode WebrtcAudioConduit::Init(WebrtcAudioConduit *other)
 {
   CSFLogDebug(logTag,  "%s this=%p other=%p", __FUNCTION__, this, other);
 
   if (other) {
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.h
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.h
@@ -33,16 +33,21 @@
  using webrtc::VoEVideoSync;
 
 /** This file hosts several structures identifying different aspects
  * of a RTP Session.
  */
 
 namespace mozilla {
 
+// Helper function
+
+DOMHighResTimeStamp
+NTPtoDOMHighResTimeStamp(uint32_t ntpHigh, uint32_t ntpLow);
+
 /**
  * Concrete class for Audio session. Hooks up
  *  - media-source and target to external transport
  */
 class WebrtcAudioConduit:public AudioSessionConduit
 	      		            ,public webrtc::Transport
 {
 public:
@@ -167,17 +172,24 @@ public:
   virtual ~WebrtcAudioConduit();
 
   MediaConduitErrorCode Init(WebrtcAudioConduit *other);
 
   int GetChannel() { return mChannel; }
   webrtc::VoiceEngine* GetVoiceEngine() { return mVoiceEngine; }
   bool GetLocalSSRC(unsigned int* ssrc);
   bool GetRemoteSSRC(unsigned int* ssrc);
-  bool GetReceivedJitter(unsigned int* jitterMs);
+  bool GetRTPJitter(unsigned int* jitterMs);
+  bool GetRTCPReceiverReport(DOMHighResTimeStamp* timestamp,
+                             unsigned int* jitterMs,
+                             unsigned int* packetsReceived,
+                             uint64_t* bytesReceived);
+  bool GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
+                           unsigned int* packetsSent,
+                           uint64_t* bytesSent);
 
 private:
   WebrtcAudioConduit(const WebrtcAudioConduit& other) MOZ_DELETE;
   void operator=(const WebrtcAudioConduit& other) MOZ_DELETE;
 
   //Local database of currently applied receive codecs
   typedef std::vector<AudioCodecConfig* > RecvCodecList;
 
--- a/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
+++ b/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
@@ -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/. */
 
 #ifndef MEDIA_CONDUIT_ABSTRACTION_
 #define MEDIA_CONDUIT_ABSTRACTION_
 
 #include "nsISupportsImpl.h"
 #include "nsXPCOM.h"
+#include "nsDOMNavigationTiming.h"
 #include "mozilla/RefPtr.h"
 #include "CodecConfig.h"
 #include "VideoTypes.h"
 #include "MediaConduitErrors.h"
 
 #include <vector>
 
 namespace mozilla {
@@ -133,17 +134,28 @@ public:
    * @param aTransport: Reference to the concrete teansport implementation
    * Note: Multiple invocations of this call , replaces existing transport with
    * with the new one.
    */
   virtual MediaConduitErrorCode AttachTransport(RefPtr<TransportInterface> aTransport) = 0;
 
   virtual bool GetLocalSSRC(unsigned int* ssrc) = 0;
   virtual bool GetRemoteSSRC(unsigned int* ssrc) = 0;
-  virtual bool GetReceivedJitter(unsigned int* jitterMs) = 0;
+
+  /**
+   * Functions returning stats needed by w3c stats model.
+   */
+  virtual bool GetRTPJitter(unsigned int* jitterMs) = 0;
+  virtual bool GetRTCPReceiverReport(DOMHighResTimeStamp* timestamp,
+                                     unsigned int* jitterMs,
+                                     unsigned int* packetsReceived,
+                                     uint64_t* bytesReceived) = 0;
+  virtual bool GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
+                                   unsigned int* packetsSent,
+                                   uint64_t* bytesSent) = 0;
 
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSessionConduit)
 
 };
 
 
 /**
  * MediaSessionConduit for video
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.cpp
@@ -140,29 +140,84 @@ WebrtcVideoConduit::~WebrtcVideoConduit(
 bool WebrtcVideoConduit::GetLocalSSRC(unsigned int* ssrc) {
   return !mPtrRTP->GetLocalSSRC(mChannel, *ssrc);
 }
 
 bool WebrtcVideoConduit::GetRemoteSSRC(unsigned int* ssrc) {
   return !mPtrRTP->GetRemoteSSRC(mChannel, *ssrc);
 }
 
-bool WebrtcVideoConduit::GetReceivedJitter(unsigned int* jitterMs) {
+bool WebrtcVideoConduit::GetRTPJitter(unsigned int* jitterMs) {
+  unsigned int ntpHigh, ntpLow;
+  unsigned int packetsSent, bytesSent;
   unsigned short fractionLost;
   unsigned int cumulativeLost;
   unsigned extendedMax;
   int rttMs;
-  return !mPtrRTP->GetReceivedRTCPStatistics(mChannel,
+  // GetReceivedRTCPStatistics is a poorly named GetRTPStatistics variant
+  return !mPtrRTP->GetReceivedRTCPStatistics(mChannel, ntpHigh, ntpLow,
+                                             packetsSent, bytesSent,
                                              fractionLost,
                                              cumulativeLost,
                                              extendedMax,
                                              *jitterMs,
                                              rttMs);
 }
 
+bool WebrtcVideoConduit::GetRTCPReceiverReport(DOMHighResTimeStamp* timestamp,
+                                               unsigned int* jitterMs,
+                                               unsigned int* packetsReceived,
+                                               uint64_t* bytesReceived) {
+  unsigned int ntpHigh, ntpLow;
+  unsigned int packetsSent;
+  unsigned int bytesSent32;
+  unsigned short fractionLost;
+  unsigned int cumulativeLost;
+  unsigned extendedMax;
+  int rttMs;
+  bool result = !mPtrRTP->GetSentRTCPStatistics(mChannel, ntpHigh, ntpLow,
+                                                bytesSent32, packetsSent,
+                                                fractionLost,
+                                                cumulativeLost,
+                                                extendedMax,
+                                                *jitterMs,
+                                                rttMs);
+  if (result) {
+    *timestamp = NTPtoDOMHighResTimeStamp(ntpHigh, ntpLow);
+    *packetsReceived = (packetsSent >= cumulativeLost) ?
+                       (packetsSent - cumulativeLost) : 0;
+    *bytesReceived = (packetsSent ?
+                      (bytesSent32 / packetsSent) : 0) * (*packetsReceived);
+  }
+  return result;
+}
+
+bool WebrtcVideoConduit::GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
+                                             unsigned int* packetsSent,
+                                             uint64_t* bytesSent) {
+  unsigned int ntpHigh, ntpLow;
+  unsigned int bytesSent32;
+  unsigned int jitterMs;
+  unsigned short fractionLost;
+  unsigned int cumulativeLost;
+  unsigned extendedMax;
+  int rttMs;
+  bool result = !mPtrRTP->GetReceivedRTCPStatistics(mChannel, ntpHigh, ntpLow,
+                                                    bytesSent32, *packetsSent,
+                                                    fractionLost,
+                                                    cumulativeLost,
+                                                    jitterMs, extendedMax,
+                                                    rttMs);
+  if (result) {
+    *timestamp = NTPtoDOMHighResTimeStamp(ntpHigh, ntpLow);
+    *bytesSent = bytesSent32;
+  }
+  return result;
+}
+
 /**
  * Peforms intialization of the MANDATORY components of the Video Engine
  */
 MediaConduitErrorCode WebrtcVideoConduit::Init(WebrtcVideoConduit *other)
 {
   CSFLogDebug(logTag,  "%s this=%p other=%p", __FUNCTION__, this, other);
 
   if (other) {
--- a/media/webrtc/signaling/src/media-conduit/VideoConduit.h
+++ b/media/webrtc/signaling/src/media-conduit/VideoConduit.h
@@ -205,17 +205,24 @@ public:
   virtual ~WebrtcVideoConduit() ;
 
   MediaConduitErrorCode Init(WebrtcVideoConduit *other);
 
   int GetChannel() { return mChannel; }
   webrtc::VideoEngine* GetVideoEngine() { return mVideoEngine; }
   bool GetLocalSSRC(unsigned int* ssrc);
   bool GetRemoteSSRC(unsigned int* ssrc);
-  bool GetReceivedJitter(unsigned int* jitterMs);
+  bool GetRTPJitter(unsigned int* jitterMs);
+  bool GetRTCPReceiverReport(DOMHighResTimeStamp* timestamp,
+                             unsigned int* jitterMs,
+                             unsigned int* packetsReceived,
+                             uint64_t* bytesReceived);
+  bool GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
+                           unsigned int* packetsSent,
+                           uint64_t* bytesSent);
 
 private:
 
   WebrtcVideoConduit(const WebrtcVideoConduit& other) MOZ_DELETE;
   void operator=(const WebrtcVideoConduit& other) MOZ_DELETE;
 
   //Local database of currently applied receive codecs
   typedef std::vector<VideoCodecConfig* > RecvCodecList;
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -1320,17 +1320,17 @@ PeerConnectionImpl::GetStats(MediaStream
     // TODO(bcampen@mozilla.com): I may need to revisit this for bundle.
     // (Bug 786234)
     RefPtr<NrIceMediaStream> temp(mMedia->ice_media_stream(level-1));
     if (temp.get()) {
       streams.push_back(temp);
     } else {
        CSFLogError(logTag, "Failed to get NrIceMediaStream for level %u "
                            "in %s:  %s",
-                           level, __FUNCTION__, mHandle.c_str());
+                           uint32_t(level), __FUNCTION__, mHandle.c_str());
        MOZ_CRASH();
     }
   }
 
   DOMHighResTimeStamp now;
   nsresult rv = GetTimeSinceEpoch(&now);
   NS_ENSURE_SUCCESS(rv, rv);
 
@@ -1973,46 +1973,113 @@ PeerConnectionImpl::GetStatsImpl_s(
   for (auto it = pipelines.begin(); it != pipelines.end(); ++it) {
     const MediaPipeline& mp = **it;
     nsString idstr = (mp.Conduit()->type() == MediaSessionConduit::AUDIO) ?
         NS_LITERAL_STRING("audio_") : NS_LITERAL_STRING("video_");
     idstr.AppendInt(mp.trackid());
 
     switch (mp.direction()) {
       case MediaPipeline::TRANSMIT: {
-        RTCOutboundRTPStreamStats s;
-        s.mTimestamp.Construct(now);
-        s.mId.Construct(NS_LITERAL_STRING("outbound_rtp_") + idstr);
-        s.mType.Construct(RTCStatsType::Outboundrtp);
-        unsigned int ssrc;
-        if (mp.Conduit()->GetLocalSSRC(&ssrc)) {
-          nsString str;
-          str.AppendInt(ssrc);
-          s.mSsrc.Construct(str);
+        nsString localId = NS_LITERAL_STRING("outbound_rtp_") + idstr;
+        nsString remoteId;
+        nsString ssrc;
+        unsigned int ssrcval;
+        if (mp.Conduit()->GetLocalSSRC(&ssrcval)) {
+          ssrc.AppendInt(ssrcval);
         }
-        s.mPacketsSent.Construct(mp.rtp_packets_sent());
-        s.mBytesSent.Construct(mp.rtp_bytes_sent());
-        report->mOutboundRTPStreamStats.Value().AppendElement(s);
+        {
+          // First, fill in remote stat with rtcp receiver data, if present.
+          // ReceiverReports have less information than SenderReports,
+          // so fill in what we can.
+          DOMHighResTimeStamp timestamp;
+          uint32_t jitterMs;
+          uint32_t packetsReceived;
+          uint64_t bytesReceived;
+          if (mp.Conduit()->GetRTCPReceiverReport(&timestamp, &jitterMs,
+                                                  &packetsReceived,
+                                                  &bytesReceived)) {
+            remoteId = NS_LITERAL_STRING("outbound_rtcp_") + idstr;
+            RTCInboundRTPStreamStats s;
+            s.mTimestamp.Construct(timestamp);
+            s.mId.Construct(remoteId);
+            s.mType.Construct(RTCStatsType::Inboundrtp);
+            if (ssrc.Length()) {
+              s.mSsrc.Construct(ssrc);
+            }
+            s.mJitter.Construct(double(jitterMs)/1000);
+            s.mRemoteId.Construct(localId);
+            s.mIsRemote = true;
+            s.mPacketsReceived.Construct(packetsReceived);
+            s.mBytesReceived.Construct(bytesReceived);
+            report->mInboundRTPStreamStats.Value().AppendElement(s);
+          }
+        }
+        // Then, fill in local side (with cross-link to remote only if present)
+        {
+          RTCOutboundRTPStreamStats s;
+          s.mTimestamp.Construct(now);
+          s.mId.Construct(localId);
+          s.mType.Construct(RTCStatsType::Outboundrtp);
+          if (ssrc.Length()) {
+            s.mSsrc.Construct(ssrc);
+          }
+          s.mRemoteId.Construct(remoteId);
+          s.mIsRemote = false;
+          s.mPacketsSent.Construct(mp.rtp_packets_sent());
+          s.mBytesSent.Construct(mp.rtp_bytes_sent());
+          report->mOutboundRTPStreamStats.Value().AppendElement(s);
+        }
         break;
       }
       case MediaPipeline::RECEIVE: {
+        nsString localId = NS_LITERAL_STRING("inbound_rtp_") + idstr;
+        nsString remoteId;
+        nsString ssrc;
+        unsigned int ssrcval;
+        if (mp.Conduit()->GetRemoteSSRC(&ssrcval)) {
+          ssrc.AppendInt(ssrcval);
+        }
+        {
+          // First, fill in remote stat with rtcp sender data, if present.
+          DOMHighResTimeStamp timestamp;
+          uint32_t packetsSent;
+          uint64_t bytesSent;
+          if (mp.Conduit()->GetRTCPSenderReport(&timestamp,
+                                                &packetsSent, &bytesSent)) {
+            remoteId = NS_LITERAL_STRING("inbound_rtcp_") + idstr;
+            RTCOutboundRTPStreamStats s;
+            s.mTimestamp.Construct(timestamp);
+            s.mId.Construct(remoteId);
+            s.mType.Construct(RTCStatsType::Outboundrtp);
+            if (ssrc.Length()) {
+              s.mSsrc.Construct(ssrc);
+            }
+            s.mRemoteId.Construct(localId);
+            s.mIsRemote = true;
+            s.mPacketsSent.Construct(packetsSent);
+            s.mBytesSent.Construct(bytesSent);
+            report->mOutboundRTPStreamStats.Value().AppendElement(s);
+          }
+        }
+        // Then, fill in local side (with cross-link to remote only if present)
         RTCInboundRTPStreamStats s;
         s.mTimestamp.Construct(now);
-        s.mId.Construct(NS_LITERAL_STRING("inbound_rtp_") + idstr);
+        s.mId.Construct(localId);
         s.mType.Construct(RTCStatsType::Inboundrtp);
-        unsigned int ssrc;
-        if (mp.Conduit()->GetRemoteSSRC(&ssrc)) {
-          nsString str;
-          str.AppendInt(ssrc);
-          s.mSsrc.Construct(str);
+        if (ssrc.Length()) {
+          s.mSsrc.Construct(ssrc);
         }
         unsigned int jitterMs;
-        if (mp.Conduit()->GetReceivedJitter(&jitterMs)) {
+        if (mp.Conduit()->GetRTPJitter(&jitterMs)) {
           s.mJitter.Construct(double(jitterMs)/1000);
         }
+        if (remoteId.Length()) {
+          s.mRemoteId.Construct(remoteId);
+        }
+        s.mIsRemote = false;
         s.mPacketsReceived.Construct(mp.rtp_packets_received());
         s.mBytesReceived.Construct(mp.rtp_bytes_received());
         report->mInboundRTPStreamStats.Value().AppendElement(s);
         break;
       }
     }
   }
 
--- a/media/webrtc/trunk/webrtc/video_engine/include/vie_rtp_rtcp.h
+++ b/media/webrtc/trunk/webrtc/video_engine/include/vie_rtp_rtcp.h
@@ -259,25 +259,33 @@ class WEBRTC_DLLEXPORT ViERTP_RTCP {
   // back-to-back.
   virtual int SetTransmissionSmoothingStatus(int video_channel,
                                              bool enable) = 0;
 
   // This function returns our locally created statistics of the received RTP
   // stream.
   virtual int GetReceivedRTCPStatistics(
       const int video_channel,
+      unsigned int& ntpHigh,
+      unsigned int& ntpLow,
+      unsigned int& bytes_sent,
+      unsigned int& packets_sent,
       unsigned short& fraction_lost,
       unsigned int& cumulative_lost,
       unsigned int& extended_max,
       unsigned int& jitter,
       int& rtt_ms) const = 0;
 
   // This function returns statistics reported by the remote client in a RTCP
   // packet.
   virtual int GetSentRTCPStatistics(const int video_channel,
+                                    unsigned int& ntpHigh,
+                                    unsigned int& ntpLow,
+                                    unsigned int& bytes_sent,
+                                    unsigned int& packets_sent,
                                     unsigned short& fraction_lost,
                                     unsigned int& cumulative_lost,
                                     unsigned int& extended_max,
                                     unsigned int& jitter,
                                     int& rtt_ms) const = 0;
 
   // The function gets statistics from the sent and received RTP streams.
   virtual int GetRTPStatistics(const int video_channel,
--- a/media/webrtc/trunk/webrtc/video_engine/vie_channel.cc
+++ b/media/webrtc/trunk/webrtc/video_engine/vie_channel.cc
@@ -1172,17 +1172,21 @@ int32_t ViEChannel::SendApplicationDefin
                                                data_length_in_bytes) != 0) {
     WEBRTC_TRACE(kTraceError, kTraceVideo, ViEId(engine_id_, channel_id_),
                  "%s: Could not send RTCP application data", __FUNCTION__);
     return -1;
   }
   return 0;
 }
 
-int32_t ViEChannel::GetSendRtcpStatistics(uint16_t* fraction_lost,
+int32_t ViEChannel::GetSendRtcpStatistics(uint32_t* ntp_high,
+                                          uint32_t* ntp_low,
+                                          uint32_t* bytes_sent,
+                                          uint32_t* packets_sent,
+                                          uint16_t* fraction_lost,
                                           uint32_t* cumulative_lost,
                                           uint32_t* extended_max,
                                           uint32_t* jitter_samples,
                                           int32_t* rtt_ms) {
   WEBRTC_TRACE(kTraceInfo, kTraceVideo, ViEId(engine_id_, channel_id_), "%s",
                __FUNCTION__);
 
   // TODO(pwestin) how do we do this for simulcast ? average for all
@@ -1191,16 +1195,30 @@ int32_t ViEChannel::GetSendRtcpStatistic
 
   // for (std::list<RtpRtcp*>::const_iterator it = simulcast_rtp_rtcp_.begin();
   //      it != simulcast_rtp_rtcp_.end();
   //      it++) {
   //   RtpRtcp* rtp_rtcp = *it;
   // }
   uint32_t remote_ssrc = vie_receiver_.GetRemoteSsrc();
 
+  // --- Information from sender info in received RTCP Sender Reports
+
+  RTCPSenderInfo sender_info;
+  if (rtp_rtcp_->RemoteRTCPStat(&sender_info) != 0) {
+    WEBRTC_TRACE(kTraceWarning, kTraceVideo, ViEId(engine_id_, channel_id_),
+                 "%s: Could not get sender info for remote side", __FUNCTION__);
+    return -1;
+  }
+
+  *ntp_high = sender_info.NTPseconds;
+  *ntp_low = sender_info.NTPfraction;
+  *bytes_sent = sender_info.sendOctetCount;
+  *packets_sent = sender_info.sendPacketCount;
+
   // Get all RTCP receiver report blocks that have been received on this
   // channel. If we receive RTP packets from a remote source we know the
   // remote SSRC and use the report block from him.
   // Otherwise use the first report block.
   std::vector<RTCPReportBlock> remote_stats;
   if (rtp_rtcp_->RemoteRTCPStat(&remote_stats) != 0 || remote_stats.empty()) {
     WEBRTC_TRACE(kTraceWarning, kTraceVideo, ViEId(engine_id_, channel_id_),
                  "%s: Could not get remote stats", __FUNCTION__);
@@ -1235,24 +1253,44 @@ int32_t ViEChannel::GetSendRtcpStatistic
   }
   *rtt_ms = rtt;
   return 0;
 }
 
 // TODO(holmer): This is a bad function name as it implies that it returns the
 // received RTCP, while it actually returns the statistics which will be sent
 // in the RTCP.
-int32_t ViEChannel::GetReceivedRtcpStatistics(uint16_t* fraction_lost,
+int32_t ViEChannel::GetReceivedRtcpStatistics(uint32_t* ntp_high,
+                                              uint32_t* ntp_low,
+                                              uint32_t* bytes_sent,
+                                              uint32_t* packets_sent,
+                                              uint16_t* fraction_lost,
                                               uint32_t* cumulative_lost,
                                               uint32_t* extended_max,
                                               uint32_t* jitter_samples,
                                               int32_t* rtt_ms) {
   WEBRTC_TRACE(kTraceInfo, kTraceVideo, ViEId(engine_id_, channel_id_),
                "%s", __FUNCTION__);
 
+  // --- Information from sender info in received RTCP Sender Reports
+
+  RTCPSenderInfo sender_info;
+  if (rtp_rtcp_->RemoteRTCPStat(&sender_info) != 0) {
+    WEBRTC_TRACE(kTraceWarning, kTraceVideo, ViEId(engine_id_, channel_id_),
+                 "%s: Could not get sender info for remote side", __FUNCTION__);
+    return -1;
+  }
+
+  *ntp_high = sender_info.NTPseconds;
+  *ntp_low = sender_info.NTPfraction;
+  *bytes_sent = sender_info.sendOctetCount;
+  *packets_sent = sender_info.sendPacketCount;
+
+  // --- Locally derived information
+
   uint32_t remote_ssrc = vie_receiver_.GetRemoteSsrc();
   uint8_t frac_lost = 0;
   StreamStatistician* statistician =
       vie_receiver_.GetReceiveStatistics()->GetStatistician(remote_ssrc);
   StreamStatistician::Statistics receive_stats;
   if (!statistician || !statistician->GetStatistics(
       &receive_stats, rtp_rtcp_->RTCP() == kRtcpOff)) {
     WEBRTC_TRACE(kTraceError, kTraceVideo, ViEId(engine_id_, channel_id_),
--- a/media/webrtc/trunk/webrtc/video_engine/vie_channel.h
+++ b/media/webrtc/trunk/webrtc/video_engine/vie_channel.h
@@ -159,24 +159,32 @@ class ViEChannel
   int32_t RegisterRtcpObserver(ViERTCPObserver* observer);
   int32_t SendApplicationDefinedRTCPPacket(
       const uint8_t sub_type,
       uint32_t name,
       const uint8_t* data,
       uint16_t data_length_in_bytes);
 
   // Returns statistics reported by the remote client in an RTCP packet.
-  int32_t GetSendRtcpStatistics(uint16_t* fraction_lost,
+  int32_t GetSendRtcpStatistics(uint32_t* ntp_high,
+                                uint32_t* ntp_low,
+                                uint32_t* bytes_sent,
+                                uint32_t* packets_sent,
+                                uint16_t* fraction_lost,
                                 uint32_t* cumulative_lost,
                                 uint32_t* extended_max,
                                 uint32_t* jitter_samples,
                                 int32_t* rtt_ms);
 
-  // Returns our localy created statistics of the received RTP stream.
-  int32_t GetReceivedRtcpStatistics(uint16_t* fraction_lost,
+  // Returns RTCP sender report + locally created stats of received RTP stream
+  int32_t GetReceivedRtcpStatistics(uint32_t* ntp_high,
+                                    uint32_t* ntp_low,
+                                    uint32_t* bytes_sent,
+                                    uint32_t* packets_sent,
+                                    uint16_t* fraction_lost,
                                     uint32_t* cumulative_lost,
                                     uint32_t* extended_max,
                                     uint32_t* jitter_samples,
                                     int32_t* rtt_ms);
 
   // Gets sent/received packets statistics.
   int32_t GetRtpStatistics(uint32_t* bytes_sent,
                            uint32_t* packets_sent,
--- a/media/webrtc/trunk/webrtc/video_engine/vie_rtp_rtcp_impl.cc
+++ b/media/webrtc/trunk/webrtc/video_engine/vie_rtp_rtcp_impl.cc
@@ -819,16 +819,20 @@ int ViERTP_RTCPImpl::SetTransmissionSmoo
     shared_data_->SetLastError(kViERtpRtcpInvalidChannelId);
     return -1;
   }
   vie_channel->SetTransmissionSmoothingStatus(enable);
   return 0;
 }
 
 int ViERTP_RTCPImpl::GetReceivedRTCPStatistics(const int video_channel,
+                                               unsigned int& ntp_high,
+                                               unsigned int& ntp_low,
+                                               unsigned int& bytes_sent,
+                                               unsigned int& packets_sent,
                                                uint16_t& fraction_lost,
                                                unsigned int& cumulative_lost,
                                                unsigned int& extended_max,
                                                unsigned int& jitter,
                                                int& rtt_ms) const {
   WEBRTC_TRACE(kTraceApiCall, kTraceVideo,
                ViEId(shared_data_->instance_id(), video_channel),
                "%s(channel: %d)", __FUNCTION__, video_channel);
@@ -836,28 +840,36 @@ int ViERTP_RTCPImpl::GetReceivedRTCPStat
   ViEChannel* vie_channel = cs.Channel(video_channel);
   if (!vie_channel) {
     WEBRTC_TRACE(kTraceError, kTraceVideo,
                  ViEId(shared_data_->instance_id(), video_channel),
                  "%s: Channel %d doesn't exist", __FUNCTION__, video_channel);
     shared_data_->SetLastError(kViERtpRtcpInvalidChannelId);
     return -1;
   }
-  if (vie_channel->GetReceivedRtcpStatistics(&fraction_lost,
+  if (vie_channel->GetReceivedRtcpStatistics(&ntp_high,
+                                             &ntp_low,
+                                             &bytes_sent,
+                                             &packets_sent,
+                                             &fraction_lost,
                                              &cumulative_lost,
                                              &extended_max,
                                              &jitter,
                                              &rtt_ms) != 0) {
     shared_data_->SetLastError(kViERtpRtcpUnknownError);
     return -1;
   }
   return 0;
 }
 
 int ViERTP_RTCPImpl::GetSentRTCPStatistics(const int video_channel,
+                                           unsigned int& ntp_high,
+                                           unsigned int& ntp_low,
+                                           unsigned int& bytes_sent,
+                                           unsigned int& packets_sent,
                                            uint16_t& fraction_lost,
                                            unsigned int& cumulative_lost,
                                            unsigned int& extended_max,
                                            unsigned int& jitter,
                                            int& rtt_ms) const {
   WEBRTC_TRACE(kTraceApiCall, kTraceVideo,
                ViEId(shared_data_->instance_id(), video_channel),
                "%s(channel: %d)", __FUNCTION__, video_channel);
@@ -866,17 +878,19 @@ int ViERTP_RTCPImpl::GetSentRTCPStatisti
   if (!vie_channel) {
     WEBRTC_TRACE(kTraceError, kTraceVideo,
                  ViEId(shared_data_->instance_id(), video_channel),
                  "%s: Channel %d doesn't exist", __FUNCTION__, video_channel);
     shared_data_->SetLastError(kViERtpRtcpInvalidChannelId);
     return -1;
   }
 
-  if (vie_channel->GetSendRtcpStatistics(&fraction_lost, &cumulative_lost,
+  if (vie_channel->GetSendRtcpStatistics(&ntp_high, &ntp_low,
+                                         &bytes_sent, &packets_sent,
+                                         &fraction_lost, &cumulative_lost,
                                          &extended_max, &jitter,
                                          &rtt_ms) != 0) {
     shared_data_->SetLastError(kViERtpRtcpUnknownError);
     return -1;
   }
   return 0;
 }
 
--- a/media/webrtc/trunk/webrtc/video_engine/vie_rtp_rtcp_impl.h
+++ b/media/webrtc/trunk/webrtc/video_engine/vie_rtp_rtcp_impl.h
@@ -85,22 +85,30 @@ class ViERTP_RTCPImpl
   virtual int SetSendAbsoluteSendTimeStatus(int video_channel,
                                             bool enable,
                                             int id);
   virtual int SetReceiveAbsoluteSendTimeStatus(int video_channel,
                                                bool enable,
                                                int id);
   virtual int SetTransmissionSmoothingStatus(int video_channel, bool enable);
   virtual int GetReceivedRTCPStatistics(const int video_channel,
+                                        unsigned int& ntpHigh,
+                                        unsigned int& ntpLow,
+                                        unsigned int& bytes_sent,
+                                        unsigned int& packets_sent,
                                         uint16_t& fraction_lost,
                                         unsigned int& cumulative_lost,
                                         unsigned int& extended_max,
                                         unsigned int& jitter,
                                         int& rtt_ms) const;
   virtual int GetSentRTCPStatistics(const int video_channel,
+                                    unsigned int& ntpHigh,
+                                    unsigned int& ntpLow,
+                                    unsigned int& bytes_sent,
+                                    unsigned int& packets_sent,
                                     uint16_t& fraction_lost,
                                     unsigned int& cumulative_lost,
                                     unsigned int& extended_max,
                                     unsigned int& jitter, int& rtt_ms) const;
   virtual int GetRTPStatistics(const int video_channel,
                                unsigned int& bytes_sent,
                                unsigned int& packets_sent,
                                unsigned int& bytes_received,
--- a/media/webrtc/trunk/webrtc/voice_engine/channel.cc
+++ b/media/webrtc/trunk/webrtc/voice_engine/channel.cc
@@ -3794,36 +3794,39 @@ Channel::GetRemoteRTCP_CNAME(char cName[
 }
 
 int
 Channel::GetRemoteRTCPData(
     unsigned int& NTPHigh,
     unsigned int& NTPLow,
     unsigned int& timestamp,
     unsigned int& playoutTimestamp,
+    unsigned int& sendPacketCount,
+    unsigned int& sendOctetCount,
     unsigned int* jitter,
-    unsigned short* fractionLost)
+    unsigned short* fractionLost,
+    unsigned int* cumulativeLost)
 {
     // --- Information from sender info in received Sender Reports
 
     RTCPSenderInfo senderInfo;
     if (_rtpRtcpModule->RemoteRTCPStat(&senderInfo) != 0)
     {
         _engineStatisticsPtr->SetLastError(
             VE_RTP_RTCP_MODULE_ERROR, kTraceError,
             "GetRemoteRTCPData() failed to retrieve sender info for remote "
             "side");
         return -1;
     }
 
-    // We only utilize 12 out of 20 bytes in the sender info (ignores packet
-    // and octet count)
     NTPHigh = senderInfo.NTPseconds;
     NTPLow = senderInfo.NTPfraction;
     timestamp = senderInfo.RTPtimeStamp;
+    sendPacketCount = senderInfo.sendPacketCount;
+    sendOctetCount = senderInfo.sendOctetCount;
 
     WEBRTC_TRACE(kTraceStateInfo, kTraceVoice,
                  VoEId(_instanceId, _channelId),
                  "GetRemoteRTCPData() => NTPHigh=%lu, NTPLow=%lu, "
                  "timestamp=%lu",
                  NTPHigh, NTPLow, timestamp);
 
     // --- Locally derived information
@@ -3832,17 +3835,17 @@ Channel::GetRemoteRTCPData(
     // has been received)
     playoutTimestamp = playout_timestamp_rtcp_;
 
     WEBRTC_TRACE(kTraceStateInfo, kTraceVoice,
                  VoEId(_instanceId, _channelId),
                  "GetRemoteRTCPData() => playoutTimestamp=%lu",
                  playout_timestamp_rtcp_);
 
-    if (NULL != jitter || NULL != fractionLost)
+    if (NULL != jitter || NULL != fractionLost || NULL != cumulativeLost)
     {
         // Get all RTCP receiver report blocks that have been received on this
         // channel. If we receive RTP packets from a remote source we know the
         // remote SSRC and use the report block from him.
         // Otherwise use the first report block.
         std::vector<RTCPReportBlock> remote_stats;
         if (_rtpRtcpModule->RemoteRTCPStat(&remote_stats) != 0 ||
             remote_stats.empty()) {
@@ -3877,16 +3880,24 @@ Channel::GetRemoteRTCPData(
 
         if (fractionLost) {
           *fractionLost = it->fractionLost;
           WEBRTC_TRACE(kTraceStateInfo, kTraceVoice,
                        VoEId(_instanceId, _channelId),
                        "GetRemoteRTCPData() => fractionLost = %lu",
                        *fractionLost);
         }
+
+        if (cumulativeLost) {
+          *cumulativeLost = it->cumulativeLost;
+          WEBRTC_TRACE(kTraceStateInfo, kTraceVoice,
+                       VoEId(_instanceId, _channelId),
+                       "GetRemoteRTCPData() => cumulativeLost = %lu",
+                       *cumulativeLost);
+        }
     }
     return 0;
 }
 
 int
 Channel::SendApplicationDefinedRTCPPacket(unsigned char subType,
                                              unsigned int name,
                                              const char* data,
--- a/media/webrtc/trunk/webrtc/voice_engine/channel.h
+++ b/media/webrtc/trunk/webrtc/voice_engine/channel.h
@@ -259,18 +259,22 @@ public:
     int GetRTPAudioLevelIndicationStatus(bool& enable, unsigned char& ID);
     int SetRTCPStatus(bool enable);
     int GetRTCPStatus(bool& enabled);
     int SetRTCP_CNAME(const char cName[256]);
     int GetRTCP_CNAME(char cName[256]);
     int GetRemoteRTCP_CNAME(char cName[256]);
     int GetRemoteRTCPData(unsigned int& NTPHigh, unsigned int& NTPLow,
                           unsigned int& timestamp,
-                          unsigned int& playoutTimestamp, unsigned int* jitter,
-                          unsigned short* fractionLost);
+                          unsigned int& playoutTimestamp,
+                          unsigned int& sendPacketCount,
+                          unsigned int& sendOctetCount,
+                          unsigned int* jitter,
+                          unsigned short* fractionLost,
+                          unsigned int* cumulativeLost);
     int SendApplicationDefinedRTCPPacket(unsigned char subType,
                                          unsigned int name, const char* data,
                                          unsigned short dataLengthInBytes);
     int GetRTPStatistics(unsigned int& averageJitterMs,
                          unsigned int& maxJitterMs,
                          unsigned int& discardedPackets);
     int GetRemoteRTCPSenderInfo(SenderInfo* sender_info);
     int GetRemoteRTCPReportBlocks(std::vector<ReportBlock>* report_blocks);
--- a/media/webrtc/trunk/webrtc/voice_engine/include/voe_rtp_rtcp.h
+++ b/media/webrtc/trunk/webrtc/voice_engine/include/voe_rtp_rtcp.h
@@ -180,17 +180,19 @@ public:
     // Gets the canonical name (CNAME) parameter for incoming RTCP reports
     // on a specific channel.
     virtual int GetRemoteRTCP_CNAME(int channel, char cName[256]) = 0;
 
     // Gets RTCP data from incoming RTCP Sender Reports.
     virtual int GetRemoteRTCPData(
         int channel, unsigned int& NTPHigh, unsigned int& NTPLow,
         unsigned int& timestamp, unsigned int& playoutTimestamp,
-        unsigned int* jitter = NULL, unsigned short* fractionLost = NULL) = 0;
+        unsigned int& sendPacketCount, unsigned int& sendOctetCount,
+        unsigned int* jitter = NULL, unsigned short* fractionLost = NULL,
+        unsigned int* cumulativeLost = NULL) = 0;
 
     // Gets RTP statistics for a specific |channel|.
     virtual int GetRTPStatistics(
         int channel, unsigned int& averageJitterMs, unsigned int& maxJitterMs,
         unsigned int& discardedPackets) = 0;
 
     // Gets RTCP statistics for a specific |channel|.
     virtual int GetRTCPStatistics(int channel, CallStatistics& stats) = 0;
--- a/media/webrtc/trunk/webrtc/voice_engine/voe_rtp_rtcp_impl.cc
+++ b/media/webrtc/trunk/webrtc/voice_engine/voe_rtp_rtcp_impl.cc
@@ -370,18 +370,21 @@ int VoERTP_RTCPImpl::GetRemoteRTCP_CNAME
 }
 
 int VoERTP_RTCPImpl::GetRemoteRTCPData(
     int channel,
     unsigned int& NTPHigh, // from sender info in SR
     unsigned int& NTPLow, // from sender info in SR
     unsigned int& timestamp, // from sender info in SR
     unsigned int& playoutTimestamp, // derived locally
+    unsigned int& sendPacketCount, // from sender info in SR
+    unsigned int& sendOctetCount, // from sender info in SR
     unsigned int* jitter, // from report block 1 in SR/RR
-    unsigned short* fractionLost) // from report block 1 in SR/RR
+    unsigned short* fractionLost, // from report block 1 in SR/RR
+    unsigned int* cumulativeLost) // from report block 1 in SR/RR
 {
     WEBRTC_TRACE(kTraceApiCall, kTraceVoice, VoEId(_shared->instance_id(), -1),
                  "GetRemoteRTCPData(channel=%d,...)", channel);
     if (!_shared->statistics().Initialized())
     {
         _shared->SetLastError(VE_NOT_INITED, kTraceError);
         return -1;
     }
@@ -392,18 +395,21 @@ int VoERTP_RTCPImpl::GetRemoteRTCPData(
         _shared->SetLastError(VE_CHANNEL_NOT_VALID, kTraceError,
             "GetRemoteRTCP_CNAME() failed to locate channel");
         return -1;
     }
     return channelPtr->GetRemoteRTCPData(NTPHigh,
                                          NTPLow,
                                          timestamp,
                                          playoutTimestamp,
+                                         sendPacketCount,
+                                         sendOctetCount,
                                          jitter,
-                                         fractionLost);
+                                         fractionLost,
+                                         cumulativeLost);
 }
 
 int VoERTP_RTCPImpl::SendApplicationDefinedRTCPPacket(
     int channel,
     unsigned char subType,
     unsigned int name,
     const char* data,
     unsigned short dataLengthInBytes)
--- a/media/webrtc/trunk/webrtc/voice_engine/voe_rtp_rtcp_impl.h
+++ b/media/webrtc/trunk/webrtc/voice_engine/voe_rtp_rtcp_impl.h
@@ -40,18 +40,21 @@ public:
 
     virtual int GetRemoteRTCP_CNAME(int channel, char cName[256]);
 
     virtual int GetRemoteRTCPData(int channel,
                                   unsigned int& NTPHigh,
                                   unsigned int& NTPLow,
                                   unsigned int& timestamp,
                                   unsigned int& playoutTimestamp,
+                                  unsigned int& sendPacketCount,
+                                  unsigned int& sendOctetCount,
                                   unsigned int* jitter = NULL,
-                                  unsigned short* fractionLost = NULL);
+                                  unsigned short* fractionLost = NULL,
+                                  unsigned int* cumulativeLost = NULL);
 
     virtual int SendApplicationDefinedRTCPPacket(
         int channel,
         unsigned char subType,
         unsigned int name,
         const char* data,
         unsigned short dataLengthInBytes);
 
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -1953,17 +1953,18 @@ public class GeckoAppShell
     private static final String PLUGIN_TYPE = "type";
     private static final String TYPE_NATIVE = "native";
     static public ArrayList<PackageInfo> mPackageInfoCache = new ArrayList<PackageInfo>();
 
     // Returns null if plugins are blocked on the device.
     static String[] getPluginDirectories() {
 
         // An awful hack to detect Tegra devices. Easiest way to do it without spinning up a EGL context.
-        boolean isTegra = (new File("/system/lib/hw/gralloc.tegra.so")).exists();
+        boolean isTegra = (new File("/system/lib/hw/gralloc.tegra.so")).exists() ||
+                          (new File("/system/lib/hw/gralloc.tegra3.so")).exists();
         if (isTegra) {
             // disable Flash on Tegra ICS with CM9 and other custom firmware (bug 736421)
             File vfile = new File("/proc/version");
             FileReader vreader = null;
             try {
                 if (vfile.canRead()) {
                     vreader = new FileReader(vfile);
                     String version = new BufferedReader(vreader).readLine();
@@ -1981,16 +1982,22 @@ public class GeckoAppShell
                 try {
                     if (vreader != null) {
                         vreader.close();
                     }
                 } catch (IOException ex) {
                     // nothing
                 }
             }
+
+            // disable on KitKat (bug 957694)
+            if (Build.VERSION.SDK_INT >= 19) {
+                Log.w(LOGTAG, "Blocking plugins because of Tegra (bug 957694)");
+                return null;
+            }
         }
 
         ArrayList<String> directories = new ArrayList<String>();
         PackageManager pm = getContext().getPackageManager();
         List<ResolveInfo> plugins = pm.queryIntentServices(new Intent(PLUGIN_ACTION),
                 PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
 
         synchronized(mPackageInfoCache) {
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -503,16 +503,17 @@ sync_java_files = [
     'background/fxa/FxAccount10CreateDelegate.java',
     'background/fxa/FxAccount20CreateDelegate.java',
     'background/fxa/FxAccount20LoginDelegate.java',
     'background/fxa/FxAccountAgeLockoutHelper.java',
     'background/fxa/FxAccountClient.java',
     'background/fxa/FxAccountClient10.java',
     'background/fxa/FxAccountClient20.java',
     'background/fxa/FxAccountClientException.java',
+    'background/fxa/FxAccountRemoteError.java',
     'background/fxa/FxAccountUtils.java',
     'background/fxa/SkewHandler.java',
     'background/healthreport/Environment.java',
     'background/healthreport/EnvironmentBuilder.java',
     'background/healthreport/EnvironmentV1.java',
     'background/healthreport/HealthReportBroadcastReceiver.java',
     'background/healthreport/HealthReportBroadcastService.java',
     'background/healthreport/HealthReportDatabases.java',
@@ -550,23 +551,33 @@ sync_java_files = [
     'fxa/activities/FxAccountCreateAccountFragment.java',
     'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
     'fxa/activities/FxAccountGetStartedActivity.java',
     'fxa/activities/FxAccountSetupTask.java',
     'fxa/activities/FxAccountSignInActivity.java',
     'fxa/activities/FxAccountStatusActivity.java',
     'fxa/activities/FxAccountUpdateCredentialsActivity.java',
     'fxa/activities/FxAccountVerifiedAccountActivity.java',
-    'fxa/authenticator/AbstractFxAccount.java',
     'fxa/authenticator/AndroidFxAccount.java',
     'fxa/authenticator/FxAccountAuthenticator.java',
     'fxa/authenticator/FxAccountAuthenticatorService.java',
     'fxa/authenticator/FxAccountLoginDelegate.java',
     'fxa/authenticator/FxAccountLoginException.java',
-    'fxa/authenticator/FxAccountLoginPolicy.java',
+    'fxa/login/BaseRequestDelegate.java',
+    'fxa/login/Cohabiting.java',
+    'fxa/login/Doghouse.java',
+    'fxa/login/Engaged.java',
+    'fxa/login/FxAccountLoginStateMachine.java',
+    'fxa/login/FxAccountLoginTransition.java',
+    'fxa/login/Married.java',
+    'fxa/login/Promised.java',
+    'fxa/login/Separated.java',
+    'fxa/login/State.java',
+    'fxa/login/StateFactory.java',
+    'fxa/login/TokensAndKeysState.java',
     'fxa/sync/FxAccountGlobalSession.java',
     'fxa/sync/FxAccountSyncAdapter.java',
     'fxa/sync/FxAccountSyncService.java',
     'sync/AlreadySyncingException.java',
     'sync/BadRequiredFieldJSONException.java',
     'sync/CollectionKeys.java',
     'sync/CommandProcessor.java',
     'sync/CommandRunner.java',
--- a/mobile/android/base/background/fxa/FxAccountAgeLockoutHelper.java
+++ b/mobile/android/base/background/fxa/FxAccountAgeLockoutHelper.java
@@ -19,19 +19,25 @@ import org.mozilla.gecko.fxa.FxAccountCo
  * For now we maintain "locked out" state as a static variable. In the future we
  * might need to persist this state across process restarts, so we'll force
  * consumers to create an instance of this class. Then, we can drop in a class
  * backed by shared preferences.
  */
 public class FxAccountAgeLockoutHelper {
   private static final String LOG_TAG = FxAccountAgeLockoutHelper.class.getSimpleName();
 
-  protected static long ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK = 0;
+  protected static long ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK = 0L;
 
   public static synchronized boolean isLockedOut(long elapsedRealtime) {
+    if (ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK == 0L) {
+      // We never failed, so we're not locked out.
+      return false;
+    }
+
+    // Otherwise, find out how long it's been since we last failed.
     long millsecondsSinceLastFailedAgeCheck = elapsedRealtime - ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK;
     boolean isLockedOut = millsecondsSinceLastFailedAgeCheck < FxAccountConstants.MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS;
     FxAccountConstants.pii(LOG_TAG, "Checking if locked out: it's been " + millsecondsSinceLastFailedAgeCheck + "ms " +
         "since last lockout, so " + (isLockedOut ? "yes." : "no."));
     return isLockedOut;
   }
 
   public static synchronized void lockOut(long elapsedRealtime) {
--- a/mobile/android/base/background/fxa/FxAccountClient10.java
+++ b/mobile/android/base/background/fxa/FxAccountClient10.java
@@ -12,25 +12,28 @@ import java.security.GeneralSecurityExce
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.concurrent.Executor;
 
 import javax.crypto.Mac;
 
 import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.HKDF;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.Resource;
 import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 
 /**
  * An HTTP client for talking to an FxAccount server.
  * <p>
@@ -52,16 +55,25 @@ public class FxAccountClient10 {
 
   protected static final String VERSION_FRAGMENT = "v1/";
 
   public static final String JSON_KEY_EMAIL = "email";
   public static final String JSON_KEY_KEYFETCHTOKEN = "keyFetchToken";
   public static final String JSON_KEY_SESSIONTOKEN = "sessionToken";
   public static final String JSON_KEY_UID = "uid";
   public static final String JSON_KEY_VERIFIED = "verified";
+  public static final String JSON_KEY_ERROR = "error";
+  public static final String JSON_KEY_MESSAGE = "message";
+  public static final String JSON_KEY_INFO = "info";
+  public static final String JSON_KEY_CODE = "code";
+  public static final String JSON_KEY_ERRNO = "errno";
+
+
+  protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
+  protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
 
   protected final String serverURI;
   protected final Executor executor;
 
   public FxAccountClient10(String serverURI, Executor executor) {
     if (serverURI == null) {
       throw new IllegalArgumentException("Must provide a server URI.");
     }
@@ -73,17 +85,17 @@ public class FxAccountClient10 {
   }
 
   /**
    * Process a typed value extracted from a successful response (in an
    * endpoint-dependent way).
    */
   public interface RequestDelegate<T> {
     public void handleError(Exception e);
-    public void handleFailure(int status, HttpResponse response);
+    public void handleFailure(FxAccountClientRemoteException e);
     public void handleSuccess(T result);
   }
 
   /**
    * A <code>CreateDelegate</code> produces the body of a /create request.
    */
   public interface CreateDelegate {
     public JSONObject getCreateBody() throws FxAccountClientException;
@@ -176,37 +188,34 @@ public class FxAccountClient10 {
       if (tokenId != null && reqHMACKey != null) {
         return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload, skewHandler.getSkewInSeconds());
       }
       return super.getAuthHeaderProvider();
     }
 
     @Override
     public void handleHttpResponse(HttpResponse response) {
-      final int status = response.getStatusLine().getStatusCode();
-      switch (status) {
-      case 200:
+      try {
+        final int status = validateResponse(response);
         skewHandler.updateSkew(response, now());
         invokeHandleSuccess(status, response);
-        return;
-      default:
+      } catch (FxAccountClientRemoteException e) {
         if (!skewHandler.updateSkew(response, now())) {
           // If we couldn't update skew, but we got a failure, let's try clearing the skew.
           skewHandler.resetSkew();
         }
-        invokeHandleFailure(status, response);
-        return;
+        invokeHandleFailure(e);
       }
     }
 
-    protected void invokeHandleFailure(final int status, final HttpResponse response) {
+    protected void invokeHandleFailure(final FxAccountClientRemoteException e) {
       executor.execute(new Runnable() {
         @Override
         public void run() {
-          delegate.handleFailure(status, response);
+          delegate.handleFailure(e);
         }
       });
     }
 
     protected void invokeHandleSuccess(final int status, final HttpResponse response) {
       executor.execute(new Runnable() {
         @Override
         public void run() {
@@ -249,16 +258,50 @@ public class FxAccountClient10 {
     }
   }
 
   @SuppressWarnings("static-method")
   public long now() {
     return System.currentTimeMillis();
   }
 
+  /**
+   * Intepret a response from the auth server.
+   * <p>
+   * Throw an appropriate exception on errors; otherwise, return the response's
+   * status code.
+   *
+   * @return response's HTTP status code.
+   * @throws FxAccountClientException
+   */
+  public static int validateResponse(HttpResponse response) throws FxAccountClientRemoteException {
+    final int status = response.getStatusLine().getStatusCode();
+    if (status == 200) {
+      return status;
+    }
+    int code;
+    int errno;
+    String error;
+    String message;
+    String info;
+    try {
+      ExtendedJSONObject body = new SyncStorageResponse(response).jsonObjectBody();
+      body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
+      body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+      code = body.getLong(JSON_KEY_CODE).intValue();
+      errno = body.getLong(JSON_KEY_ERRNO).intValue();
+      error = body.getString(JSON_KEY_ERROR);
+      message = body.getString(JSON_KEY_MESSAGE);
+      info = body.getString(JSON_KEY_INFO);
+    } catch (Exception e) {
+      throw new FxAccountClientMalformedResponseException(response);
+    }
+    throw new FxAccountClientRemoteException(response, code, errno, error, message, info);
+  }
+
   public void createAccount(final String email, final byte[] stretchedPWBytes,
       final String srpSalt, final String mainSalt,
       final RequestDelegate<String> delegate) {
     try {
       createAccount(new FxAccount10CreateDelegate(email, stretchedPWBytes, srpSalt, mainSalt), delegate);
     } catch (final Exception e) {
       invokeHandleError(delegate, e);
       return;
@@ -374,21 +417,21 @@ public class FxAccountClient10 {
 
       @Override
       public void handleError(final Exception e) {
         invokeHandleError(delegate, e);
         return;
       }
 
       @Override
-      public void handleFailure(final int status, final HttpResponse response) {
+      public void handleFailure(final FxAccountClientRemoteException e) {
         executor.execute(new Runnable() {
           @Override
           public void run() {
-            delegate.handleFailure(status, response);
+            delegate.handleFailure(e);
           }
         });
       }
     });
   }
 
   public void sessionCreate(byte[] authToken, final RequestDelegate<TwoTokens> delegate) {
     final byte[] tokenId = new byte[32];
@@ -533,18 +576,18 @@ public class FxAccountClient10 {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey, false) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
-          byte[] kA = new byte[32];
-          byte[] wrapkB = new byte[32];
+          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;
         } catch (Exception e) {
           delegate.handleError(e);
           return;
         }
       }
--- a/mobile/android/base/background/fxa/FxAccountClientException.java
+++ b/mobile/android/base/background/fxa/FxAccountClientException.java
@@ -1,17 +1,108 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.background.fxa;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+
+/**
+ * From <a href="https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md">https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md</a>.
+ */
 public class FxAccountClientException extends Exception {
   private static final long serialVersionUID = 7953459541558266597L;
 
   public FxAccountClientException(String detailMessage) {
     super(detailMessage);
   }
 
   public FxAccountClientException(Exception e) {
     super(e);
   }
-}
\ No newline at end of file
+
+  public static class FxAccountClientRemoteException extends FxAccountClientException {
+    private static final long serialVersionUID = 2209313149952001097L;
+
+    public final HttpResponse response;
+    public final long httpStatusCode;
+    public final long apiErrorNumber;
+    public final String error;
+    public final String message;
+    public final String info;
+
+    public FxAccountClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, String info) {
+      super(new HTTPFailureException(new SyncStorageResponse(response)));
+      this.response = response;
+      this.httpStatusCode = httpStatusCode;
+      this.apiErrorNumber = apiErrorNumber;
+      this.error = error;
+      this.message = message;
+      this.info = info;
+    }
+
+    @Override
+    public String toString() {
+      return "<FxAccountClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
+    }
+
+    public boolean isInvalidAuthentication() {
+      return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
+    }
+
+    public boolean isAccountAlreadyExists() {
+      return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
+    }
+
+    public boolean isBadPassword() {
+      return apiErrorNumber == FxAccountRemoteError.INCORRECT_PASSWORD;
+    }
+
+    public boolean isUnverified() {
+      return apiErrorNumber == FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
+    }
+
+    public boolean isUpgradeRequired() {
+      return
+          apiErrorNumber == FxAccountRemoteError.ENDPOINT_IS_NO_LONGER_SUPPORTED ||
+          apiErrorNumber == FxAccountRemoteError.INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT ||
+          apiErrorNumber == FxAccountRemoteError.INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT ||
+          apiErrorNumber == FxAccountRemoteError.INCORRECT_API_VERSION_FOR_THIS_ACCOUNT;
+    }
+
+    public int getErrorMessageStringResource() {
+      if (isUpgradeRequired()) {
+        return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
+      }
+      switch ((int) apiErrorNumber) {
+      case FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS:
+        return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
+      case FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST:
+        return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
+      case FxAccountRemoteError.INCORRECT_PASSWORD:
+        return R.string.fxaccount_remote_error_INCORRECT_PASSWORD;
+      case FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT:
+        return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
+      case FxAccountRemoteError.CLIENT_HAS_SENT_TOO_MANY_REQUESTS:
+        return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
+      case FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD:
+        return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
+      default:
+        return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
+      }
+    }
+  }
+
+
+  public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException {
+    private static final long serialVersionUID = 2209313149952001098L;
+
+    public FxAccountClientMalformedResponseException(HttpResponse response) {
+      super(response, 0, FxAccountRemoteError.UNKNOWN_ERROR, "Response malformed", "Response malformed", "Response malformed");
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/FxAccountRemoteError.java
@@ -0,0 +1,30 @@
+/* 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.background.fxa;
+
+public interface FxAccountRemoteError {
+  public static final int ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS = 101;
+  public static final int ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST = 102;
+  public static final int INCORRECT_PASSWORD = 103;
+  public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104;
+  public static final int INVALID_VERIFICATION_CODE = 105;
+  public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106;
+  public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107;
+  public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108;
+  public static final int INVALID_REQUEST_SIGNATURE = 109;
+  public static final int INVALID_AUTHENTICATION_TOKEN = 110;
+  public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
+  public static final int INVALID_AUTHENTICATION_NONCE = 115;
+  public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
+  public static final int REQUEST_BODY_TOO_LARGE = 113;
+  public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
+  public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
+  public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
+  public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
+  public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
+  public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
+  public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
+  public static final int UNKNOWN_ERROR = 999;
+}
--- a/mobile/android/base/background/fxa/FxAccountUtils.java
+++ b/mobile/android/base/background/fxa/FxAccountUtils.java
@@ -17,16 +17,19 @@ import org.mozilla.gecko.sync.crypto.PBK
 
 public class FxAccountUtils {
   public static final int SALT_LENGTH_BYTES = 32;
   public static final int SALT_LENGTH_HEX = 2 * SALT_LENGTH_BYTES;
 
   public static final int HASH_LENGTH_BYTES = 16;
   public static final int HASH_LENGTH_HEX = 2 * HASH_LENGTH_BYTES;
 
+  public static final int CRYPTO_KEY_LENGTH_BYTES = 32;
+  public static final int CRYPTO_KEY_LENGTH_HEX = 2 * CRYPTO_KEY_LENGTH_BYTES;
+
   public static final String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
 
   public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
 
   public static String bytes(String string) throws UnsupportedEncodingException {
     return Utils.byte2Hex(string.getBytes("UTF-8"));
   }
 
@@ -114,9 +117,26 @@ public class FxAccountUtils {
 
   /**
    * The password-derived credential used to unwrap keys managed by the Firefox
    * Account auth server.
    */
   public static byte[] generateUnwrapBKey(byte[] quickStretchedPW) throws GeneralSecurityException, UnsupportedEncodingException {
     return HKDF.derive(quickStretchedPW, new byte[0], FxAccountUtils.KW("unwrapBkey"), 32);
   }
+
+  public static byte[] unwrapkB(byte[] unwrapkB, byte[] wrapkB) {
+    if (unwrapkB == null) {
+      throw new IllegalArgumentException("unwrapkB must not be null");
+    }
+    if (wrapkB == null) {
+      throw new IllegalArgumentException("wrapkB must not be null");
+    }
+    if (unwrapkB.length != CRYPTO_KEY_LENGTH_BYTES || wrapkB.length != CRYPTO_KEY_LENGTH_BYTES) {
+      throw new IllegalArgumentException("unwrapkB and wrapkB must be " + CRYPTO_KEY_LENGTH_BYTES + " bytes long");
+    }
+    byte[] kB = new byte[CRYPTO_KEY_LENGTH_BYTES];
+    for (int i = 0; i < wrapkB.length; i++) {
+      kB[i] = (byte) (wrapkB[i] ^ unwrapkB[i]);
+    }
+    return kB;
+  }
 }
--- a/mobile/android/base/browserid/JSONWebTokenUtils.java
+++ b/mobile/android/base/browserid/JSONWebTokenUtils.java
@@ -125,66 +125,113 @@ public class JSONWebTokenUtils {
     long issuedAt = System.currentTimeMillis();
     long durationInMilliseconds = DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
     return createAssertion(privateKeyToSignWith, certificate, audience, issuer, issuedAt, durationInMilliseconds);
   }
 
   /**
    * For debugging only!
    *
+   * @param input
+   *          certificate to dump.
+   * @return non-null object with keys header, payload, signature if the
+   *         certificate is well-formed.
+   */
+  public static ExtendedJSONObject parseCertificate(String input) {
+    try {
+      String[] parts = input.split("\\.");
+      if (parts.length != 3) {
+        return null;
+      }
+      String cHeader = new String(Base64.decodeBase64(parts[0]));
+      String cPayload = new String(Base64.decodeBase64(parts[1]));
+      String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
+      ExtendedJSONObject o = new ExtendedJSONObject();
+      o.put("header", new ExtendedJSONObject(cHeader));
+      o.put("payload", new ExtendedJSONObject(cPayload));
+      o.put("signature", cSignature);
+      return o;
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  /**
+   * For debugging only!
+   *
    * @param input certificate to dump.
    * @return true if the certificate is well-formed.
    */
   public static boolean dumpCertificate(String input) {
+    ExtendedJSONObject c = parseCertificate(input);
     try {
-      String[] parts = input.split("\\.");
-      if (parts.length != 3) {
-        throw new IllegalArgumentException("certificate must have three parts");
+      if (c == null) {
+        System.out.println("Malformed certificate -- got exception trying to dump contents.");
+        return false;
       }
-      String cHeader = new String(Base64.decodeBase64(parts[0]));
-      String cPayload = new String(Base64.decodeBase64(parts[1]));
-      String cSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
-      System.out.println("certificate header:    " + cHeader);
-      System.out.println("certificate payload:   " + cPayload);
-      System.out.println("certificate signature: " + cSignature);
+      System.out.println("certificate header:    " + c.getString("header"));
+      System.out.println("certificate payload:   " + c.getString("payload"));
+      System.out.println("certificate signature: " + c.getString("signature"));
       return true;
     } catch (Exception e) {
       System.out.println("Malformed certificate -- got exception trying to dump contents.");
-      e.printStackTrace();
       return false;
     }
   }
 
   /**
    * For debugging only!
    *
    * @param input assertion to dump.
    * @return true if the assertion is well-formed.
    */
-  public static boolean dumpAssertion(String input) {
+  public static ExtendedJSONObject parseAssertion(String input) {
     try {
       String[] parts = input.split("~");
       if (parts.length != 2) {
-        throw new IllegalArgumentException("input must have two parts");
+        return null;
       }
       String certificate = parts[0];
       String assertion = parts[1];
       parts = assertion.split("\\.");
       if (parts.length != 3) {
-        throw new IllegalArgumentException("assertion must have three parts");
+        return null;
       }
       String aHeader = new String(Base64.decodeBase64(parts[0]));
       String aPayload = new String(Base64.decodeBase64(parts[1]));
       String aSignature = Utils.byte2Hex(Base64.decodeBase64(parts[2]));
       // We do all the assertion parsing *before* dumping the certificate in
       // case there's a malformed assertion.
-      dumpCertificate(certificate);
-      System.out.println("assertion   header:    " + aHeader);
-      System.out.println("assertion   payload:   " + aPayload);
-      System.out.println("assertion   signature: " + aSignature);
+      ExtendedJSONObject o = new ExtendedJSONObject();
+      o.put("header", new ExtendedJSONObject(aHeader));
+      o.put("payload", new ExtendedJSONObject(aPayload));
+      o.put("signature", aSignature);
+      o.put("certificate", certificate);
+      return o;
+    } catch (Exception e) {
+      return null;
+    }
+  }
+
+  /**
+   * For debugging only!
+   *
+   * @param input assertion to dump.
+   * @return true if the assertion is well-formed.
+   */
+  public static boolean dumpAssertion(String input) {
+    ExtendedJSONObject a = parseAssertion(input);
+    try {
+      if (a == null) {
+        System.out.println("Malformed assertion -- got exception trying to dump contents.");
+        return false;
+      }
+      dumpCertificate(a.getString("certificate"));
+      System.out.println("assertion   header:    " + a.getString("header"));
+      System.out.println("assertion   payload:   " + a.getString("payload"));
+      System.out.println("assertion   signature: " + a.getString("signature"));
       return true;
     } catch (Exception e) {
       System.out.println("Malformed assertion -- got exception trying to dump contents.");
-      e.printStackTrace();
       return false;
     }
   }
 }
--- a/mobile/android/base/fxa/FxAccountConstants.java.in
+++ b/mobile/android/base/fxa/FxAccountConstants.java.in
@@ -6,20 +6,20 @@
 package org.mozilla.gecko.fxa;
 
 import org.mozilla.gecko.background.common.log.Logger;
 
 public class FxAccountConstants {
   public static final String GLOBAL_LOG_TAG = "FxAccounts";
   public static final String ACCOUNT_TYPE = "@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@";
 
-  public static final String DEFAULT_IDP_ENDPOINT = "https://api-accounts-onepw.dev.lcip.org";
-  public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "http://auth.oldsync.dev.lcip.org";
+  public static final String DEFAULT_IDP_ENDPOINT = "https://api-accounts.dev.lcip.org";
+  public static final String DEFAULT_TOKEN_SERVER_ENDPOINT = "https://token.dev.lcip.org";
   public static final String DEFAULT_TOKEN_SERVER_URI = DEFAULT_TOKEN_SERVER_ENDPOINT +
-            (DEFAULT_TOKEN_SERVER_ENDPOINT.endsWith("/") ? "" : "/") + "1.0/sync/1.1";
+            (DEFAULT_TOKEN_SERVER_ENDPOINT.endsWith("/") ? "" : "/") + "1.0/sync/1.5";
 
   // For extra debugging.  Not final so it can be changed from Fennec, or from
   // an add-on.
   public static boolean LOG_PERSONAL_INFORMATION = true;
 
   public static void pii(String tag, String message) {
     if (LOG_PERSONAL_INFORMATION) {
       Logger.info(tag, "$$FxA PII$$: " + message);
--- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
@@ -1,49 +1,54 @@
 /* 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 org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
 
-import android.app.AlertDialog;
 import android.text.Editable;
 import android.text.InputType;
 import android.text.TextWatcher;
 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.Button;
 import android.widget.EditText;
+import android.widget.ProgressBar;
 import android.widget.TextView;
 import android.widget.TextView.OnEditorActionListener;
 
-abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity {
+abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractActivity implements ProgressDisplay {
   public FxAccountAbstractSetupActivity() {
     super(CANNOT_RESUME_WHEN_ACCOUNTS_EXIST | CANNOT_RESUME_WHEN_LOCKED_OUT);
   }
 
   protected FxAccountAbstractSetupActivity(int resume) {
     super(resume);
   }
 
   private static final String LOG_TAG = FxAccountAbstractSetupActivity.class.getSimpleName();
 
   protected int minimumPasswordLength = 8;
 
-  protected TextView localErrorTextView;
   protected EditText emailEdit;
   protected EditText passwordEdit;
   protected Button showPasswordButton;
+  protected TextView remoteErrorTextView;
   protected Button button;
+  protected ProgressBar progressBar;
 
   protected void createShowPasswordButton() {
     showPasswordButton.setOnClickListener(new OnClickListener() {
       @Override
       public void onClick(View v) {
         boolean isShown = 0 == (passwordEdit.getInputType() & InputType.TYPE_TEXT_VARIATION_PASSWORD);
         // Changing input type loses position in edit text; let's try to maintain it.
         int start = passwordEdit.getSelectionStart();
@@ -54,18 +59,30 @@ abstract public class FxAccountAbstractS
           showPasswordButton.setText(R.string.fxaccount_password_show);
         } else {
           showPasswordButton.setText(R.string.fxaccount_password_hide);
         }
       }
     });
   }
 
-  protected void showRemoteError(Exception e) {
-    new AlertDialog.Builder(this).setTitle("Remote error!").setMessage(e.toString()).show();
+  protected void hideRemoteError() {
+    remoteErrorTextView.setVisibility(View.INVISIBLE);
+  }
+
+  protected void showRemoteError(Exception e, int defaultResourceId) {
+    if (e instanceof IOException) {
+      remoteErrorTextView.setText(R.string.fxaccount_remote_error_COULD_NOT_CONNECT);
+    } else if (e instanceof FxAccountClientRemoteException) {
+      remoteErrorTextView.setText(((FxAccountClientRemoteException) e).getErrorMessageStringResource());
+    } else {
+      remoteErrorTextView.setText(defaultResourceId);
+    }
+    Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e);
+    remoteErrorTextView.setVisibility(View.VISIBLE);
   }
 
   protected void addListeners() {
     TextChangedListener textChangedListener = new TextChangedListener();
     EditorActionListener editorActionListener = new EditorActionListener();
     FocusChangeListener focusChangeListener = new FocusChangeListener();
 
     emailEdit.addTextChangedListener(textChangedListener);
@@ -119,16 +136,32 @@ abstract public class FxAccountAbstractS
         (email.length() > 0) &&
         Patterns.EMAIL_ADDRESS.matcher(email).matches() &&
         (password.length() >= minimumPasswordLength);
     return enabled;
   }
 
   protected boolean updateButtonState() {
     boolean enabled = shouldButtonBeEnabled();
+    if (!enabled) {
+      // The user needs to do something before you can interact with the button;
+      // presumably that interaction will fix whatever error is shown.
+      hideRemoteError();
+    }
     if (enabled != button.isEnabled()) {
       Logger.debug(LOG_TAG, (enabled ? "En" : "Dis") + "abling button.");
       button.setEnabled(enabled);
     }
-
     return enabled;
   }
+
+  @Override
+  public void showProgress() {
+    progressBar.setVisibility(View.VISIBLE);
+    button.setVisibility(View.INVISIBLE);
+  }
+
+  @Override
+  public void dismissProgress() {
+    progressBar.setVisibility(View.INVISIBLE);
+    button.setVisibility(View.VISIBLE);
+  }
 }
--- a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
@@ -6,28 +6,26 @@ package org.mozilla.gecko.fxa.activities
 
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 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;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.sync.HTTPFailureException;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
 import android.app.Activity;
 import android.content.Context;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.TextView;
 import android.widget.Toast;
-import ch.boye.httpclientandroidlib.HttpResponse;
 
 /**
  * Activity which displays account created successfully screen to the user, and
  * starts them on the email verification path.
  */
 public class FxAccountConfirmAccountActivity extends Activity implements OnClickListener {
   protected static final String LOG_TAG = FxAccountConfirmAccountActivity.class.getSimpleName();
 
@@ -83,17 +81,17 @@ public class FxAccountConfirmAccountActi
   }
 
   public static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
     protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
 
     protected final byte[] sessionToken;
 
     public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient20 client, RequestDelegate<Void> delegate) {
-      super(context, false, client, delegate);
+      super(context, null, client, delegate);
       this.sessionToken = sessionToken;
     }
 
     @Override
     protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
       try {
         client.resendCode(sessionToken, innerDelegate);
         latch.await();
@@ -109,18 +107,18 @@ public class FxAccountConfirmAccountActi
   protected class ResendCodeDelegate implements RequestDelegate<Void> {
     @Override
     public void handleError(Exception e) {
       Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
       Toast.makeText(getApplicationContext(), R.string.fxaccount_confirm_verification_link_not_sent, Toast.LENGTH_LONG).show();
     }
 
     @Override
-    public void handleFailure(int status, HttpResponse response) {
-      handleError(new HTTPFailureException(new SyncStorageResponse(response)));
+    public void handleFailure(FxAccountClientRemoteException e) {
+      handleError(e);
     }
 
     @Override
     public void handleSuccess(Void result) {
       Toast.makeText(getApplicationContext(), R.string.fxaccount_confirm_verification_link_sent, Toast.LENGTH_SHORT).show();
     }
   }
 
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
@@ -7,38 +7,39 @@ package org.mozilla.gecko.fxa.activities
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.sync.HTTPFailureException;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.fxa.login.Promised;
+import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.setup.Constants;
 
-import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
 import android.widget.EditText;
+import android.widget.ProgressBar;
 import android.widget.TextView;
-import ch.boye.httpclientandroidlib.HttpResponse;
 
 /**
  * Activity which displays create account screen to the user.
  */
 public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivity {
   protected static final String LOG_TAG = FxAccountCreateAccountActivity.class.getSimpleName();
 
   private static final int CHILD_REQUEST_CODE = 2;
@@ -53,22 +54,23 @@ public class FxAccountCreateAccountActiv
   public void onCreate(Bundle icicle) {
     Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
 
     super.onCreate(icicle);
     setContentView(R.layout.fxaccount_create_account);
 
     linkifyTextViews(null, new int[] { R.id.policy });
 
-    localErrorTextView = (TextView) ensureFindViewById(null, R.id.local_error, "local error text view");
     emailEdit = (EditText) 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");
-    button = (Button) ensureFindViewById(null, R.id.create_account_button, "create account button");
+    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");
 
     createCreateAccountButton();
     createYearEdit();
     addListeners();
     updateButtonState();
     createShowPasswordButton();
 
     View signInInsteadLink = ensureFindViewById(null, R.id.sign_in_instead_link, "sign in instead link");
@@ -140,50 +142,54 @@ public class FxAccountCreateAccountActiv
     public CreateAccountDelegate(String email, String password, String serverURI) {
       this.email = email;
       this.password = password;
       this.serverURI = serverURI;
     }
 
     @Override
     public void handleError(Exception e) {
-      showRemoteError(e);
+      showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
     }
 
     @Override
-    public void handleFailure(int status, HttpResponse response) {
-      handleError(new HTTPFailureException(new SyncStorageResponse(response)));
+    public void handleFailure(final FxAccountClientRemoteException e) {
+      showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
     }
 
     @Override
-    public void handleSuccess(String result) {
+    public void handleSuccess(String uid) {
       Activity activity = FxAccountCreateAccountActivity.this;
       Logger.info(LOG_TAG, "Got success creating account.");
 
       // We're on the UI thread, but it's okay to create the account here.
-      Account account;
+      AndroidFxAccount fxAccount;
       try {
         final String profile = Constants.DEFAULT_PROFILE;
         final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
-        account = AndroidFxAccount.addAndroidAccount(activity, email, password,
+        // TODO: This is wasteful.  We should be able to thread these through so they don't get recomputed.
+        byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(email.getBytes("UTF-8"), password.getBytes("UTF-8"));
+        byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
+        State state = new Promised(email, uid, false, unwrapkB, quickStretchedPW);
+        fxAccount = AndroidFxAccount.addAndroidAccount(activity, email, password,
             profile,
             serverURI,
             tokenServerURI,
-            null, null, false);
-        if (account == null) {
-          throw new RuntimeException("XXX what?");
+            state);
+        if (fxAccount == null) {
+          throw new RuntimeException("Could not add Android account.");
         }
       } catch (Exception e) {
         handleError(e);
         return;
       }
 
       // For great debugging.
       if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-        new AndroidFxAccount(activity, account).dump();
+        fxAccount.dump();
       }
 
       // The GetStarted activity has called us and needs to return a result to the authenticator.
       final Intent intent = new Intent();
       intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
       intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
       // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
       setResult(RESULT_OK, intent);
@@ -200,19 +206,20 @@ public class FxAccountCreateAccountActiv
   }
 
   public void createAccount(String email, String password) {
     String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
     RequestDelegate<String> delegate = new CreateAccountDelegate(email, password, serverURI);
     Executor executor = Executors.newSingleThreadExecutor();
     FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
     try {
-      new FxAccountCreateAccountTask(this, email, password, client, delegate).execute();
+      hideRemoteError();
+      new FxAccountCreateAccountTask(this, this, email, password, client, delegate).execute();
     } catch (Exception e) {
-      showRemoteError(e);
+      showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
     }
   }
 
   @Override
   protected boolean shouldButtonBeEnabled() {
     return super.shouldButtonBeEnabled() &&
         (yearEdit.length() > 0);
   }
--- a/mobile/android/base/fxa/activities/FxAccountSetupTask.java
+++ b/mobile/android/base/fxa/activities/FxAccountSetupTask.java
@@ -7,136 +7,131 @@ package org.mozilla.gecko.fxa.activities
 import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 import java.util.concurrent.CountDownLatch;
 
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 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.fxa.activities.FxAccountSetupTask.InnerRequestDelegate;
-import org.mozilla.gecko.sync.HTTPFailureException;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
 
-import android.app.ProgressDialog;
 import android.content.Context;
 import android.os.AsyncTask;
-import ch.boye.httpclientandroidlib.HttpResponse;
 
 /**
  * An <code>AsyncTask</code> wrapper around signing up for, and signing in to, a
  * Firefox Account.
  * <p>
  * It's strange to add explicit blocking to callback-threading code, but we do
  * it here to take advantage of Android's built in support for background work.
  * We really want to avoid making a threading mistake that brings down the whole
  * process.
  */
 abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
-  protected static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
+  private static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
+
+  public interface ProgressDisplay {
+    public void showProgress();
+    public void dismissProgress();
+  }
 
   protected final Context context;
   protected final FxAccountClient20 client;
-
-  protected ProgressDialog progressDialog = null;
+  protected final ProgressDisplay progressDisplay;
 
   // Initialized lazily.
   protected byte[] quickStretchedPW;
 
   // AsyncTask's are one-time-use, so final members are fine.
   protected final CountDownLatch latch = new CountDownLatch(1);
   protected final InnerRequestDelegate<T> innerDelegate = new InnerRequestDelegate<T>(latch);
 
   protected final RequestDelegate<T> delegate;
 
-  public FxAccountSetupTask(Context context, boolean shouldShowProgressDialog, FxAccountClient20 client, RequestDelegate<T> delegate) {
+  public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient20 client, RequestDelegate<T> delegate) {
     this.context = context;
     this.client = client;
     this.delegate = delegate;
-    if (shouldShowProgressDialog) {
-      progressDialog = new ProgressDialog(context);
-    }
+    this.progressDisplay = progressDisplay;
   }
 
   @Override
   protected void onPreExecute() {
-    if (progressDialog != null) {
-      progressDialog.setTitle("Firefox Account..."); // XXX.
-      progressDialog.setMessage("Please wait.");
-      progressDialog.setCancelable(false);
-      progressDialog.setIndeterminate(true);
-      progressDialog.show();
+    if (progressDisplay != null) {
+      progressDisplay.showProgress();
     }
   }
 
   @Override
   protected void onPostExecute(InnerRequestDelegate<T> result) {
-    if (progressDialog != null) {
-      progressDialog.dismiss();
+    if (progressDisplay != null) {
+      progressDisplay.dismissProgress();
     }
 
     // We are on the UI thread, and need to invoke these callbacks here to allow UI updating.
-    if (result.exception instanceof HTTPFailureException) {
-      HTTPFailureException e = (HTTPFailureException) result.exception;
-      delegate.handleFailure(e.response.getStatusCode(), e.response.httpResponse());
+    if (innerDelegate.failure != null) {
+      delegate.handleFailure(innerDelegate.failure);
     } else if (innerDelegate.exception != null) {
       delegate.handleError(innerDelegate.exception);
     } else {
       delegate.handleSuccess(result.response);
     }
   }
 
   @Override
   protected void onCancelled(InnerRequestDelegate<T> result) {
-    if (progressDialog != null) {
-      progressDialog.dismiss();
+    if (progressDisplay != null) {
+      progressDisplay.dismissProgress();
     }
     delegate.handleError(new IllegalStateException("Task was cancelled."));
   }
 
   protected static class InnerRequestDelegate<T> implements RequestDelegate<T> {
     protected final CountDownLatch latch;
     public T response = null;
     public Exception exception = null;
+    public FxAccountClientRemoteException failure = null;
 
     protected InnerRequestDelegate(CountDownLatch latch) {
       this.latch = latch;
     }
 
     @Override
     public void handleError(Exception e) {
       Logger.error(LOG_TAG, "Got exception.");
       this.exception = e;
       latch.countDown();
     }
 
     @Override
-    public void handleFailure(int status, HttpResponse response) {
+    public void handleFailure(FxAccountClientRemoteException e) {
       Logger.warn(LOG_TAG, "Got failure.");
-      this.exception = new HTTPFailureException(new SyncStorageResponse(response));
+      this.failure = e;
       latch.countDown();
     }
 
     @Override
     public void handleSuccess(T result) {
       Logger.info(LOG_TAG, "Got success.");
       this.response = result;
       latch.countDown();
     }
   }
 
   public static class FxAccountCreateAccountTask extends FxAccountSetupTask<String> {
-    protected static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
+    private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
 
     protected final byte[] emailUTF8;
     protected final byte[] passwordUTF8;
 
-    public FxAccountCreateAccountTask(Context context, String email, String password, FxAccountClient20 client, RequestDelegate<String> delegate) throws UnsupportedEncodingException {
-      super(context, true, client, delegate);
+    public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, String password, FxAccountClient20 client, RequestDelegate<String> delegate) throws UnsupportedEncodingException {
+      super(context, progressDisplay, client, delegate);
       this.emailUTF8 = email.getBytes("UTF-8");
       this.passwordUTF8 = password.getBytes("UTF-8");
     }
 
     /**
      * Stretching the password is expensive, so we compute the stretched value lazily.
      *
      * @return stretched password.
@@ -160,23 +155,23 @@ abstract class FxAccountSetupTask<T> ext
         Logger.error(LOG_TAG, "Got exception logging in.", e);
         delegate.handleError(e);
       }
       return null;
     }
   }
 
   public static class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
-    protected static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
+    protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
 
     protected final byte[] emailUTF8;
     protected final byte[] passwordUTF8;
 
-    public FxAccountSignInTask(Context context, String email, String password, FxAccountClient20 client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
-      super(context, true, client, delegate);
+    public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, String password, FxAccountClient20 client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+      super(context, progressDisplay, client, delegate);
       this.emailUTF8 = email.getBytes("UTF-8");
       this.passwordUTF8 = password.getBytes("UTF-8");
     }
 
     /**
      * Stretching the password is expensive, so we compute the stretched value lazily.
      *
      * @return stretched password.
--- a/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
@@ -7,34 +7,35 @@ package org.mozilla.gecko.fxa.activities
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 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;
 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.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.sync.HTTPFailureException;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.fxa.login.Engaged;
+import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.sync.setup.Constants;
 
-import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
 import android.widget.EditText;
+import android.widget.ProgressBar;
 import android.widget.TextView;
-import ch.boye.httpclientandroidlib.HttpResponse;
 
 /**
  * Activity which displays sign in screen to the user.
  */
 public class FxAccountSignInActivity extends FxAccountAbstractSetupActivity {
   protected static final String LOG_TAG = FxAccountSignInActivity.class.getSimpleName();
 
   private static final int CHILD_REQUEST_CODE = 3;
@@ -44,21 +45,22 @@ 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);
 
-    localErrorTextView = (TextView) ensureFindViewById(null, R.id.local_error, "local error text view");
     emailEdit = (EditText) 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");
-    button = (Button) ensureFindViewById(null, R.id.sign_in_button, "sign in 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();
     addListeners();
     updateButtonState();
     createShowPasswordButton();
 
     View signInInsteadLink = ensureFindViewById(null, R.id.create_account_link, "create account instead link");
@@ -109,49 +111,54 @@ public class FxAccountSignInActivity ext
     public SignInDelegate(String email, String password, String serverURI) {
       this.email = email;
       this.password = password;
       this.serverURI = serverURI;
     }
 
     @Override
     public void handleError(Exception e) {
-      showRemoteError(e);
+      showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
     }
 
     @Override
-    public void handleFailure(int status, HttpResponse response) {
-      showRemoteError(new HTTPFailureException(new SyncStorageResponse(response)));
+    public void handleFailure(FxAccountClientRemoteException e) {
+      showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
     }
 
     @Override
     public void handleSuccess(LoginResponse result) {
       Activity activity = FxAccountSignInActivity.this;
       Logger.info(LOG_TAG, "Got success signing in.");
 
       // We're on the UI thread, but it's okay to create the account here.
-      Account account;
+      AndroidFxAccount fxAccount;
       try {
         final String profile = Constants.DEFAULT_PROFILE;
         final String tokenServerURI = FxAccountConstants.DEFAULT_TOKEN_SERVER_URI;
-        account = AndroidFxAccount.addAndroidAccount(activity, email, password,
+        // TODO: This is wasteful.  We should be able to thread these through so they don't get recomputed.
+        byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(email.getBytes("UTF-8"), password.getBytes("UTF-8"));
+        byte[] unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
+        State state = new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken);
+        fxAccount = AndroidFxAccount.addAndroidAccount(activity, email, password,
+            profile,
             serverURI,
             tokenServerURI,
-            profile, result.sessionToken, result.keyFetchToken, result.verified);
-        if (account == null) {
-          throw new RuntimeException("XXX what?");
+            state);
+        if (fxAccount == null) {
+          throw new RuntimeException("Could not add Android account.");
         }
       } catch (Exception e) {
         handleError(e);
         return;
       }
 
       // For great debugging.
       if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-        new AndroidFxAccount(activity, account).dump();
+        fxAccount.dump();
       }
 
       // The GetStarted activity has called us and needs to return a result to the authenticator.
       final Intent intent = new Intent();
       intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, email);
       intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
       // intent.putExtra(AccountManager.KEY_AUTHTOKEN, accountType);
       setResult(RESULT_OK, intent);
@@ -174,19 +181,20 @@ public class FxAccountSignInActivity ext
   }
 
   public void signIn(String email, String password) {
     String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
     RequestDelegate<LoginResponse> delegate = new SignInDelegate(email, password, serverURI);
     Executor executor = Executors.newSingleThreadExecutor();
     FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
     try {
-      new FxAccountSignInTask(this, email, password, client, delegate).execute();
+      hideRemoteError();
+      new FxAccountSignInTask(this, this, email, password, client, delegate).execute();
     } catch (Exception e) {
-      showRemoteError(e);
+      showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
     }
   }
 
   protected void createSignInButton() {
     button.setOnClickListener(new OnClickListener() {
       @Override
       public void onClick(View v) {
         final String email = emailEdit.getText().toString();
--- a/mobile/android/base/fxa/activities/FxAccountStatusActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusActivity.java
@@ -1,158 +1,215 @@
 /* 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 org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
+import org.mozilla.gecko.fxa.login.Married;
+import org.mozilla.gecko.fxa.login.State;
 
 import android.accounts.Account;
+import android.content.ContentResolver;
 import android.os.Bundle;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.widget.TextView;
+import android.widget.ViewFlipper;
 
 /**
  * Activity which displays account status.
  */
 public class FxAccountStatusActivity extends FxAccountAbstractActivity {
   protected static final String LOG_TAG = FxAccountStatusActivity.class.getSimpleName();
 
+  protected ViewFlipper connectionStatusViewFlipper;
   protected View connectionStatusUnverifiedView;
   protected View connectionStatusSignInView;
   protected View connectionStatusSyncingView;
+  protected TextView emailTextView;
 
   public FxAccountStatusActivity() {
     super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
   }
 
   /**
    * {@inheritDoc}
    */
   @Override
   public void onCreate(Bundle icicle) {
     Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
     Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
 
     super.onCreate(icicle);
     setContentView(R.layout.fxaccount_status);
 
-    connectionStatusUnverifiedView = ensureFindViewById(null, R.id.unverified_view, "unverified view");
+    connectionStatusViewFlipper = (ViewFlipper) ensureFindViewById(null, R.id.connection_status_view, "connection status frame layout");
+    connectionStatusUnverifiedView = ensureFindViewById(null, R.id.unverified_view, "unverified vie w");
     connectionStatusSignInView = ensureFindViewById(null, R.id.sign_in_view, "sign in view");
     connectionStatusSyncingView = ensureFindViewById(null, R.id.syncing_view, "syncing view");
 
     launchActivityOnClick(connectionStatusSignInView, FxAccountUpdateCredentialsActivity.class);
+
+    emailTextView = (TextView) findViewById(R.id.email);
+
+    if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+      createDebugButtons();
+    }
+  }
+
+  protected void createDebugButtons() {
+    if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+      return;
+    }
+
+    findViewById(R.id.debug_buttons).setVisibility(View.VISIBLE);
+
+    findViewById(R.id.debug_refresh_button).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Logger.info(LOG_TAG, "Refreshing.");
+        refresh();
+      }
+    });
+
+    findViewById(R.id.debug_dump_button).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Logger.info(LOG_TAG, "Dumping account details.");
+        Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
+        if (accounts.length < 1) {
+          return;
+        }
+        AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
+        account.dump();
+      }
+    });
+
+    findViewById(R.id.debug_sync_button).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Logger.info(LOG_TAG, "Syncing.");
+        Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
+        if (accounts.length < 1) {
+          return;
+        }
+        final Bundle extras = new Bundle();
+        extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
+        ContentResolver.requestSync(accounts[0], BrowserContract.AUTHORITY, extras);
+        // No sense refreshing, since the sync will complete in the future.
+      }
+    });
+
+    findViewById(R.id.debug_forget_certificate_button).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
+        if (accounts.length < 1) {
+          return;
+        }
+        AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
+        State state = account.getState();
+        try {
+          Married married = (Married) state;
+          Logger.info(LOG_TAG, "Moving to Cohabiting state: Forgetting certificate.");
+          account.setState(married.makeCohabitingState());
+          refresh();
+        } catch (ClassCastException e) {
+          Logger.info(LOG_TAG, "Not in Married state; can't forget certificate.");
+          // Ignore.
+        }
+      }
+    });
+
+    findViewById(R.id.debug_require_password_button).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Logger.info(LOG_TAG, "Moving to Separated state: Forgetting password.");
+        Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
+        if (accounts.length < 1) {
+          return;
+        }
+        AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
+        State state = account.getState();
+        account.setState(state.makeSeparatedState());
+        refresh();
+      }
+    });
+
+    findViewById(R.id.debug_require_upgrade_button).setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
+        Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(FxAccountStatusActivity.this);
+        if (accounts.length < 1) {
+          return;
+        }
+        AndroidFxAccount account = new AndroidFxAccount(FxAccountStatusActivity.this, accounts[0]);
+        State state = account.getState();
+        account.setState(state.makeDoghouseState());
+        refresh();
+      }
+    });
   }
 
   @Override
   public void onResume() {
     super.onResume();
     refresh();
   }
 
+  protected void showNeedsUpgrade() {
+    connectionStatusViewFlipper.setDisplayedChild(0);
+  }
+
+  protected void showNeedsPassword() {
+    connectionStatusViewFlipper.setDisplayedChild(1);
+  }
+
+  protected void showNeedsVerification() {
+    connectionStatusViewFlipper.setDisplayedChild(2);
+  }
+
+  protected void showConnected() {
+    connectionStatusViewFlipper.setDisplayedChild(3);
+  }
+
   protected void refresh(Account account) {
-    TextView email = (TextView) findViewById(R.id.email);
-
     if (account == null) {
       redirectToActivity(FxAccountGetStartedActivity.class);
       return;
     }
-
-    AndroidFxAccount fxAccount = new AndroidFxAccount(this, account);
-
-    email.setText(account.name);
+    emailTextView.setText(account.name);
 
-    // Not as good as interrogating state machine, but will do for now.
-    if (!fxAccount.isVerified()) {
-      connectionStatusUnverifiedView.setVisibility(View.VISIBLE);
-      connectionStatusSignInView.setVisibility(View.GONE);
-      connectionStatusSyncingView.setVisibility(View.GONE);
-      return;
+    // Interrogate the Firefox Account's state.
+    AndroidFxAccount fxAccount = new AndroidFxAccount(this, account);
+    State state = fxAccount.getState();
+    switch (state.getNeededAction()) {
+    case NeedsUpgrade:
+      showNeedsUpgrade();
+      break;
+    case NeedsPassword:
+      showNeedsPassword();
+      break;
+    case NeedsVerification:
+      showNeedsVerification();
+      break;
+    default:
+      showConnected();
     }
-
-    if (fxAccount.getQuickStretchedPW() == null) {
-      connectionStatusUnverifiedView.setVisibility(View.GONE);
-      connectionStatusSignInView.setVisibility(View.VISIBLE);
-      connectionStatusSyncingView.setVisibility(View.GONE);
-      return;
-    }
-
-    connectionStatusUnverifiedView.setVisibility(View.GONE);
-    connectionStatusSignInView.setVisibility(View.GONE);
-    connectionStatusSyncingView.setVisibility(View.VISIBLE);
   }
 
   protected void refresh() {
     Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
     if (accounts.length < 1) {
       refresh(null);
       return;
     }
     refresh(accounts[0]);
   }
-
-  protected void dumpAccountDetails() {
-    Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
-    if (accounts.length < 1) {
-      return;
-    }
-    AndroidFxAccount fxAccount = new AndroidFxAccount(this, accounts[0]);
-    fxAccount.dump();
-  }
-
-  protected void forgetAccountTokens() {
-    Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
-    if (accounts.length < 1) {
-      return;
-    }
-    AndroidFxAccount fxAccount = new AndroidFxAccount(this, accounts[0]);
-    fxAccount.forgetAccountTokens();
-    fxAccount.dump();
-  }
-
-  protected void forgetQuickStretchedPW() {
-    Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
-    if (accounts.length < 1) {
-      return;
-    }
-    AndroidFxAccount fxAccount = new AndroidFxAccount(this, accounts[0]);
-    fxAccount.forgetQuickstretchedPW();
-    fxAccount.dump();
-  }
-
-  public void onClickRefresh(View view) {
-    Logger.debug(LOG_TAG, "Refreshing.");
-    refresh();
-  }
-
-  public void onClickForgetAccountTokens(View view) {
-    Logger.debug(LOG_TAG, "Forgetting account tokens.");
-    forgetAccountTokens();
-  }
-
-  public void onClickForgetPassword(View view) {
-    Logger.debug(LOG_TAG, "Forgetting quickStretchedPW.");
-    forgetQuickStretchedPW();
-  }
-
-  public void onClickDumpAccountDetails(View view) {
-    Logger.debug(LOG_TAG, "Dumping account details.");
-    dumpAccountDetails();
-  }
-
-  public void onClickGetStarted(View view) {
-    Logger.debug(LOG_TAG, "Launching get started activity.");
-    redirectToActivity(FxAccountGetStartedActivity.class);
-  }
-
-  public void onClickVerify(View view) {
-    Logger.debug(LOG_TAG, "Launching verification activity.");
-  }
-
-  public void onClickSignIn(View view) {
-    Logger.debug(LOG_TAG, "Launching sign in again activity.");
-  }
 }
--- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
@@ -9,41 +9,44 @@ import java.security.GeneralSecurityExce
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
 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;
 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.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
-import org.mozilla.gecko.sync.HTTPFailureException;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.fxa.login.Engaged;
+import org.mozilla.gecko.fxa.login.Separated;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
 
 import android.accounts.Account;
-import android.app.Activity;
 import android.os.Bundle;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.Button;
 import android.widget.EditText;
+import android.widget.ProgressBar;
 import android.widget.TextView;
-import ch.boye.httpclientandroidlib.HttpResponse;
 
 /**
  * Activity which displays a screen for updating the local password.
  */
 public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
   protected static final String LOG_TAG = FxAccountUpdateCredentialsActivity.class.getSimpleName();
 
-  protected Account account;
+  protected AndroidFxAccount fxAccount;
+  protected Separated accountState;
 
   public FxAccountUpdateCredentialsActivity() {
     // We want to share code with the other setup activities, but this activity
     // doesn't create a new Android Account, it modifies an existing one. If you
     // manage to get an account, and somehow be locked out too, we'll let you
     // update it.
     super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
   }
@@ -53,21 +56,22 @@ public class FxAccountUpdateCredentialsA
    */
   @Override
   public void onCreate(Bundle icicle) {
     Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
 
     super.onCreate(icicle);
     setContentView(R.layout.fxaccount_update_credentials);
 
-    localErrorTextView = (TextView) ensureFindViewById(null, R.id.local_error, "local error text view");
     emailEdit = (EditText) 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();
     addListeners();
     updateButtonState();
     createShowPasswordButton();
 
     emailEdit.setEnabled(false);
@@ -75,80 +79,103 @@ public class FxAccountUpdateCredentialsA
     // Not yet implemented.
     // this.launchActivityOnClick(ensureFindViewById(null, R.id.forgot_password_link, "forgot password link"), null);
   }
 
   @Override
   public void onResume() {
     super.onResume();
     Account accounts[] = FxAccountAuthenticator.getFirefoxAccounts(this);
-    account = accounts[0];
-    if (account == null) {
+    if (accounts.length < 1 || accounts[0] == null) {
+      Logger.warn(LOG_TAG, "No Android accounts.");
       setResult(RESULT_CANCELED);
       finish();
       return;
     }
-    emailEdit.setText(account.name);
+    this.fxAccount = new AndroidFxAccount(this, accounts[0]);
+    if (fxAccount == null) {
+      Logger.warn(LOG_TAG, "Could not get Firefox Account from Android account.");
+      setResult(RESULT_CANCELED);
+      finish();
+      return;
+    }
+    State state = fxAccount.getState();
+    if (state.getStateLabel() != StateLabel.Separated) {
+      Logger.warn(LOG_TAG, "Could not get state from Firefox Account.");
+      setResult(RESULT_CANCELED);
+      finish();
+      return;
+    }
+    this.accountState = (Separated) state;
+    emailEdit.setText(fxAccount.getAndroidAccount().name);
   }
 
   protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
     public final String email;
     public final String password;
     public final String serverURI;
     public final byte[] quickStretchedPW;
 
     public UpdateCredentialsDelegate(String email, String password, String serverURI) throws UnsupportedEncodingException, GeneralSecurityException {
       this.email = email;
       this.password = password;
       this.serverURI = serverURI;
+      // XXX This needs to be calculated lazily.
       this.quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(email.getBytes("UTF-8"), password.getBytes("UTF-8"));
     }
 
     @Override
     public void handleError(Exception e) {
-      showRemoteError(e);
+      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
     }
 
     @Override
-    public void handleFailure(int status, HttpResponse response) {
-      showRemoteError(new HTTPFailureException(new SyncStorageResponse(response)));
+    public void handleFailure(FxAccountClientRemoteException e) {
+      // TODO On isUpgradeRequired, transition to Doghouse state.
+      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
     }
 
     @Override
     public void handleSuccess(LoginResponse result) {
-      Activity activity = FxAccountUpdateCredentialsActivity.this;
       Logger.info(LOG_TAG, "Got success signing in.");
 
-      if (account == null) {
-        Logger.warn(LOG_TAG, "account must not be null");
+      if (fxAccount == null) {
+        this.handleError(new IllegalStateException("fxAccount must not be null"));
         return;
       }
 
-      AndroidFxAccount fxAccount = new AndroidFxAccount(activity, account);
-      // XXX wasteful, should only do this once.
-      fxAccount.setQuickStretchedPW(quickStretchedPW);
+      byte[] unwrapkB;
+      try {
+        unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
+      } catch (Exception e) {
+        this.handleError(e);
+        return;
+      }
+      fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
 
       // For great debugging.
       if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
         fxAccount.dump();
       }
 
       redirectToActivity(FxAccountStatusActivity.class);
     }
   }
 
   public void updateCredentials(String email, String password) {
     String serverURI = FxAccountConstants.DEFAULT_IDP_ENDPOINT;
     Executor executor = Executors.newSingleThreadExecutor();
     FxAccountClient20 client = new FxAccountClient20(serverURI, executor);
     try {
+      hideRemoteError();
       RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, password, serverURI);
-      new FxAccountSignInTask(this, email, password, client, delegate).execute();
+      new FxAccountSignInTask(this, this, email, password, client, delegate).execute();
     } catch (Exception e) {
-      showRemoteError(e);
+      Logger.warn(LOG_TAG, "Got exception updating credentials for account.", e);
+      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
     }
   }
 
   protected void createButton() {
     button.setOnClickListener(new OnClickListener() {
       @Override
       public void onClick(View v) {
         final String email = emailEdit.getText().toString();
deleted file mode 100644
--- a/mobile/android/base/fxa/authenticator/AbstractFxAccount.java
+++ /dev/null
@@ -1,99 +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.authenticator;
-
-import java.security.GeneralSecurityException;
-
-import org.mozilla.gecko.browserid.BrowserIDKeyPair;
-
-/**
- * A representation of a Firefox Account.
- * <p>
- * Keeps track of:
- * <ul>
- * <li>tokens;</li>
- * <li>verification state;</li>
- * <li>auth server managed keys;</li>
- * <li>locally managed key pairs</li>
- * </ul>
- * <p>
- * <code>kA</code> is a <i>recoverable</i> auth server managed key.
- * <code>kB</code> is an <i>unrecoverable</i> auth server managed key. Changing
- * the user's password maintains <code>kA</code> and <code>kB</code>, but
- * resetting the user's password retains only <code>kA</code> (and losees
- * <code>kB</code>).
- * <p>
- * The entropy of <code>kB</code> is partially derived from the server and
- * partially from the user's password. The auth server stores <code>kB</code>
- * remotely, wrapped in a key derived from the user's password. The unwrapping
- * process is implementation specific, but it is expected that the appropriate
- * derivative of the user's password will be stored until
- * <code>setWrappedKb</code> is called, at which point <code>kB</code> will be
- * computed and cached, ready to be returned by <code>getKb</code>.
- */
-public interface AbstractFxAccount {
-  /**
-   * Get the Firefox Account auth server URI that this account login flow should
-   * talk to.
-   */
-  public String getAccountServerURI();
-
-  /**
-   * @return the profile name associated with the account, such as "default".
-   */
-  public String getProfile();
-
-  public boolean isValid();
-  public void setInvalid();
-
-  public byte[] getSessionToken();
-  public byte[] getKeyFetchToken();
-
-  public void setSessionToken(byte[] token);
-  public void setKeyFetchToken(byte[] token);
-
-  /**
-   * Return true if and only if this account is guaranteed to be verified. This
-   * is intended to be a local cache of the verified state. Do not query the
-   * auth server!
-   */
-  public boolean isVerified();
-
-  /**
-   * Update the account's local cache to reflect that this account is known to
-   * be verified.
-   */
-  public void setVerified();
-
-  public byte[] getKa();
-  public void setKa(byte[] kA);
-
-  public byte[] getKb();
-
-  /**
-   * The auth server returns <code>kA</code> and <code>wrap(kB)</code> in
-   * response to <code>/account/keys</code>. This method accepts that wrapped
-   * value and uses whatever (per concrete type) method it can to derive the
-   * unwrapped value and cache it for retrieval by <code>getKb</code>.
-   * <p>
-   * See also {@link AbstractFxAccount}.
-   *
-   * @param wrappedKb <code>wrap(kB)</code> from auth server response.
-   */
-  public void setWrappedKb(byte[] wrappedKb);
-
-  BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException;
-
-  public String getCertificate();
-  public void setCertificate(String certificate);
-
-  public String getAssertion();
-  public void setAssertion(String assertion);
-
-  public byte[] getEmailUTF8();
-
-  public byte[] getQuickStretchedPW();
-  public void setQuickStretchedPW(byte[] quickStretchedPW);
-}
--- a/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/base/fxa/authenticator/AndroidFxAccount.java
@@ -7,64 +7,55 @@ package org.mozilla.gecko.fxa.authentica
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Collections;
 
 import org.mozilla.gecko.background.common.GlobalConstants;
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.browserid.BrowserIDKeyPair;
-import org.mozilla.gecko.browserid.RSACryptoImplementation;
 import org.mozilla.gecko.fxa.FxAccountConstants;
+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.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.Context;
 import android.os.Bundle;
 
 /**
  * A Firefox Account that stores its details and state as user data attached to
  * an Android Account instance.
  * <p>
  * Account user data is accessible only to the Android App(s) that own the
  * Account type. Account user data is not removed when the App's private data is
  * cleared.
  */
-public class AndroidFxAccount implements AbstractFxAccount {
+public class AndroidFxAccount {
   protected static final String LOG_TAG = AndroidFxAccount.class.getSimpleName();
 
   public static final int CURRENT_PREFS_VERSION = 1;
 
   public static final int CURRENT_ACCOUNT_VERSION = 3;
   public static final String ACCOUNT_KEY_ACCOUNT_VERSION = "version";
   public static final String ACCOUNT_KEY_PROFILE = "profile";
   public static final String ACCOUNT_KEY_IDP_SERVER = "idpServerURI";
 
   // The audience should always be a prefix of the token server URI.
   public static final String ACCOUNT_KEY_AUDIENCE = "audience";                 // Sync-specific.
   public static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI";       // Sync-specific.
   public static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
 
-  public static final int CURRENT_BUNDLE_VERSION = 1;
+  public static final int CURRENT_BUNDLE_VERSION = 2;
   public static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
-  public static final String BUNDLE_KEY_ASSERTION = "assertion";
-  public static final String BUNDLE_KEY_CERTIFICATE = "certificate";
-  public static final String BUNDLE_KEY_INVALID = "invalid";
-  public static final String BUNDLE_KEY_SESSION_TOKEN = "sessionToken";
-  public static final String BUNDLE_KEY_KEY_FETCH_TOKEN = "keyFetchToken";
-  public static final String BUNDLE_KEY_VERIFIED = "verified";
-  public static final String BUNDLE_KEY_KA = "kA";
-  public static final String BUNDLE_KEY_KB = "kB";
-  public static final String BUNDLE_KEY_UNWRAPKB = "unwrapkB";
-  public static final String BUNDLE_KEY_ASSERTION_KEY_PAIR = "assertionKeyPair";
+  public static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
+  public static final String BUNDLE_KEY_STATE = "state";
 
   protected final Context context;
   protected final AccountManager accountManager;
   protected final Account account;
 
   /**
    * Create an Android Firefox Account instance backed by an Android Account
    * instance.
@@ -82,16 +73,20 @@ public class AndroidFxAccount implements
    *          Android account to use for storage.
    */
   public AndroidFxAccount(Context applicationContext, Account account) {
     this.context = applicationContext;
     this.account = account;
     this.accountManager = AccountManager.get(this.context);
   }
 
+  public Account getAndroidAccount() {
+    return this.account;
+  }
+
   protected int getAccountVersion() {
     String v = accountManager.getUserData(account, ACCOUNT_KEY_ACCOUNT_VERSION);
     if (v == null) {
       return 0;         // Implicit.
     }
 
     try {
       return Integer.parseInt(v, 10);
@@ -102,30 +97,31 @@ public class AndroidFxAccount implements
 
   protected void persistBundle(ExtendedJSONObject bundle) {
     accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
   }
 
   protected ExtendedJSONObject unbundle() {
     final int version = getAccountVersion();
     if (version < CURRENT_ACCOUNT_VERSION) {
-      // Needs upgrade. For now, do nothing.
+      // Needs upgrade. For now, do nothing. We'd like to just put your account
+      // into the Separated state here and have you update your credentials.
       return null;
     }
 
     if (version > CURRENT_ACCOUNT_VERSION) {
       // Oh dear.
       return null;
     }
 
     String bundle = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
     if (bundle == null) {
       return null;
     }
-    return unbundleAccountV1(bundle);
+    return unbundleAccountV2(bundle);
   }
 
   protected String getBundleData(String key) {
     ExtendedJSONObject o = unbundle();
     if (o == null) {
       return null;
     }
     return o.getString(key);
@@ -181,36 +177,28 @@ public class AndroidFxAccount implements
       return null;
     }
     if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
       return o;
     }
     return null;
   }
 
-  @Override
-  public byte[] getEmailUTF8() {
-    try {
-      return account.name.getBytes("UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      // Ignore.
-      return null;
-    }
+  private ExtendedJSONObject unbundleAccountV2(String bundle) {
+    return unbundleAccountV1(bundle);
   }
 
   /**
    * Note that if the user clears data, an account will be left pointing to a
    * deleted profile. Such is life.
    */
-  @Override
   public String getProfile() {
     return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
   }
 
-  @Override
   public String getAccountServerURI() {
     return accountManager.getUserData(account, ACCOUNT_KEY_IDP_SERVER);
   }
 
   public String getAudience() {
     return accountManager.getUserData(account, ACCOUNT_KEY_AUDIENCE);
   }
 
@@ -246,138 +234,16 @@ public class AndroidFxAccount implements
     final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".fxa";
     final long version = CURRENT_PREFS_VERSION;
 
     // This is unique for each syncing 'view' of the account.
     final String serverURLThing = fxaServerURI + "!" + tokenServerURI;
     return Utils.getPrefsPath(product, username, serverURLThing, profile, version);
   }
 
-  @Override
-  public void setQuickStretchedPW(byte[] quickStretchedPW) {
-    accountManager.setPassword(account, quickStretchedPW == null ? null : Utils.byte2Hex(quickStretchedPW));
-  }
-
-
-  @Override
-  public byte[] getQuickStretchedPW() {
-    String quickStretchedPW = accountManager.getPassword(account);
-    return quickStretchedPW == null ? null : Utils.hex2Byte(quickStretchedPW);
-  }
-
-  @Override
-  public byte[] getSessionToken() {
-    return getBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN);
-  }
-
-  @Override
-  public byte[] getKeyFetchToken() {
-    return getBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN);
-  }
-
-  @Override
-  public void setSessionToken(byte[] sessionToken) {
-    updateBundleDataBytes(BUNDLE_KEY_SESSION_TOKEN, sessionToken);
-  }
-
-  @Override
-  public void setKeyFetchToken(byte[] keyFetchToken) {
-    updateBundleDataBytes(BUNDLE_KEY_KEY_FETCH_TOKEN, keyFetchToken);
-  }
-
-  @Override
-  public boolean isVerified() {
-    return getBundleDataBoolean(BUNDLE_KEY_VERIFIED, false);
-  }
-
-  @Override
-  public void setVerified() {
-    updateBundleValue(BUNDLE_KEY_VERIFIED, true);
-  }
-
-  @Override
-  public byte[] getKa() {
-    return getBundleDataBytes(BUNDLE_KEY_KA);
-  }
-
-  @Override
-  public void setKa(byte[] kA) {
-    updateBundleValue(BUNDLE_KEY_KA, Utils.byte2Hex(kA));
-  }
-
-  @Override
-  public void setWrappedKb(byte[] wrappedKb) {
-    if (wrappedKb == null) {
-      final String message = "wrappedKb is null: cannot set kB.";
-      Logger.error(LOG_TAG, message);
-      throw new IllegalArgumentException(message);
-    }
-    byte[] unwrapKb = getBundleDataBytes(BUNDLE_KEY_UNWRAPKB);
-    if (unwrapKb == null) {
-      Logger.error(LOG_TAG, "unwrapKb is null: cannot set kB.");
-      return;
-    }
-    byte[] kB = new byte[wrappedKb.length]; // We could hard-code this to be 32.
-    for (int i = 0; i < wrappedKb.length; i++) {
-      kB[i] = (byte) (wrappedKb[i] ^ unwrapKb[i]);
-    }
-    updateBundleValue(BUNDLE_KEY_KB, Utils.byte2Hex(kB));
-  }
-
-  @Override
-  public byte[] getKb() {
-    return getBundleDataBytes(BUNDLE_KEY_KB);
-  }
-
-  protected BrowserIDKeyPair generateNewAssertionKeyPair() throws GeneralSecurityException {
-    Logger.info(LOG_TAG, "Generating new assertion key pair.");
-    // TODO Have the key size be a non-constant in FxAccountUtils, or read from SharedPreferences, or...
-    return RSACryptoImplementation.generateKeyPair(1024);
-  }
-
-  @Override
-  public BrowserIDKeyPair getAssertionKeyPair() throws GeneralSecurityException {
-    try {
-      String data = getBundleData(BUNDLE_KEY_ASSERTION_KEY_PAIR);
-      return RSACryptoImplementation.fromJSONObject(new ExtendedJSONObject(data));
-    } catch (Exception e) {
-      // Fall through to generating a new key pair.
-    }
-
-    BrowserIDKeyPair keyPair = generateNewAssertionKeyPair();
-
-    ExtendedJSONObject descriptor = unbundle();
-    if (descriptor == null) {
-      descriptor = new ExtendedJSONObject();
-    }
-    descriptor.put(BUNDLE_KEY_ASSERTION_KEY_PAIR, keyPair.toJSONObject().toJSONString());
-    persistBundle(descriptor);
-    return keyPair;
-  }
-
-  @Override
-  public String getCertificate() {
-    return getBundleData(BUNDLE_KEY_CERTIFICATE);
-  }
-
-  @Override
-  public void setCertificate(String certificate) {
-    updateBundleValue(BUNDLE_KEY_CERTIFICATE, certificate);
-  }
-
-  @Override
-  public String getAssertion() {
-    return getBundleData(BUNDLE_KEY_ASSERTION);
-  }
-
-  @Override
-  public void setAssertion(String assertion) {
-    updateBundleValue(BUNDLE_KEY_ASSERTION, assertion);
-  }
-
   /**
    * Extract a JSON dictionary of the string values associated to this account.
    * <p>
    * <b>For debugging use only!</b> The contents of this JSON object completely
    * determine the user's Firefox Account status and yield access to whatever
    * user data the device has access to.
    *
    * @return JSON-object of Strings.
@@ -385,128 +251,127 @@ public class AndroidFxAccount implements
   public ExtendedJSONObject toJSONObject() {
     ExtendedJSONObject o = unbundle();
     o.put("email", account.name);
     try {
       o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
     } catch (UnsupportedEncodingException e) {
       // Ignore.
     }
-    o.put("quickStretchedPW", accountManager.getPassword(account));
     return o;
   }
 
-  public static Account addAndroidAccount(
+  public static AndroidFxAccount addAndroidAccount(
       Context context,
       String email,
       String password,
       String profile,
       String idpServerURI,
       String tokenServerURI,
-      byte[] sessionToken,
-      byte[] keyFetchToken,
-      boolean verified)
+      State state)
           throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
     if (email == null) {
       throw new IllegalArgumentException("email must not be null");
     }
     if (password == null) {
       throw new IllegalArgumentException("password must not be null");
     }
     if (idpServerURI == null) {
       throw new IllegalArgumentException("idpServerURI must not be null");
     }
     if (tokenServerURI == null) {
       throw new IllegalArgumentException("tokenServerURI must not be null");
     }
-    // sessionToken and keyFetchToken are allowed to be null; they can be
-    // fetched via /account/login from the password. These tokens are generated
-    // by the server and we have no length or formatting guarantees. However, if
-    // one is given, both should be given: they come from the server together.
-    if ((sessionToken == null && keyFetchToken != null) ||
-        (sessionToken != null && keyFetchToken == null)) {
-      throw new IllegalArgumentException("none or both of sessionToken and keyFetchToken may be null");
+    if (state == null) {
+      throw new IllegalArgumentException("state must not be null");
     }
 
-    byte[] emailUTF8 = email.getBytes("UTF-8");
-    byte[] passwordUTF8 = password.getBytes("UTF-8");
-    byte[] quickStretchedPW = FxAccountUtils.generateQuickStretchedPW(emailUTF8, passwordUTF8);
-    byte[] unwrapBkey = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
-
     // Android has internal restrictions that require all values in this
     // bundle to be strings. *sigh*
     Bundle userdata = new Bundle();
     userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
     userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
     userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
     userdata.putString(ACCOUNT_KEY_AUDIENCE, computeAudience(tokenServerURI));
     userdata.putString(ACCOUNT_KEY_PROFILE, profile);
 
     ExtendedJSONObject descriptor = new ExtendedJSONObject();
+
+    descriptor.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
+    descriptor.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
+
     descriptor.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
-    descriptor.put(BUNDLE_KEY_SESSION_TOKEN, sessionToken == null ? null : Utils.byte2Hex(sessionToken));
-    descriptor.put(BUNDLE_KEY_KEY_FETCH_TOKEN, keyFetchToken == null ? null : Utils.byte2Hex(keyFetchToken));
-    descriptor.put(BUNDLE_KEY_VERIFIED, verified);
-    descriptor.put(BUNDLE_KEY_UNWRAPKB, Utils.byte2Hex(unwrapBkey));
-
     userdata.putString(ACCOUNT_KEY_DESCRIPTOR, descriptor.toJSONString());
 
     Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
     AccountManager accountManager = AccountManager.get(context);
-    boolean added = accountManager.addAccountExplicitly(account, Utils.byte2Hex(quickStretchedPW), userdata);
+    boolean added = accountManager.addAccountExplicitly(account, null, userdata); // XXX what should the password be?
     if (!added) {
       return null;
     }
+
+    AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+    fxAccount.clearSyncPrefs();
+    fxAccount.enableSyncing();
+
+    return fxAccount;
+  }
+
+  public void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
+    context.getSharedPreferences(getSyncPrefsPath(), Utils.SHARED_PREFERENCES_MODE).edit().clear().commit();
+  }
+
+  public void enableSyncing() {
     FxAccountAuthenticator.enableSyncing(context, account);
-    return account;
+  }
+
+  public void disableSyncing() {
+    FxAccountAuthenticator.disableSyncing(context, account);
+  }
+
+  public synchronized void setState(State state) {
+    if (state == null) {
+      throw new IllegalArgumentException("state must not be null");
+    }
+    updateBundleValue(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
+    updateBundleValue(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
+  }
+
+  public synchronized State getState() {
+    String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
+    String stateString = getBundleData(BUNDLE_KEY_STATE);
+    if (stateLabelString == null) {
+      throw new IllegalStateException("stateLabelString must not be null");
+    }
+    if (stateString == null) {
+      throw new IllegalStateException("stateString must not be null");
+    }
+
+    try {
+      StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
+      return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
+    } catch (Exception e) {
+      throw new IllegalStateException("could not get state", e);
+    }
   }
 
   // TODO: this is shit.
   private static String computeAudience(String tokenServerURI) throws URISyntaxException {
-     URI uri = new URI(tokenServerURI);
-     return new URI(uri.getScheme(), uri.getHost(), null, null).toString();
+    URI uri = new URI(tokenServerURI);
+    return new URI(uri.getScheme(), uri.getHost(), null, null).toString();
   }
 
-  @Override
-  public boolean isValid() {
-    return !getBundleDataBoolean(BUNDLE_KEY_INVALID, false);
-  }
-
-  @Override
-  public void setInvalid() {
-    updateBundleValue(BUNDLE_KEY_INVALID, true);
-  }
 
   /**
    * <b>For debugging only!</b>
    */
   public void dump() {
     if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
       return;
     }
     ExtendedJSONObject o = toJSONObject();
     ArrayList<String> list = new ArrayList<String>(o.keySet());
     Collections.sort(list);
     for (String key : list) {
       FxAccountConstants.pii(LOG_TAG, key + ": " + o.get(key));
     }
   }
-
-  /**
-   * <b>For debugging only!</b>
-   */
-  public void forgetAccountTokens() {
-    ExtendedJSONObject descriptor = unbundle();
-    if (descriptor == null) {
-      return;
-    }
-    descriptor.remove(BUNDLE_KEY_SESSION_TOKEN);
-    descriptor.remove(BUNDLE_KEY_KEY_FETCH_TOKEN);
-    persistBundle(descriptor);
-  }
-
-  /**
-   * <b>For debugging only!</b>
-   */
-  public void forgetQuickstretchedPW() {
-    accountManager.setPassword(account, null);
-  }
 }
--- a/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java
+++ b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java
@@ -42,16 +42,24 @@ public class FxAccountAuthenticator exte
     for (String authority : new String[] {
         AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
     }) {
       ContentResolver.setSyncAutomatically(account, authority, true);
       ContentResolver.setIsSyncable(account, authority, 1);
     }
   }
 
+  protected static void disableSyncing(Context context, Account account) {
+    for (String authority : new String[] {
+        AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
+    }) {
+      ContentResolver.setSyncAutomatically(account, authority, false);
+    }
+  }
+
   public static Account addAccount(Context context, String email, String uid, String sessionToken, String kA, String kB) {
     final AccountManager accountManager = AccountManager.get(context);
     final Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
     final Bundle userData = new Bundle();
     userData.putString(JSON_KEY_UID, uid);
     userData.putString(JSON_KEY_SESSION_TOKEN, sessionToken);
     userData.putString(JSON_KEY_KA, kA);
     userData.putString(JSON_KEY_KB, kB);
deleted file mode 100644
--- a/mobile/android/base/fxa/authenticator/FxAccountLoginPolicy.java
+++ /dev/null
@@ -1,586 +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.authenticator;
-
-import java.util.LinkedList;
-import java.util.concurrent.Executor;
-
-import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountClient;
-import org.mozilla.gecko.background.fxa.FxAccountClient10;
-import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
-import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
-import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
-import org.mozilla.gecko.background.fxa.FxAccountClient20;
-import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
-import org.mozilla.gecko.background.fxa.SkewHandler;
-import org.mozilla.gecko.browserid.BrowserIDKeyPair;
-import org.mozilla.gecko.browserid.JSONWebTokenUtils;
-import org.mozilla.gecko.browserid.VerifyingPublicKey;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginAccountNotVerifiedException;
-import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException.FxAccountLoginBadPasswordException;
-import org.mozilla.gecko.sync.HTTPFailureException;
-import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.sync.net.SyncStorageResponse;
-
-import android.content.Context;
-import ch.boye.httpclientandroidlib.HttpResponse;
-
-public class FxAccountLoginPolicy {
-  public static final String LOG_TAG = FxAccountLoginPolicy.class.getSimpleName();
-
-  public final Context context;
-  public final AbstractFxAccount fxAccount;
-  public final Executor executor;
-
-  public FxAccountLoginPolicy(Context context, AbstractFxAccount fxAccount, Executor executor) {
-    this.context = context;
-    this.fxAccount = fxAccount;
-    this.executor = executor;
-  }
-
-  public long certificateDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
-  public long assertionDurationInMilliseconds = JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS;
-
-  public long getCertificateDurationInMilliseconds() {
-    return certificateDurationInMilliseconds;
-  }
-
-  public long getAssertionDurationInMilliseconds() {
-    return assertionDurationInMilliseconds;
-  }
-
-  protected FxAccountClient makeFxAccountClient() {
-    String serverURI = fxAccount.getAccountServerURI();
-    return new FxAccountClient20(serverURI, executor);
-  }
-
-  private SkewHandler skewHandler;
-
-  /**
-   * Check if this certificate is not worth generating an assertion from: for
-   * example, because it is not well-formed, or it is already expired.
-   *
-   * @param certificate
-   *          to check.
-   * @return if it is definitely not worth generating an assertion from this
-   *         certificate.
-   */
-  protected boolean isInvalidCertificate(String certificate) {
-    return false;
-  }
-
-  /**
-   * Check if this assertion is not worth presenting to the token server: for
-   * example, because it is not well-formed, or it is already expired.
-   *
-   * @param assertion
-   *          to check.
-   * @return if assertion is definitely not worth presenting to the token
-   *         server.
-   */
-  protected boolean isInvalidAssertion(String assertion) {
-    return false;
-  }
-
-  protected long now() {
-    return System.currentTimeMillis();
-  }
-
-  public enum AccountState {
-    Invalid,
-    NeedsSessionToken,
-    NeedsVerification,
-    NeedsKeys,
-    NeedsCertificate,
-    NeedsAssertion,
-    Valid,
-  };
-
-  public AccountState getAccountState(AbstractFxAccount fxAccount) {
-    String serverURI = fxAccount.getAccountServerURI();
-    byte[] emailUTF8 = fxAccount.getEmailUTF8();
-    byte[] quickStretchedPW = fxAccount.getQuickStretchedPW();
-    if (!fxAccount.isValid() || serverURI == null || emailUTF8 == null || quickStretchedPW == null) {
-      return AccountState.Invalid;
-    }
-
-    byte[] sessionToken = fxAccount.getSessionToken();
-    if (sessionToken == null) {
-      return AccountState.NeedsSessionToken;
-    }
-
-    if (!fxAccount.isVerified()) {
-      return AccountState.NeedsVerification;
-    }
-
-    // Verify against server?  Tricky.
-    if (fxAccount.getKa() == null || fxAccount.getKb() == null) {
-      return AccountState.NeedsKeys;
-    }
-
-    String certificate = fxAccount.getCertificate();
-    if (certificate == null || isInvalidCertificate(certificate)) {
-      return AccountState.NeedsCertificate;
-    }
-
-    String assertion = fxAccount.getAssertion();
-    if (assertion == null || isInvalidAssertion(assertion)) {
-      return AccountState.NeedsAssertion;
-    }
-
-    return AccountState.Valid;
-  }
-
-  protected interface LoginStage {
-    public void execute(LoginStageDelegate delegate) throws Exception;
-  }
-
-  protected LinkedList<LoginStage> getStages(AccountState state) {
-    final LinkedList<LoginStage> stages = new LinkedList<LoginStage>();
-    if (state == AccountState.Invalid) {
-      stages.addFirst(new FailStage());
-      return stages;
-    }
-
-    stages.addFirst(new SuccessStage());
-    if (state == AccountState.Valid) {
-      return stages;
-    }
-    stages.addFirst(new EnsureAssertionStage());
-    if (state == AccountState.NeedsAssertion) {
-      return stages;
-    }
-    stages.addFirst(new EnsureCertificateStage());
-    if (state == AccountState.NeedsCertificate) {
-      return stages;
-    }
-    stages.addFirst(new EnsureKeysStage());
-    stages.addFirst(new EnsureKeyFetchTokenStage());
-    if (state == AccountState.NeedsKeys) {
-      return stages;
-    }
-    stages.addFirst(new EnsureVerificationStage());
-    if (state == AccountState.NeedsVerification) {
-      return stages;
-    }
-    stages.addFirst(new EnsureSessionTokenStage());
-    if (state == AccountState.NeedsSessionToken) {
-      return stages;
-    }
-    return stages;
-  }
-
-  public void login(final String audience, final FxAccountLoginDelegate delegate, final SkewHandler skewHandler) {
-    this.skewHandler = skewHandler;
-    this.login(audience, delegate);
-  }
-
-  /**
-   * Do as much of a Firefox Account login dance as possible.
-   * <p>
-   * To avoid deeply nested callbacks, we maintain a simple queue of stages to
-   * execute in sequence.
-   *
-   * @param audience to generate assertion for.
-   * @param delegate providing callbacks to invoke.
-   */
-  public void login(final String audience, final FxAccountLoginDelegate delegate) {
-    final AccountState initialState = getAccountState(fxAccount);
-    Logger.info(LOG_TAG, "Logging in account from initial state " + initialState + ".");
-
-    final LinkedList<LoginStage> stages = getStages(initialState);
-    final LinkedList<String> stageNames = new LinkedList<String>();
-    for (LoginStage stage : stages) {
-      stageNames.add(stage.getClass().getSimpleName());
-    }
-    Logger.info(LOG_TAG, "Executing stages: [" + Utils.toCommaSeparatedString(stageNames) + "]");
-
-    LoginStageDelegate loginStageDelegate = new LoginStageDelegate(stages, audience, delegate);
-    loginStageDelegate.advance();
-  }
-
-  protected class LoginStageDelegate {
-    public final LinkedList<LoginStage> stages;
-    public final String audience;
-    public final FxAccountLoginDelegate delegate;
-    public final FxAccountClient client;
-
-    protected LoginStage currentStage = null;
-
-    public LoginStageDelegate(LinkedList<LoginStage> stages, String audience, FxAccountLoginDelegate delegate) {
-      this.stages = stages;
-      this.audience = audience;
-      this.delegate = delegate;
-      this.client = makeFxAccountClient();
-    }
-
-    protected void invokeHandleHardFailure(final FxAccountLoginDelegate delegate, final FxAccountLoginException e) {
-      executor.execute(new Runnable() {
-        @Override
-        public void run() {
-          delegate.handleError(e);
-        }
-      });
-    }
-
-    public void advance() {
-      currentStage = stages.poll();
-      if (currentStage == null) {
-        // No more stages.  But we haven't seen an assertion. Failure!
-        Logger.info(LOG_TAG, "No more stages: login failed?");
-        invokeHandleHardFailure(delegate, new FxAccountLoginException("No more stages, but no assertion: login failed?"));
-        return;
-      }
-
-      try {
-        Logger.info(LOG_TAG, "Executing stage: " + currentStage.getClass().getSimpleName());
-        currentStage.execute(this);
-      } catch (Exception e) {
-        Logger.info(LOG_TAG, "Got exception during stage.", e);
-        invokeHandleHardFailure(delegate, new FxAccountLoginException(e));
-        return;
-      }
-    }
-
-    public void handleStageSuccess() {
-      Logger.info(LOG_TAG, "Stage succeeded: " + currentStage.getClass().getSimpleName());
-      advance();
-    }
-
-    public void handleLoginSuccess(final String assertion) {
-      Logger.info(LOG_TAG, "Login succeeded.");
-      executor.execute(new Runnable() {
-        @Override
-        public void run() {
-          delegate.handleSuccess(assertion);
-        }
-      });
-      return;
-    }
-
-    public void handleError(FxAccountLoginException e) {
-      invokeHandleHardFailure(delegate, e);
-    }
-  }
-
-  public class EnsureSessionTokenStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) throws Exception {
-      byte[] emailUTF8 = fxAccount.getEmailUTF8();
-      if (emailUTF8 == null) {
-        throw new IllegalStateException("emailUTF8 must not be null");
-      }
-      byte[] quickStretchedPW = fxAccount.getQuickStretchedPW();
-      if (quickStretchedPW == null) {
-        throw new IllegalStateException("quickStretchedPW must not be null");
-      }
-
-      delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate<FxAccountClient20.LoginResponse>() {
-        @Override
-        public void handleError(Exception e) {
-          delegate.handleError(new FxAccountLoginException(e));
-        }
-
-        @Override
-        public void handleFailure(int status, HttpResponse response) {
-          if (skewHandler != null) {
-            skewHandler.updateSkew(response, now());
-          }
-
-          if (status != 401) {
-            delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
-            return;
-          }
-          // We just got denied for a sessionToken. That's a problem with
-          // our email or password. Only thing to do is mark the account
-          // invalid and ask for user intervention.
-          fxAccount.setInvalid();
-          delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected email/password while fetching sessionToken."));
-        }
-
-        @Override
-        public void handleSuccess(LoginResponse result) {
-          fxAccount.setSessionToken(result.sessionToken);
-          fxAccount.setKeyFetchToken(result.keyFetchToken);
-          if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-            FxAccountConstants.pii(LOG_TAG, "Fetched sessionToken : " + Utils.byte2Hex(result.sessionToken));
-            FxAccountConstants.pii(LOG_TAG, "Fetched keyFetchToken: " + Utils.byte2Hex(result.keyFetchToken));
-          }
-          delegate.handleStageSuccess();
-        }
-      });
-    }
-  }
-
-  /**
-   * Now that we have a server to talk to and a session token, we can use them
-   * to check that the account is verified.
-   */
-  public class EnsureVerificationStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) {
-      byte[] sessionToken = fxAccount.getSessionToken();
-      if (sessionToken == null) {
-        throw new IllegalArgumentException("sessionToken must not be null");
-      }
-
-      delegate.client.status(sessionToken, new RequestDelegate<StatusResponse>() {
-        @Override
-        public void handleError(Exception e) {
-          delegate.handleError(new FxAccountLoginException(e));
-        }
-
-        @Override
-        public void handleFailure(int status, HttpResponse response) {
-          if (skewHandler != null) {
-            skewHandler.updateSkew(response, now());
-          }
-
-          if (status != 401) {
-            delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
-            return;
-          }
-          // We just got denied due to our sessionToken.  Invalidate it.
-          fxAccount.setSessionToken(null);
-          delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status."));
-        }
-
-        @Override
-        public void handleSuccess(StatusResponse result) {
-          // We're not yet verified.  We can't go forward yet.
-          if (!result.verified) {
-            delegate.handleError(new FxAccountLoginAccountNotVerifiedException("Account is not yet verified."));
-            return;
-          }
-          // We've transitioned to verified state.  Make a note of it, and continue past go.
-          fxAccount.setVerified();
-          delegate.handleStageSuccess();
-        }
-      });
-    }
-  }
-
-  public static int[] DUMMY = null;
-
-  public class EnsureKeyFetchTokenStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) throws Exception {
-      byte[] emailUTF8 = fxAccount.getEmailUTF8();
-      if (emailUTF8 == null) {
-        throw new IllegalStateException("emailUTF8 must not be null");
-      }
-      byte[] quickStretchedPW = fxAccount.getQuickStretchedPW();
-      if (quickStretchedPW == null) {
-        throw new IllegalStateException("quickStretchedPW must not be null");
-      }
-
-      boolean verified = fxAccount.isVerified();
-      if (!verified) {
-        throw new IllegalStateException("must be verified");
-      }
-
-      // We might already have a valid keyFetchToken. If so, try it. If it's not
-      // valid, we'll invalidate it in EnsureKeysStage.
-      if (fxAccount.getKeyFetchToken() != null) {
-        Logger.info(LOG_TAG, "Using existing keyFetchToken.");
-        delegate.handleStageSuccess();
-        return;
-      }
-
-      delegate.client.loginAndGetKeys(emailUTF8, quickStretchedPW, new RequestDelegate<FxAccountClient20.LoginResponse>() {
-        @Override
-        public void handleError(Exception e) {
-          delegate.handleError(new FxAccountLoginException(e));
-        }
-
-        @Override
-        public void handleFailure(int status, HttpResponse response) {
-          if (skewHandler != null) {
-            skewHandler.updateSkew(response, now());
-          }
-
-          if (status != 401) {
-            delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
-            return;
-          }
-          // We just got denied for a keyFetchToken. That's a problem with
-          // our email or password. Only thing to do is mark the account
-          // invalid and ask for user intervention.
-          fxAccount.setInvalid();
-          delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected email/password while fetching keyFetchToken."));
-        }
-
-        @Override
-        public void handleSuccess(LoginResponse result) {
-          fxAccount.setKeyFetchToken(result.keyFetchToken);
-          if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-            FxAccountConstants.pii(LOG_TAG, "Fetched keyFetchToken: " + Utils.byte2Hex(result.keyFetchToken));
-          }
-          delegate.handleStageSuccess();
-        }
-      });
-    }
-  }
-
-  /**
-   * Now we have a verified account, we can make sure that our local keys are
-   * consistent with the account's keys.
-   */
-  public class EnsureKeysStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) throws Exception {
-      byte[] keyFetchToken = fxAccount.getKeyFetchToken();
-      if (keyFetchToken == null) {
-        throw new IllegalStateException("keyFetchToken must not be null");
-      }
-
-      // Make sure we don't use a keyFetchToken twice. This conveniently
-      // invalidates any invalid keyFetchToken we might try, too.
-      fxAccount.setKeyFetchToken(null);
-
-      delegate.client.keys(keyFetchToken, new RequestDelegate<FxAccountClient10.TwoKeys>() {
-        @Override
-        public void handleError(Exception e) {
-          delegate.handleError(new FxAccountLoginException(e));
-        }
-
-        @Override
-        public void handleFailure(int status, HttpResponse response) {
-          if (skewHandler != null) {
-            skewHandler.updateSkew(response, now());
-          }
-
-          if (status != 401) {
-            delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
-            return;
-          }
-          delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected key token while fetching keys."));
-        }
-
-        @Override
-        public void handleSuccess(TwoKeys result) {
-          fxAccount.setKa(result.kA);
-          fxAccount.setWrappedKb(result.wrapkB);
-          if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-            FxAccountConstants.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
-            FxAccountConstants.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
-            FxAccountConstants.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(fxAccount.getKb()));
-          }
-          delegate.handleStageSuccess();
-        }
-      });
-    }
-  }
-
-  public class EnsureCertificateStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) throws Exception{
-      byte[] sessionToken = fxAccount.getSessionToken();
-      if (sessionToken == null) {
-        throw new IllegalStateException("keyPair must not be null");
-      }
-      BrowserIDKeyPair keyPair = fxAccount.getAssertionKeyPair();
-      if (keyPair == null) {
-        // If we can't fetch a keypair, we probably have some crypto
-        // configuration error on device, which we are never going to recover
-        // from. Mark the account invalid.
-        fxAccount.setInvalid();
-        throw new IllegalStateException("keyPair must not be null");
-      }
-      final VerifyingPublicKey publicKey = keyPair.getPublic();
-
-      delegate.client.sign(sessionToken, publicKey.toJSONObject(), getCertificateDurationInMilliseconds(), new RequestDelegate<String>() {
-        @Override
-        public void handleError(Exception e) {
-          delegate.handleError(new FxAccountLoginException(e));
-        }
-
-        @Override
-        public void handleFailure(int status, HttpResponse response) {
-          if (skewHandler != null) {
-            skewHandler.updateSkew(response, now());
-          }
-
-          if (status != 401) {
-            delegate.handleError(new FxAccountLoginException(new HTTPFailureException(new SyncStorageResponse(response))));
-            return;
-          }
-          // Our sessionToken was just rejected; we should get a new
-          // sessionToken. TODO: Make sure the exception below is fine
-          // enough grained.
-          // Since this is the place we'll see the majority of lifecylcle
-          // auth problems, we should be much more aggressive bumping the
-          // state machine out of this state when we don't get success.
-          fxAccount.setSessionToken(null);
-          delegate.handleError(new FxAccountLoginBadPasswordException("Auth server rejected session token while fetching status."));
-        }
-
-        @Override
-        public void handleSuccess(String certificate) {
-          fxAccount.setCertificate(certificate);
-          if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-            FxAccountConstants.pii(LOG_TAG, "Fetched certificate: " + certificate);
-            JSONWebTokenUtils.dumpCertificate(certificate);
-          }
-          delegate.handleStageSuccess();
-        }
-      });
-    }
-  }
-
-  public class EnsureAssertionStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) throws Exception {
-      BrowserIDKeyPair keyPair = fxAccount.getAssertionKeyPair();
-      if (keyPair == null) {
-        throw new IllegalStateException("keyPair must not be null");
-      }
-      String certificate = fxAccount.getCertificate();
-      if (certificate == null) {
-        throw new IllegalStateException("certificate must not be null");
-      }
-      String assertion;
-      try {
-        long now = System.currentTimeMillis();
-        assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, delegate.audience,
-            JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, now, getAssertionDurationInMilliseconds());
-      } catch (Exception e) {
-        // If we can't sign an assertion, we probably have some crypto
-        // configuration error on device, which we are never going to recover
-        // from. Mark the account invalid before raising the exception.
-        fxAccount.setInvalid();
-        throw e;
-      }
-      fxAccount.setAssertion(assertion);
-      if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-        FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion);
-        JSONWebTokenUtils.dumpAssertion(assertion);
-      }
-      delegate.handleStageSuccess();
-    }
-  }
-
-  public class SuccessStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) throws Exception {
-      String assertion = fxAccount.getAssertion();
-      if (assertion == null) {
-        throw new IllegalStateException("assertion must not be null");
-      }
-      delegate.handleLoginSuccess(assertion);
-    }
-  }
-
-  public class FailStage implements LoginStage {
-    @Override
-    public void execute(final LoginStageDelegate delegate) {
-      AccountState finalState = getAccountState(fxAccount);
-      Logger.info(LOG_TAG, "Failed to login account; final state is " + finalState + ".");
-      delegate.handleError(new FxAccountLoginException("Failed to login."));
-    }
-  }
-}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/BaseRequestDelegate.java
@@ -0,0 +1,49 @@
+/* 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.login;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient10;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountNeedsVerification;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
+
+public abstract class BaseRequestDelegate<T> implements FxAccountClient10.RequestDelegate<T> {
+  protected final ExecuteDelegate delegate;
+  protected final State state;
+
+  public BaseRequestDelegate(State state, ExecuteDelegate delegate) {
+    this.delegate = delegate;
+    this.state = state;
+  }
+
+  @Override
+  public void handleFailure(FxAccountClientRemoteException e) {
+    // Order matters here: we don't want to ignore upgrade required responses
+    // even if the server tells us something else as well. We don't go directly
+    // to the Doghouse on upgrade required; we want the user to try to update
+    // their credentials, and then display UI telling them they need to upgrade.
+    // Then they go to the Doghouse.
+    if (e.isUpgradeRequired()) {
+      delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
+      return;
+    }
+    if (e.isInvalidAuthentication()) {
+      delegate.handleTransition(new RemoteError(e), new Separated(state.email, state.uid, state.verified));
+      return;
+    }
+    if (e.isUnverified()) {
+      delegate.handleTransition(new AccountNeedsVerification(), state);
+      return;
+    }
+    delegate.handleTransition(new RemoteError(e), state);
+  }
+
+  @Override
+  public void handleError(Exception e) {
+    delegate.handleTransition(new LocalError(e), state);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/Cohabiting.java
@@ -0,0 +1,46 @@
+/* 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.login;
+
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class Cohabiting extends TokensAndKeysState {
+  private static final String LOG_TAG = Cohabiting.class.getSimpleName();
+
+  public Cohabiting(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
+    super(StateLabel.Cohabiting, email, uid, sessionToken, kA, kB, keyPair);
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    delegate.getClient().sign(sessionToken, keyPair.getPublic().toJSONObject(), delegate.getCertificateDurationInMilliseconds(),
+        new BaseRequestDelegate<String>(this, delegate) {
+      @Override
+      public void handleSuccess(String certificate) {
+        if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+          try {
+            FxAccountConstants.pii(LOG_TAG, "Fetched certificate: " + certificate);
+            ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
+            if (c != null) {
+              FxAccountConstants.pii(LOG_TAG, "Header   : " + c.getObject("header"));
+              FxAccountConstants.pii(LOG_TAG, "Payload  : " + c.getObject("payload"));
+              FxAccountConstants.pii(LOG_TAG, "Signature: " + c.getString("signature"));
+            } else {
+              FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
+            }
+          } catch (Exception e) {
+            FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
+          }
+        }
+        delegate.handleTransition(new LogMessage("sign succeeded"), new Married(email, uid, sessionToken, kA, kB, keyPair, certificate));
+      }
+    });
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/Doghouse.java
@@ -0,0 +1,25 @@
+/* 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.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+
+
+public class Doghouse extends State {
+  public Doghouse(String email, String uid, boolean verified) {
+    super(StateLabel.Doghouse, email, uid, verified);
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    delegate.handleTransition(new LogMessage("Upgraded Firefox clients might know what to do here."), this);
+  }
+
+  @Override
+  public Action getNeededAction() {
+    return Action.NeedsUpgrade;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/Engaged.java
@@ -0,0 +1,83 @@
+/* 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.login;
+
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.RemoteError;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public class Engaged extends State {
+  private static final String LOG_TAG = Engaged.class.getSimpleName();
+
+  protected final byte[] sessionToken;
+  protected final byte[] keyFetchToken;
+  protected final byte[] unwrapkB;
+
+  public Engaged(String email, String uid, boolean verified, byte[] unwrapkB, byte[] sessionToken, byte[] keyFetchToken) {
+    super(StateLabel.Engaged, email, uid, verified);
+    Utils.throwIfNull(unwrapkB, sessionToken, keyFetchToken);
+    this.unwrapkB = unwrapkB;
+    this.sessionToken = sessionToken;
+    this.keyFetchToken = keyFetchToken;
+  }
+
+  @Override
+  public ExtendedJSONObject toJSONObject() {
+    ExtendedJSONObject o = super.toJSONObject();
+    // Fields are non-null by constructor.
+    o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
+    o.put("sessionToken", Utils.byte2Hex(sessionToken));
+    o.put("keyFetchToken", Utils.byte2Hex(keyFetchToken));
+    return o;
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    BrowserIDKeyPair theKeyPair;
+    try {
+      theKeyPair = delegate.generateKeyPair();
+    } catch (NoSuchAlgorithmException e) {
+      delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
+      return;
+    }
+    final BrowserIDKeyPair keyPair = theKeyPair;
+
+    delegate.getClient().keys(keyFetchToken, new BaseRequestDelegate<TwoKeys>(this, delegate) {
+      @Override
+      public void handleSuccess(TwoKeys result) {
+        byte[] kB;
+        try {
+          kB = FxAccountUtils.unwrapkB(unwrapkB, result.wrapkB);
+          if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+            FxAccountConstants.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
+            FxAccountConstants.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
+            FxAccountConstants.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB));
+          }
+        } catch (Exception e) {
+          delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified));
+          return;
+        }
+        delegate.handleTransition(new LogMessage("keys succeeded"), new Cohabiting(email, uid, sessionToken, result.kA, kB, keyPair));
+      }
+    });
+  }
+
+  @Override
+  public Action getNeededAction() {
+    if (!verified) {
+      return Action.NeedsVerification;
+    }
+    return Action.None;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/FxAccountLoginStateMachine.java
@@ -0,0 +1,84 @@
+/* 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.login;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+
+public class FxAccountLoginStateMachine {
+  public static final String LOG_TAG = FxAccountLoginStateMachine.class.getSimpleName();
+
+  public interface LoginStateMachineDelegate {
+    public FxAccountClient getClient();
+    public long getCertificateDurationInMilliseconds();
+    public long getAssertionDurationInMilliseconds();
+    public void handleTransition(Transition transition, State state);
+    public void handleFinal(State state);
+    public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException;
+  }
+
+  public static class ExecuteDelegate {
+    protected final LoginStateMachineDelegate delegate;
+    protected final StateLabel desiredStateLabel;
+    // It's as difficult to detect arbitrary cycles as repeated states.
+    protected final Set<StateLabel> stateLabelsSeen = new HashSet<StateLabel>();
+
+    protected ExecuteDelegate(StateLabel initialStateLabel, StateLabel desiredStateLabel, LoginStateMachineDelegate delegate) {
+      this.delegate = delegate;
+      this.desiredStateLabel = desiredStateLabel;
+      this.stateLabelsSeen.add(initialStateLabel);
+    }
+
+    public FxAccountClient getClient() {
+      return delegate.getClient();
+    }
+
+    public long getCertificateDurationInMilliseconds() {
+      return delegate.getCertificateDurationInMilliseconds();
+    }
+
+    public long getAssertionDurationInMilliseconds() {
+      return delegate.getAssertionDurationInMilliseconds();
+    }
+
+    public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+      return delegate.generateKeyPair();
+    }
+
+    public void handleTransition(Transition transition, State state) {
+      // Always trigger the transition callback.
+      delegate.handleTransition(transition, state);
+
+      // Possibly trigger the final callback. We trigger if we're at our desired
+      // state, or if we've seen this state before.
+      StateLabel stateLabel = state.getStateLabel();
+      if (stateLabel == desiredStateLabel || stateLabelsSeen.contains(stateLabel)) {
+        delegate.handleFinal(state);
+        return;
+      }
+
+      // If this wasn't the last state, leave a bread crumb and move on to the
+      // next state.
+      stateLabelsSeen.add(stateLabel);
+      state.execute(this);
+    }
+  }
+
+  public void advance(State initialState, final StateLabel desiredStateLabel, final LoginStateMachineDelegate delegate) {
+    if (initialState.getStateLabel() == desiredStateLabel) {
+      // We're already where we want to be!
+      delegate.handleFinal(initialState);
+      return;
+    }
+    ExecuteDelegate executeDelegate = new ExecuteDelegate(initialState.getStateLabel(), desiredStateLabel, delegate);
+    initialState.execute(executeDelegate);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/FxAccountLoginTransition.java
@@ -0,0 +1,62 @@
+/* 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.login;
+
+
+public class FxAccountLoginTransition {
+  public interface Transition {
+  }
+
+  public static class LogMessage implements Transition {
+    public final String detailMessage;
+
+    public LogMessage(String detailMessage) {
+      this.detailMessage = detailMessage;
+    }
+
+    @Override
+    public String toString() {
+      return getClass().getSimpleName() + (this.detailMessage == null ? "" : "('" + this.detailMessage + "')");
+    }
+  }
+
+  public static class AccountNeedsVerification extends LogMessage {
+    public AccountNeedsVerification() {
+      super(null);
+    }
+  }
+
+  public static class PasswordRequired extends LogMessage {
+    public PasswordRequired() {
+      super(null);
+    }
+  }
+
+  public static class LocalError implements Transition {
+    public final Exception e;
+
+    public LocalError(Exception e) {
+      this.e = e;
+    }
+
+    @Override
+    public String toString() {
+      return "Log(" + this.e + ")";
+    }
+  }
+
+  public static class RemoteError implements Transition {
+    public final Exception e;
+
+    public RemoteError(Exception e) {
+      this.e = e;
+    }
+
+    @Override
+    public String toString() {
+      return "Log(" + (this.e == null ? "null" : this.e) + ")";
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/Married.java
@@ -0,0 +1,102 @@
+/* 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.login;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+public class Married extends TokensAndKeysState {
+  private static final String LOG_TAG = Married.class.getSimpleName();
+
+  protected final String certificate;
+
+  public Married(String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair, String certificate) {
+    super(StateLabel.Married, email, uid, sessionToken, kA, kB, keyPair);
+    Utils.throwIfNull(certificate);
+    this.certificate = certificate;
+  }
+
+  @Override
+  public ExtendedJSONObject toJSONObject() {
+    ExtendedJSONObject o = super.toJSONObject();
+    // Fields are non-null by constructor.
+    o.put("certificate", certificate);
+    return o;
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    delegate.handleTransition(new LogMessage("staying married"), this);
+  }
+
+  public String generateAssertion(String audience, String issuer, long issuedAt, long durationInMilliseconds) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException {
+    String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, issuedAt, durationInMilliseconds);
+    if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+      return assertion;
+    }
+
+    try {
+      FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion);
+      ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion);
+      if (a != null) {
+        FxAccountConstants.pii(LOG_TAG, "aHeader   : " + a.getObject("header"));
+        FxAccountConstants.pii(LOG_TAG, "aPayload  : " + a.getObject("payload"));
+        FxAccountConstants.pii(LOG_TAG, "aSignature: " + a.getString("signature"));
+        String certificate = a.getString("certificate");
+        if (certificate != null) {
+          ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
+          FxAccountConstants.pii(LOG_TAG, "cHeader   : " + c.getObject("header"));
+          FxAccountConstants.pii(LOG_TAG, "cPayload  : " + c.getObject("payload"));
+          FxAccountConstants.pii(LOG_TAG, "cSignature: " + c.getString("signature"));
+          // Print the relevant timestamps in sorted order with labels.
+          HashMap<Long, String> map = new HashMap<Long, String>();
+          map.put(a.getObject("payload").getLong("iat"), "aiat");
+          map.put(a.getObject("payload").getLong("exp"), "aexp");
+          map.put(c.getObject("payload").getLong("iat"), "ciat");
+          map.put(c.getObject("payload").getLong("exp"), "cexp");
+          ArrayList<Long> values = new ArrayList<Long>(map.keySet());
+          Collections.sort(values);
+          for (Long value : values) {
+            FxAccountConstants.pii(LOG_TAG, map.get(value) + ": " + value);
+          }
+        } else {
+          FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
+        }
+      } else {
+        FxAccountConstants.pii(LOG_TAG, "Could not parse assertion!");
+      }
+    } catch (Exception e) {
+      FxAccountConstants.pii(LOG_TAG, "Got exception dumping assertion debug info.");
+    }
+    return assertion;
+  }
+
+  public KeyBundle getSyncKeyBundle() throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+    // TODO Document this choice for deriving from kB.
+    return FxAccountUtils.generateSyncKeyBundle(kB);
+  }
+
+  public State makeCohabitingState() {
+    return new Cohabiting(email, uid, sessionToken, kA, kB, keyPair);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/Promised.java
@@ -0,0 +1,59 @@
+/* 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.login;
+
+import java.io.UnsupportedEncodingException;
+
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public class Promised extends State {
+  protected final byte[] quickStretchedPW;
+  protected final byte[] unwrapkB;
+
+  public Promised(String email, String uid, boolean verified, byte[] unwrapkB, byte[] quickStretchedPW) {
+    super(StateLabel.Promised, email, uid, verified);
+    Utils.throwIfNull(email, uid, unwrapkB, quickStretchedPW);
+    this.unwrapkB = unwrapkB;
+    this.quickStretchedPW = quickStretchedPW;
+  }
+
+  @Override
+  public ExtendedJSONObject toJSONObject() {
+    ExtendedJSONObject o = super.toJSONObject();
+    // Fields are non-null by constructor.
+    o.put("unwrapkB", Utils.byte2Hex(unwrapkB));
+    o.put("quickStretchedPW", Utils.byte2Hex(quickStretchedPW));
+    return o;
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    byte[] emailUTF8;
+    try {
+      emailUTF8 = email.getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      delegate.handleTransition(new LocalError(e), new Doghouse(email, uid, verified));
+      return;
+    }
+
+    delegate.getClient().loginAndGetKeys(emailUTF8, quickStretchedPW, new BaseRequestDelegate<FxAccountClient20.LoginResponse>(this, delegate) {
+      @Override
+      public void handleSuccess(LoginResponse result) {
+        delegate.handleTransition(new LogMessage("loginAndGetKeys succeeded"), new Engaged(email, uid, verified, unwrapkB, result.sessionToken, result.keyFetchToken));
+      }
+    });
+  }
+
+  @Override
+  public Action getNeededAction() {
+    return Action.NeedsVerification;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/Separated.java
@@ -0,0 +1,25 @@
+/* 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.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
+
+
+public class Separated extends State {
+  public Separated(String email, String uid, boolean verified) {
+    super(StateLabel.Separated, email, uid, verified);
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    delegate.handleTransition(new PasswordRequired(), this);
+  }
+
+  @Override
+  public Action getNeededAction() {
+    return Action.NeedsPassword;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/State.java
@@ -0,0 +1,71 @@
+/* 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.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public abstract class State {
+  public static final long CURRENT_VERSION = 1L;
+
+  public enum StateLabel {
+    Promised,
+    Engaged,
+    Cohabiting,
+    Married,
+    Separated,
+    Doghouse,
+  }
+
+  public enum Action {
+    NeedsUpgrade,
+    NeedsPassword,
+    NeedsVerification,
+    None,
+  }
+
+  protected final StateLabel stateLabel;
+  public final String email;
+  public final String uid;
+  public final boolean verified;
+
+  public State(StateLabel stateLabel, String email, String uid, boolean verified) {
+    Utils.throwIfNull(email, uid);
+    this.stateLabel = stateLabel;
+    this.email = email;
+    this.uid = uid;
+    this.verified = verified;
+  }
+
+  public StateLabel getStateLabel() {
+    return this.stateLabel;
+  }
+
+  public boolean isVerified() {
+    return this.verified;
+  }
+
+  public ExtendedJSONObject toJSONObject() {
+    ExtendedJSONObject o = new ExtendedJSONObject();
+    o.put("version", State.CURRENT_VERSION);
+    o.put("email", email);
+    o.put("uid", uid);
+    o.put("verified", verified);
+    return o;
+  }
+
+  public State makeSeparatedState() {
+    return new Separated(email, uid, verified);
+  }
+
+  public State makeDoghouseState() {
+    return new Doghouse(email, uid, verified);
+  }
+
+  public abstract void execute(ExecuteDelegate delegate);
+
+  public abstract Action getNeededAction();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/StateFactory.java
@@ -0,0 +1,69 @@
+/* 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.login;
+
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+
+public class StateFactory {
+  public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+    Long version = o.getLong("version");
+    if (version == null || version.intValue() != 1) {
+      throw new IllegalStateException("version must be 1");
+    }
+    switch (stateLabel) {
+    case Promised:
+      return new Promised(
+          o.getString("email"),
+          o.getString("uid"),
+          o.getBoolean("verified"),
+          Utils.hex2Byte(o.getString("unwrapkB")),
+          Utils.hex2Byte(o.getString("quickStretchedPW")));
+    case Engaged:
+      return new Engaged(
+          o.getString("email"),
+          o.getString("uid"),
+          o.getBoolean("verified"),
+          Utils.hex2Byte(o.getString("unwrapkB")),
+          Utils.hex2Byte(o.getString("sessionToken")),
+          Utils.hex2Byte(o.getString("keyFetchToken")));
+    case Cohabiting:
+      return new Cohabiting(
+          o.getString("email"),
+          o.getString("uid"),
+          Utils.hex2Byte(o.getString("sessionToken")),
+          Utils.hex2Byte(o.getString("kA")),
+          Utils.hex2Byte(o.getString("kB")),
+          RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")));
+    case Married:
+      return new Married(
+          o.getString("email"),
+          o.getString("uid"),
+          Utils.hex2Byte(o.getString("sessionToken")),
+          Utils.hex2Byte(o.getString("kA")),
+          Utils.hex2Byte(o.getString("kB")),
+          RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")),
+          o.getString("certificate"));
+    case Separated:
+      return new Separated(
+          o.getString("email"),
+          o.getString("uid"),
+          o.getBoolean("verified"));
+    case Doghouse:
+      return new Doghouse(
+          o.getString("email"),
+          o.getString("uid"),
+          o.getBoolean("verified"));
+    default:
+      throw new IllegalStateException("unrecognized state label: " + stateLabel);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/TokensAndKeysState.java
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.login;
+
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public abstract class TokensAndKeysState extends State {
+  protected final byte[] sessionToken;
+  protected final byte[] kA;
+  protected final byte[] kB;
+  protected final BrowserIDKeyPair keyPair;
+
+  public TokensAndKeysState(StateLabel stateLabel, String email, String uid, byte[] sessionToken, byte[] kA, byte[] kB, BrowserIDKeyPair keyPair) {
+    super(stateLabel, email, uid, true);
+    Utils.throwIfNull(sessionToken, kA, kB, keyPair);
+    this.sessionToken = sessionToken;
+    this.kA = kA;
+    this.kB = kB;
+    this.keyPair = keyPair;
+  }
+
+  @Override
+  public ExtendedJSONObject toJSONObject() {
+    ExtendedJSONObject o = super.toJSONObject();
+    // Fields are non-null by constructor.
+    o.put("sessionToken", Utils.byte2Hex(sessionToken));
+    o.put("kA", Utils.byte2Hex(kA));
+    o.put("kB", Utils.byte2Hex(kB));
+    o.put("keyPair", keyPair.toJSONObject());
+    return o;
+  }
+
+  @Override
+  public Action getNeededAction() {
+    return Action.None;
+  }
+}
--- a/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
+++ b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
@@ -9,39 +9,35 @@ 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.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
-import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 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;
 import android.os.Bundle;
 
 public class FxAccountGlobalSession extends GlobalSession {
   private static final String LOG_TAG = FxAccountGlobalSession.class.getSimpleName();
 
-  public FxAccountGlobalSession(String storageEndpoint, String username,
-      AuthHeaderProvider authHeaderProvider, String prefsPath,
-      KeyBundle syncKeyBundle, BaseGlobalSessionCallback callback,
+  public FxAccountGlobalSession(String storageEndpoint, SyncConfiguration config, BaseGlobalSessionCallback callback,
       Context context, Bundle extras, ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
       ParseException, NonObjectJSONException, URISyntaxException {
-    super(username, authHeaderProvider, prefsPath, syncKeyBundle,
-        callback, context, extras, clientsDelegate, null);
+    super(config, callback, context, extras, clientsDelegate, null);
     URI uri = new URI(storageEndpoint);
     this.config.clusterURL = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), "/", null, null);
     FxAccountConstants.pii(LOG_TAG, "storageEndpoint is " + uri + " and clusterURL is " + config.clusterURL);
   }
 
   @Override
   public void prepareStages() {
     super.prepareStages();
--- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -1,33 +1,42 @@
 /* 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.net.URI;
+import java.security.NoSuchAlgorithmException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
 import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
+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.RSACryptoImplementation;
 import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
 import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
-import org.mozilla.gecko.fxa.authenticator.FxAccountLoginDelegate;
-import org.mozilla.gecko.fxa.authenticator.FxAccountLoginException;
-import org.mozilla.gecko.fxa.authenticator.FxAccountLoginPolicy;
+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.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 import org.mozilla.gecko.tokenserver.TokenServerClient;
@@ -55,26 +64,31 @@ public class FxAccountSyncAdapter extend
 
   /**
    * A trivial global session callback that ignores backoff requests, upgrades,
    * and authorization errors. It simply waits until the sync completes.
    */
   protected static class SessionCallback implements BaseGlobalSessionCallback {
     protected final CountDownLatch latch;
     protected final SyncResult syncResult;
+    protected final AndroidFxAccount fxAccount;
 
-    public SessionCallback(CountDownLatch latch, SyncResult syncResult) {
+    public SessionCallback(CountDownLatch latch, SyncResult syncResult, AndroidFxAccount fxAccount) {
       if (latch == null) {
         throw new IllegalArgumentException("latch must not be null");
       }
       if (syncResult == null) {
         throw new IllegalArgumentException("syncResult must not be null");
       }
+      if (fxAccount == null) {
+        throw new IllegalArgumentException("fxAccount must not be null");
+      }
       this.latch = latch;
       this.syncResult = syncResult;
+      this.fxAccount = fxAccount;
     }
 
     @Override
     public boolean shouldBackOff() {
       return false;
     }
 
     @Override
@@ -121,136 +135,180 @@ public class FxAccountSyncAdapter extend
     public void handleSuccess(GlobalSession globalSession) {
       setSyncResultSuccess();
       Logger.info(LOG_TAG, "Sync succeeded.");
       latch.countDown();
     }
 
     @Override
     public void handleError(GlobalSession globalSession, Exception e) {
+      // This is awful, but we need to propagate bad assertions back up the
+      // chain somehow, and this will do for now.
+      if (e instanceof TokenServerException) {
+        // We should only get here *after* we're locked into the married state.
+        State state = fxAccount.getState();
+        if (state.getStateLabel() == StateLabel.Married) {
+          Married married = (Married) state;
+          fxAccount.setState(married.makeCohabitingState());
+        }
+      }
       setSyncResultSoftError();
       Logger.warn(LOG_TAG, "Sync failed.", e);
       latch.countDown();
     }
 
     @Override
     public void handleAborted(GlobalSession globalSession, String reason) {
       setSyncResultSoftError();
       Logger.warn(LOG_TAG, "Sync aborted: " + reason);
       latch.countDown();
     }
   };
 
+  protected void syncWithAssertion(final String audience, final String assertion, URI tokenServerEndpointURI, final String prefsPath, final SharedPreferences sharedPrefs, final KeyBundle syncKeyBundle, final BaseGlobalSessionCallback callback) {
+    TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
+    tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
+      @Override
+      public void handleSuccess(final TokenServerToken token) {
+        FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
+
+        FxAccountGlobalSession globalSession = null;
+        try {
+          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 tokenServerSkewHandler = SkewHandler.getSkewHandlerFromEndpointString(token.endpoint);
+          final long tokenServerSkew = tokenServerSkewHandler.getSkewInSeconds();
+          AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, tokenServerSkew);
+
+          final Context context = getContext();
+          SyncConfiguration syncConfig = new SyncConfiguration(token.uid, authHeaderProvider, sharedPrefs, syncKeyBundle);
+
+          // EXTRAS
+          final Bundle extras = Bundle.EMPTY;
+          globalSession = new FxAccountGlobalSession(token.endpoint, syncConfig, callback, context, extras, 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);
+      }
+    });
+  }
+
   /**
    * A trivial Sync implementation that does not cache client keys,
    * certificates, or tokens.
    *
    * This should be replaced with a full {@link FxAccountAuthenticator}-based
    * token implementation.
    */
   @Override
   public void onPerformSync(final Account account, final Bundle extras, final String authority, ContentProviderClient provider, SyncResult syncResult) {
     Logger.setThreadLogTag(FxAccountConstants.GLOBAL_LOG_TAG);
+    Logger.resetLogging();
 
     Logger.info(LOG_TAG, "Syncing FxAccount" +
         " account named " + account.name +
         " for authority " + authority +
         " with instance " + this + ".");
 
+    final Context context = getContext();
+    final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
+    if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
+      fxAccount.dump();
+    }
     final CountDownLatch latch = new CountDownLatch(1);
-    final BaseGlobalSessionCallback callback = new SessionCallback(latch, syncResult);
+    final BaseGlobalSessionCallback callback = new SessionCallback(latch, syncResult, fxAccount);
 
     try {
-      final Context context = getContext();
-      final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
-
-      if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
-        fxAccount.dump();
+      State state;
+      try {
+        state = fxAccount.getState();
+      } catch (Exception e) {
+        callback.handleError(null, e);
+        return;
       }
 
       final String prefsPath = fxAccount.getSyncPrefsPath();
 
       // This will be the same chunk of SharedPreferences that GlobalSession/SyncConfiguration will later create.
       final SharedPreferences sharedPrefs = context.getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE);
 
       final String audience = fxAccount.getAudience();
+      final String authServerEndpoint = fxAccount.getAccountServerURI();
       final String tokenServerEndpoint = fxAccount.getTokenServerURI();
       final URI tokenServerEndpointURI = new URI(tokenServerEndpoint);
 
       // TODO: why doesn't the loginPolicy extract the audience from the account?
-      final FxAccountLoginPolicy loginPolicy = new FxAccountLoginPolicy(context, fxAccount, executor);
-      loginPolicy.certificateDurationInMilliseconds = 20 * 60 * 1000;
-      loginPolicy.assertionDurationInMilliseconds = 15 * 60 * 1000;
-      Logger.info(LOG_TAG, "Asking for certificates to expire after 20 minutes and assertions to expire after 15 minutes.");
-
-      loginPolicy.login(audience, new FxAccountLoginDelegate() {
+      final FxAccountClient client = new FxAccountClient20(authServerEndpoint, executor);
+      final FxAccountLoginStateMachine stateMachine = new FxAccountLoginStateMachine();
+      stateMachine.advance(state, StateLabel.Married, new LoginStateMachineDelegate() {
         @Override
-        public void handleSuccess(final String assertion) {
-          TokenServerClient tokenServerclient = new TokenServerClient(tokenServerEndpointURI, executor);
-          tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new TokenServerClientDelegate() {
-            @Override
-            public void handleSuccess(final TokenServerToken token) {
-              FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
-              sharedPrefs.edit().putLong("tokenFailures", 0).commit();
-
-              FxAccountGlobalSession globalSession = null;
-              try {
-                ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
-
-                // TODO Document this choice for deriving from kB.
-                final KeyBundle syncKeyBundle = FxAccountUtils.generateSyncKeyBundle(fxAccount.getKb());
-
-                // 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 tokenServerSkewHandler = SkewHandler.getSkewHandlerFromEndpointString(token.endpoint);
-                final long tokenServerSkew = tokenServerSkewHandler.getSkewInSeconds();
-                AuthHeaderProvider authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false, tokenServerSkew);
+        public FxAccountClient getClient() {
+          return client;
+        }
 
-                globalSession = new FxAccountGlobalSession(token.endpoint, token.uid, authHeaderProvider, prefsPath, syncKeyBundle, callback, context, extras, clientsDataDelegate);
-                globalSession.start();
-              } catch (Exception e) {
-                callback.handleError(globalSession, e);
-                return;
-              }
-            }
+        @Override
+        public long getCertificateDurationInMilliseconds() {
+          return 60 * 60 * 1000;
+        }
 
-            @Override
-            public void handleFailure(TokenServerException e) {
-              // This is tricky since the token server fairly
-              // consistently rejects a token the first time it sees it
-              // before accepting it for the rest of its lifetime.
-              long MAX_TOKEN_FAILURES_PER_TOKEN = 2;
-              long tokenFailures = 1 + sharedPrefs.getLong("tokenFailures", 0);
-              if (tokenFailures > MAX_TOKEN_FAILURES_PER_TOKEN) {
-                fxAccount.setCertificate(null);
-                tokenFailures = 0;
-                Logger.warn(LOG_TAG, "Seen too many failures with this token; resetting: " + tokenFailures);
-                Logger.warn(LOG_TAG, "To aid debugging, synchronously sending assertion to remote verifier for second look.");
-                debugAssertion(tokenServerEndpoint, assertion);
-              } else {
-                Logger.info(LOG_TAG, "Seen " + tokenFailures + " failures with this token so far.");
-              }
-              sharedPrefs.edit().putLong("tokenFailures", tokenFailures).commit();
-              handleError(e);
-            }
+        @Override
+        public long getAssertionDurationInMilliseconds() {
+          return 15 * 60 * 1000;
+        }
 
-            @Override
-            public void handleError(Exception e) {
-              Logger.error(LOG_TAG, "Failed to get token.", e);
-              callback.handleError(null, e);
-            }
-          });
+        @Override
+        public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
+          return RSACryptoImplementation.generateKeyPair(1024);
         }
 
         @Override
-        public void handleError(FxAccountLoginException e) {
-          Logger.error(LOG_TAG, "Got error logging in.", e);
-          callback.handleError(null, e);
+        public void handleTransition(Transition transition, State state) {
+          Logger.warn(LOG_TAG, "handleTransition: " + transition + " to " + state);
+        }
+
+        @Override
+        public void handleFinal(State state) {
+          Logger.warn(LOG_TAG, "handleFinal: in " + state);
+          fxAccount.setState(state);
+
+          try {
+            if (state.getStateLabel() != StateLabel.Married) {
+              callback.handleError(null, new RuntimeException("Cannot sync from state: " + state));
+              return;
+            }
+
+            Married married = (Married) state;
+            final long now = System.currentTimeMillis();
+            SkewHandler skewHandler = SkewHandler.getSkewHandlerFromEndpointString(tokenServerEndpoint);
+            String assertion = married.generateAssertion(audience, JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER,
+                now + skewHandler.getSkewInMillis(),
+                this.getAssertionDurationInMilliseconds());
+            syncWithAssertion(audience, assertion, tokenServerEndpointURI, prefsPath, sharedPrefs, married.getSyncKeyBundle(), callback);
+          } catch (Exception e) {
+            callback.handleError(null, e);
+            return;
+          }
         }
       });
 
       latch.await();
     } catch (Exception e) {
       Logger.error(LOG_TAG, "Got error syncing.", e);
       callback.handleError(null, e);
     }
--- a/mobile/android/base/resources/layout/fxaccount_create_account.xml
+++ b/mobile/android/base/resources/layout/fxaccount_create_account.xml
@@ -44,25 +44,30 @@
             style="@style/FxAccountEditItem"
             android:layout_marginTop="15dp"
             android:drawableRight="@drawable/fxaccount_ddarrow_inactive"
             android:focusable="false"
             android:hint="@string/fxaccount_year_of_birth"
             android:inputType="none" />
 
         <TextView
-            android:id="@+id/local_error"
+            android:id="@+id/remote_error"
             style="@style/FxAccountErrorItem" />
 
-        <Button
-            android:id="@+id/create_account_button"
-            style="@style/FxAccountButton"
-            android:layout_marginBottom="20dp"
-            android:layout_marginTop="45dp"
-            android:text="@string/fxaccount_create_account_button_label" />
+        <FrameLayout style="@style/FxAccountButtonLayout" >
+
+            <ProgressBar
+                android:id="@+id/progress"
+                style="@style/FxAccountProgress" />
+
+            <Button
+                android:id="@+id/button"
+                style="@style/FxAccountButton"
+                android:text="@string/fxaccount_create_account_button_label" />
+        </FrameLayout>
 
         <TextView
             android:id="@+id/sign_in_instead_link"
             style="@style/FxAccountLinkItem"
             android:layout_marginBottom="20dp"
             android:focusable="true"
             android:text="@string/fxaccount_sign_in_instead" />
 
@@ -73,9 +78,9 @@
 
         <LinearLayout style="@style/FxAccountSpacer" />
 
         <ImageView
             style="@style/FxAccountIcon"
             android:contentDescription="@string/fxaccount_icon_contentDescription" />
     </LinearLayout>
 
-</ScrollView>
+</ScrollView>
\ No newline at end of file
--- a/mobile/android/base/resources/layout/fxaccount_email_password_view.xml
+++ b/mobile/android/base/resources/layout/fxaccount_email_password_view.xml
@@ -7,20 +7,16 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android" >
 
     <LinearLayout
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:orientation="vertical" >
 
-        <TextView
-            android:id="@+id/local_error"
-            style="@style/FxAccountErrorItem" />
-
         <EditText
             android:id="@+id/email"
             style="@style/FxAccountEditItem"
             android:layout_marginBottom="15dp"
             android:ems="10"
             android:hint="@string/fxaccount_email_hint"
             android:inputType="textEmailAddress" >
 
--- a/mobile/android/base/resources/layout/fxaccount_sign_in.xml
+++ b/mobile/android/base/resources/layout/fxaccount_sign_in.xml
@@ -21,22 +21,31 @@
             android:text="@string/firefox_accounts" />
 
         <TextView
             style="@style/FxAccountSubHeaderItem"
             android:text="@string/fxaccount_sign_in" />
 
         <include layout="@layout/fxaccount_email_password_view" />
 
-        <Button
-            android:id="@+id/sign_in_button"
-            style="@style/FxAccountButton"
-            android:layout_marginBottom="20dp"
-            android:layout_marginTop="45dp"
-            android:text="@string/fxaccount_sign_in_button_label" />
+        <TextView
+            android:id="@+id/remote_error"
+            style="@style/FxAccountErrorItem" />
+
+        <FrameLayout style="@style/FxAccountButtonLayout" >
+
+            <ProgressBar
+                android:id="@+id/progress"
+                style="@style/FxAccountProgress" />
+
+            <Button
+                android:id="@+id/button"
+                style="@style/FxAccountButton"
+                android:text="@string/fxaccount_sign_in_button_label" />
+        </FrameLayout>
 
         <LinearLayout
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:orientation="horizontal"
             android:paddingLeft="10dp"
             android:paddingRight="10dp" >
 
@@ -59,9 +68,9 @@
 
         <LinearLayout style="@style/FxAccountSpacer" />
 
         <ImageView
             style="@style/FxAccountIcon"
             android:contentDescription="@string/fxaccount_icon_contentDescription" />
     </LinearLayout>
 
-</ScrollView>
+</ScrollView>
\ No newline at end of file
--- a/mobile/android/base/resources/layout/fxaccount_status.xml
+++ b/mobile/android/base/resources/layout/fxaccount_status.xml
@@ -28,17 +28,17 @@
             android:id="@+id/change_password"
             style="@style/FxAccountLinkItem"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="10dp"
             android:text="@string/fxaccount_change_password" >
         </TextView>
 
-        <FrameLayout
+        <ViewFlipper
             android:id="@+id/connection_status_view"
             style="@style/FxAccountTextItem"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="10dp" >
 
             <TextView
                 android:id="@+id/unverified_view"
@@ -47,52 +47,64 @@
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
                 android:layout_marginBottom="10dp"
                 android:background="#fad4d2"
                 android:drawablePadding="10dp"
                 android:drawableStart="@drawable/fxaccount_sync_error"
                 android:gravity="center_vertical"
                 android:padding="10dp"
-                android:text="@string/fxaccount_status_needs_verification"
-                android:visibility="gone" >
+                android:text="@string/fxaccount_status_needs_upgrade" >
             </TextView>
 
             <TextView
                 android:id="@+id/sign_in_view"
                 style="@style/FxAccountTextItem"
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
                 android:layout_marginBottom="10dp"
                 android:background="#fad4d2"
                 android:drawablePadding="10dp"
                 android:drawableStart="@drawable/fxaccount_sync_error"
                 android:gravity="center_vertical"
                 android:padding="10dp"
-                android:text="@string/fxaccount_status_needs_credentials"
-                android:visibility="gone" >
+                android:text="@string/fxaccount_status_needs_credentials" >
+            </TextView>
+
+            <TextView
+                android:id="@+id/unverified_view"
+                style="@style/FxAccountTextItem"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_horizontal"
+                android:layout_marginBottom="10dp"
+                android:background="#fad4d2"
+                android:drawablePadding="10dp"
+                android:drawableStart="@drawable/fxaccount_sync_error"
+                android:gravity="center_vertical"
+                android:padding="10dp"
+                android:text="@string/fxaccount_status_needs_verification" >
             </TextView>
 
             <TextView
                 android:id="@+id/syncing_view"
                 style="@style/FxAccountTextItem"
                 android:layout_width="fill_parent"
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
                 android:layout_marginBottom="10dp"
                 android:background="#d1e7fe"
                 android:drawablePadding="10dp"
                 android:drawableStart="@drawable/fxaccount_sync_icon"
                 android:gravity="center_vertical"
                 android:padding="10dp"
-                android:text="@string/fxaccount_status_syncing"
-                android:visibility="visible" >
+                android:text="@string/fxaccount_status_syncing" >
             </TextView>
-        </FrameLayout>
+        </ViewFlipper>
 
         <TextView
             style="@style/FxAccountHeaderItem"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="10dp"
             android:text="@string/fxaccount_status_sync" >
         </TextView>
@@ -123,37 +135,49 @@
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:layout_marginBottom="10dp"
             android:text="@string/fxaccount_status_tabs" />
 
         <LinearLayout
             android:id="@+id/debug_buttons"
             style="@style/FxAccountMiddle"
-            android:background="#7f7f7f" >
+            android:visibility="gone" >
 
             <Button
+                android:id="@+id/debug_refresh_button"
                 style="@style/FxAccountButton"
                 android:layout_marginBottom="10dp"
-                android:onClick="onClickRefresh"
-                android:text="Refresh" />
+                android:text="@string/fxaccount_status_debug_refresh_button_label" />
 
             <Button
+                android:id="@+id/debug_dump_button"
+                style="@style/FxAccountButton"
+                android:layout_marginBottom="10dp"
+                android:text="@string/fxaccount_status_debug_dump_button_label" />
+
+            <Button
+                android:id="@+id/debug_sync_button"
                 style="@style/FxAccountButton"
                 android:layout_marginBottom="10dp"
-                android:onClick="onClickDumpAccountDetails"
-                android:text="Dump Account Details" />
+                android:text="@string/fxaccount_status_debug_sync_button_label" />
 
             <Button
+                android:id="@+id/debug_forget_certificate_button"
                 style="@style/FxAccountButton"
                 android:layout_marginBottom="10dp"
-                android:onClick="onClickForgetAccountTokens"
-                android:text="Forget sessionToken and keyFetchToken" />
+                android:text="@string/fxaccount_status_debug_forget_certificate_button_label" />
 
             <Button
+                android:id="@+id/debug_require_password_button"
                 style="@style/FxAccountButton"
                 android:layout_marginBottom="10dp"
-                android:onClick="onClickForgetPassword"
-                android:text="Forget password" />
+                android:text="@string/fxaccount_status_debug_require_password_button_label" />
+
+            <Button
+                android:id="@+id/debug_require_upgrade_button"
+                style="@style/FxAccountButton"
+                android:layout_marginBottom="10dp"
+                android:text="@string/fxaccount_status_debug_require_upgrade_button_label" />
         </LinearLayout>
     </LinearLayout>
 
 </ScrollView>
--- a/mobile/android/base/resources/layout/fxaccount_update_credentials.xml
+++ b/mobile/android/base/resources/layout/fxaccount_update_credentials.xml
@@ -21,28 +21,37 @@
             android:text="@string/firefox_accounts" />
 
         <TextView
             style="@style/FxAccountSubHeaderItem"
             android:text="@string/fxaccount_update_credentials" />
 
         <include layout="@layout/fxaccount_email_password_view" />
 
-        <Button
-            android:id="@+id/button"
-            style="@style/FxAccountButton"
-            android:layout_marginBottom="20dp"
-            android:layout_marginTop="45dp"
-            android:text="@string/fxaccount_update_credentials_button_label" />
+        <TextView
+            android:id="@+id/remote_error"
+            style="@style/FxAccountErrorItem" />
+
+        <FrameLayout style="@style/FxAccountButtonLayout" >
+
+            <ProgressBar
+                android:id="@+id/progress"
+                style="@style/FxAccountProgress" />
+
+            <Button
+                android:id="@+id/button"
+                style="@style/FxAccountButton"
+                android:text="@string/fxaccount_update_credentials_button_label" />
+        </FrameLayout>
 
         <TextView
             android:id="@+id/forgot_password_link"
             style="@style/FxAccountLinkItem"
             android:text="@string/fxaccount_forgot_password" />
 
         <LinearLayout style="@style/FxAccountSpacer" />
 
         <ImageView
             style="@style/FxAccountIcon"
             android:contentDescription="@string/fxaccount_icon_contentDescription" />
     </LinearLayout>
 
-</ScrollView>
+</ScrollView>
\ No newline at end of file
--- a/mobile/android/base/resources/values/fxaccount_styles.xml
+++ b/mobile/android/base/resources/values/fxaccount_styles.xml
@@ -86,21 +86,38 @@
         <item name="android:layout_marginTop">20dp</item>
         <item name="android:layout_width">wrap_content</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_gravity">center_horizontal</item>
         <item name="android:src">@drawable/fxaccount_icon</item>
     </style>
 
     <style name="FxAccountErrorItem">
-        <item name="android:layout_width">wrap_content</item>
+        <item name="android:layout_width">fill_parent</item>
+        <item name="android:layout_marginBottom">10dp</item>
+        <item name="android:layout_marginLeft">@dimen/fxaccount_corner_radius</item>
+        <item name="android:layout_marginRight">@dimen/fxaccount_corner_radius</item>
+        <item name="android:layout_marginTop">30dp</item>
         <item name="android:layout_height">wrap_content</item>
         <item name="android:layout_gravity">left</item>
-        <item name="android:layout_margin">5dp</item>
         <item name="android:background">@drawable/fxaccount_textview_error_background</item>
         <item name="android:padding">5dp</item>
         <item name="android:text">Error</item>
         <item name="android:textColor">@android:color/white</item>
         <item name="android:textSize">18sp</item>
         <item name="android:visibility">invisible</item>
     </style>
 
-</resources>
+    <style name="FxAccountProgress">
+        <item name="android:layout_width">fill_parent</item>
+        <item name="android:layout_height">fill_parent</item>
+        <item name="android:background">@drawable/fxaccount_button_background</item>
+        <item name="android:visibility">invisible</item>
+    </style>
+
+    <style name="FxAccountButtonLayout">
+        <item name="android:orientation">vertical</item>
+        <item name="android:layout_width">fill_parent</item>
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_marginBottom">20dp</item>
+    </style>
+
+</resources>
\ No newline at end of file
--- a/mobile/android/base/sync/GlobalSession.java
+++ b/mobile/android/base/sync/GlobalSession.java
@@ -95,49 +95,35 @@ public class GlobalSession implements Pr
   public AuthHeaderProvider getAuthHeaderProvider() {
     return config.getAuthHeaderProvider();
   }
 
   public URI wboURI(String collection, String id) throws URISyntaxException {
     return config.wboURI(collection, id);
   }
 
-  public GlobalSession(String username,
-                       AuthHeaderProvider authHeaderProvider,
-                       String prefsPath,
-                       KeyBundle syncKeyBundle,
+  public GlobalSession(SyncConfiguration config,
                        BaseGlobalSessionCallback callback,
                        Context context,
                        Bundle extras,
-                       ClientsDataDelegate clientsDelegate,
-                       NodeAssignmentCallback nodeAssignmentCallback)
-                           throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
-    if (username == null) {
-      throw new IllegalArgumentException("username must not be null.");
-    }
+                       ClientsDataDelegate clientsDelegate, NodeAssignmentCallback nodeAssignmentCallback)
+    throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
+
     if (callback == null) {
       throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
     }
 
     Logger.debug(LOG_TAG, "GlobalSession initialized with bundle " + extras);
 
-    if (syncKeyBundle == null ||
-        syncKeyBundle.getEncryptionKey() == null ||
-        syncKeyBundle.getHMACKey() == null) {
-      throw new SyncConfigurationException();
-    }
-
     this.callback        = callback;
     this.context         = context;
     this.clientsDelegate = clientsDelegate;
     this.nodeAssignmentCallback = nodeAssignmentCallback;
 
-    config = new SyncConfiguration(username, authHeaderProvider, prefsPath, this);
-    config.syncKeyBundle = syncKeyBundle;
-
+    this.config = config;
     registerCommands();
     prepareStages();
 
     Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
     config.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
 
     // TODO: data-driven plan for the sync, referring to prepareStages.
   }
--- a/mobile/android/base/sync/SyncConfiguration.java
+++ b/mobile/android/base/sync/SyncConfiguration.java
@@ -229,21 +229,17 @@ public class SyncConfiguration {
    *
    * If the user has made changes to engine syncing state, each engine will sync
    * according to the sync state specified in userSelectedEngines and propagate that
    * state to meta/global, to be uploaded.
    */
   public Map<String, Boolean> userSelectedEngines;
   public long userSelectedEnginesTimestamp;
 
-  // Fields that maintain a reference to a SharedPreferences instance, used for
-  // persistence.
-  // Behavior is undefined if the PrefsSource is switched out in flight.
-  public String          prefsPath;
-  public PrefsSource     prefsSource;
+  public SharedPreferences prefs;
 
   protected final AuthHeaderProvider authHeaderProvider;
 
   public static final String PREF_PREFS_VERSION = "prefs.version";
   public static final long CURRENT_PREFS_VERSION = 1;
 
   public static final String CLIENTS_COLLECTION_TIMESTAMP = "serverClientsTimestamp";  // When the collection was touched.
   public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp";  // When our record was touched.
@@ -262,26 +258,33 @@ public class SyncConfiguration {
   public static final String PREF_CLIENT_NAME = "account.clientName";
   public static final String PREF_NUM_CLIENTS = "account.numClients";
 
   /**
    * 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.prefsPath   = prefsPath;
-    this.prefsSource = prefsSource;
-    this.loadFromPrefs(getPrefs());
+    this.prefs = prefs;
+    this.loadFromPrefs(prefs);
+  }
+
+  public SyncConfiguration(String username, AuthHeaderProvider authHeaderProvider, SharedPreferences prefs, KeyBundle syncKeyBundle) {
+    this(username, authHeaderProvider, prefs);
+    this.syncKeyBundle = syncKeyBundle;
   }
 
   public SharedPreferences getPrefs() {
-    Logger.trace(LOG_TAG, "Returning prefs for " + prefsPath);
-    return prefsSource.getPrefs(prefsPath, Utils.SHARED_PREFERENCES_MODE);
+    return this.prefs;
   }
 
   /**
    * Valid engines supported by Android Sync.
    *
    * @return Set<String> of valid engine names that Android Sync implements.
    */
   public static Set<String> validEngineNames() {
--- a/mobile/android/base/sync/Utils.java
+++ b/mobile/android/base/sync/Utils.java
@@ -558,9 +558,17 @@ public class Utils {
     if (serverURL == null) {
       return SyncConstants.DEFAULT_AUTH_SERVER + "user/1.0/" + userPart;
     }
     if (!serverURL.endsWith("/")) {
       serverURL = serverURL + "/";
     }
     return serverURL + "user/1.0/" + userPart;
   }
+
+  public static void throwIfNull(Object... objects) {
+    for (Object object : objects) {
+      if (object == null) {
+        throw new IllegalArgumentException("object must not be null");
+      }
+    }
+  }
 }
--- a/mobile/android/base/sync/syncadapter/SyncAdapter.java
+++ b/mobile/android/base/sync/syncadapter/SyncAdapter.java
@@ -504,20 +504,37 @@ public class SyncAdapter extends Abstrac
             Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
           }
         }
       });
     } catch (IllegalArgumentException e) {
       // Do nothing.
     }
 
+    if (username == null) {
+      throw new IllegalArgumentException("username must not be null.");
+    }
+
+    if (syncKey == null) {
+      throw new SyncConfigurationException();
+    }
+
     final KeyBundle keyBundle = new KeyBundle(username, syncKey);
+
+    if (keyBundle == null ||
+        keyBundle.getEncryptionKey() == null ||
+        keyBundle.getHMACKey() == null) {
+      throw new SyncConfigurationException();
+    }
+
     final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(username, password);
-    GlobalSession globalSession = new GlobalSession(username, authHeaderProvider, prefsPath,
-                                                    keyBundle, this, this.mContext, extras, clientsDataDelegate, nodeAssignmentDelegate);
+    final SharedPreferences prefs = getContext().getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE);
+    final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs);
+    config.syncKeyBundle = keyBundle;
+    GlobalSession globalSession = new GlobalSession(config, this, this.mContext, extras, clientsDataDelegate, nodeAssignmentDelegate);
 
     globalSession.start();
   }
 
   private void notifyMonitor() {
     synchronized (syncMonitor) {
       Logger.trace(LOG_TAG, "Notifying sync monitor.");
       syncMonitor.notifyAll();
@@ -535,17 +552,16 @@ public class SyncAdapter extends Abstrac
   public void handleAborted(GlobalSession globalSession, String reason) {
     Logger.warn(LOG_TAG, "Sync aborted: " + reason);
     notifyMonitor();
   }
 
   @Override
   public void handleSuccess(GlobalSession globalSession) {
     Logger.info(LOG_TAG, "GlobalSession indicated success.");
-    Logger.debug(LOG_TAG, "Prefs target: " + globalSession.config.prefsPath);
     globalSession.config.persistToPrefs();
     notifyMonitor();
   }
 
   @Override
   public void handleStageCompleted(Stage currentState,
                                    GlobalSession globalSession) {
     Logger.trace(LOG_TAG, "Stage completed: " + currentState);
--- a/mobile/android/base/tokenserver/TokenServerClient.java
+++ b/mobile/android/base/tokenserver/TokenServerClient.java
@@ -8,16 +8,17 @@ import java.io.IOException;
 import java.net.URI;
 import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.SkewHandler;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonArrayJSONException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
@@ -198,21 +199,23 @@ public class TokenServerClient {
     return new TokenServerToken(result.getString(JSON_KEY_ID),
         result.getString(JSON_KEY_KEY),
         result.get(JSON_KEY_UID).toString(),
         result.getString(JSON_KEY_API_ENDPOINT));
   }
 
   public void getTokenFromBrowserIDAssertion(final String assertion, final boolean conditionsAccepted,
       final TokenServerClientDelegate delegate) {
-    BaseResource r = new BaseResource(uri);
+    final BaseResource r = new BaseResource(uri);
 
     r.delegate = new BaseResourceDelegate(r) {
       @Override
       public void handleHttpResponse(HttpResponse response) {
+        SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(r);
+        skewHandler.updateSkew(response, System.currentTimeMillis());
         try {
           TokenServerToken token = processResponse(response);
           invokeHandleSuccess(delegate, token);
         } catch (TokenServerException e) {
           invokeHandleFailure(delegate, e);
         }
       }
 
--- a/mobile/android/services/strings.xml.in
+++ b/mobile/android/services/strings.xml.in
@@ -137,16 +137,17 @@
   <string name="fxaccount_sign_up">&fxaccount.sign.up;</string>
   <string name="fxaccount_intro_contentDescription">&fxaccount.intro.contentDescription;</string>
   <string name="fxaccount_offer_resend_confirmation_email">&fxaccount.offer.resend.confirmation.email;</string>
   <string name="fxaccount_confirmation_description">&fxaccount.confirmation.description;</string>
   <string name="fxaccount_mail_contentDescription">&fxaccount.mail.contentDescription;</string>
   <string name="fxaccount_confirmation_email_sent">&fxaccount.confirmation.email.sent;</string>
   <string name="fxaccount_password_length_restriction">&fxaccount.password.length.restriction;</string>
   <string name="fxaccount_change_password">&fxaccount.change.password;</string>
+  <string name="fxaccount_status_needs_upgrade">You need to upgrade Firefox to log in to this account.</string>
   <string name="fxaccount_status_needs_verification">&fxaccount.status.needs.verification;</string>
   <string name="fxaccount_status_needs_credentials">&fxaccount.status.needs.credentials;</string>
   <string name="fxaccount_status_syncing">&fxaccount.status.syncing;</string>
   <string name="fxaccount_status_sync">&fxaccount.status.sync;</string>
   <string name="fxaccount_status_bookmarks">&fxaccount.status.bookmarks;</string>
   <string name="fxaccount_status_history">&fxaccount.status.history;</string>
   <string name="fxaccount_status_passwords">&fxaccount.status.passwords;</string>
   <string name="fxaccount_status_tabs">&fxaccount.status.tabs;</string>
@@ -176,8 +177,26 @@
   <string name="fxaccount_account_verified">Account verified</string>
   <string name="fxaccount_account_verified_description">You are now ready to use Sync!</string>
   <string name="fxaccount_confirm_your_account">Confirm your account</string>
   <string name="fxaccount_verification_link_awaits_at">A verification link awaits at:</string>
   <string name="fxaccount_verification_link_not_showing_up_offer_resend">Email not showing up? Send again</string>
   <!-- TODO: add email address to toast? -->
   <string name="fxaccount_confirm_verification_link_sent">Sent fresh verification link</string>
   <string name="fxaccount_confirm_verification_link_not_sent">Couldn\&apos;t send a fresh verification link</string>
+  <string name="fxaccount_status_debug_refresh_button_label">Refresh status view</string>
+  <string name="fxaccount_status_debug_dump_button_label">Dump account details</string>
+  <string name="fxaccount_status_debug_sync_button_label">Force sync</string>
+  <string name="fxaccount_status_debug_forget_certificate_button_label">Forget certificate (if applicable)</string>
+  <string name="fxaccount_status_debug_require_password_button_label">Require password re-entry</string>
+  <string name="fxaccount_status_debug_require_upgrade_button_label">Require upgrade</string>
+  <string name="fxaccount_create_account_unknown_error">Could not create account</string>
+  <string name="fxaccount_sign_in_unknown_error">Could not sign in</string>
+  <string name="fxaccount_update_credentials_unknown_error">Could not update password</string>
+  <string name="fxaccount_remote_error_UPGRADE_REQUIRED">You need to upgrade Firefox</string>
+  <string name="fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS">Account already exists</string>
+  <string name="fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST">Account does not exist</string>
+  <string name="fxaccount_remote_error_INCORRECT_PASSWORD">Bad password</string>
+  <string name="fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT">Account is not verified</string>
+  <string name="fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS">Try again later</string>
+  <string name="fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD">Try again later</string>
+  <string name="fxaccount_remote_error_UNKNOWN_ERROR">There was a problem</string>
+  <string name="fxaccount_remote_error_COULD_NOT_CONNECT">Couldn\&apos;t connect to the internet</string>
--- a/mobile/android/tests/background/junit3/src/sync/TestClientsStage.java
+++ b/mobile/android/tests/background/junit3/src/sync/TestClientsStage.java
@@ -2,26 +2,32 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.sync;
 
 import org.json.simple.JSONArray;
 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
 import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockClientsDataDelegate;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
 import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.SyncConfigurationException;
+import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
 
 import android.content.Context;
+import android.content.SharedPreferences;
 
 public class TestClientsStage extends AndroidSyncTestCase {
   private static final String TEST_USERNAME    = "johndoe";
   private static final String TEST_PASSWORD    = "password";
   private static final String TEST_SYNC_KEY    = "abcdeabcdeabcdeabcdeabcdea";
 
   public void setUp() {
     ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(getApplicationContext());
@@ -35,20 +41,22 @@ public class TestClientsStage extends An
     // Resetting is defined as being the same as for other engines -- discard local
     // and remote timestamps, tracked failed records, and tracked records to fetch.
 
     final Context context = getApplicationContext();
     final ClientsDatabaseAccessor dataAccessor = new ClientsDatabaseAccessor(context);
     final GlobalSessionCallback callback = new DefaultGlobalSessionCallback();
     final ClientsDataDelegate delegate = new MockClientsDataDelegate();
 
-    final GlobalSession session = new GlobalSession(
-        TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), null,
-        new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
-        callback, context, null, delegate, callback);
+    final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+    final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+    final SharedPreferences prefs = new MockSharedPreferences();
+    final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
+    config.syncKeyBundle = keyBundle;
+    GlobalSession session = new GlobalSession(config, callback, context, null, delegate, callback);
 
     SyncClientsEngineStage stage = new SyncClientsEngineStage() {
 
       @Override
       public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
         if (db == null) {
           db = dataAccessor;
         }
--- a/mobile/android/tests/background/junit3/src/sync/TestResetting.java
+++ b/mobile/android/tests/background/junit3/src/sync/TestResetting.java
@@ -5,32 +5,37 @@ package org.mozilla.gecko.background.syn
 
 import java.io.IOException;
 
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
 import org.mozilla.gecko.background.testhelpers.BaseMockServerSyncStage;
 import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockRecord;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.MetaGlobalException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 
+import android.content.SharedPreferences;
+
 /**
  * Test the on-device side effects of reset operations on a stage.
  *
  * See also "TestResetCommands" in the unit test suite.
  */
 public class TestResetting extends AndroidSyncTestCase {
   private static final String TEST_USERNAME    = "johndoe";
   private static final String TEST_PASSWORD    = "password";
@@ -147,21 +152,23 @@ public class TestResetting extends Andro
             performNotify(e);
           }
         }
       });
     }
   }
 
   private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
-    return new GlobalSession(
-        TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), null,
-        new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
-        callback, getApplicationContext(), null, null, callback) {
 
+    final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+    final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
+    final SharedPreferences prefs = new MockSharedPreferences();
+    final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
+    config.syncKeyBundle = keyBundle;
+    return new GlobalSession(config, callback, getApplicationContext(), null, null, callback) {
       @Override
       public boolean engineIsEnabled(String engineName,
                                      EngineSettings engineSettings)
         throws MetaGlobalException {
         return true;
       }
 
       @Override
--- a/mobile/android/tests/background/junit3/src/sync/TestUpgradeRequired.java
+++ b/mobile/android/tests/background/junit3/src/sync/TestUpgradeRequired.java
@@ -5,24 +5,27 @@ package org.mozilla.gecko.background.syn
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
 import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
+import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
 import org.mozilla.gecko.db.BrowserContract;
 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.SyncConstants;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.setup.Constants;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
 import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
 import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.ContentResolver;
@@ -164,19 +167,20 @@ public class TestUpgradeRequired extends
     final Result calledUpgradeRequired = new Result();
     final GlobalSessionCallback callback = new DefaultGlobalSessionCallback() {
       @Override
       public void informUpgradeRequiredResponse(final GlobalSession session) {
         calledUpgradeRequired.called = true;
       }
     };
 
-    final GlobalSession session = new MockGlobalSession(
-        TEST_USERNAME, TEST_PASSWORD,
-        new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY), callback);
+    MockSharedPreferences prefs = new MockSharedPreferences();
+    SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD), prefs);
+    config.syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
+    final GlobalSession session = new MockGlobalSession(config, callback);
 
     session.interpretHTTPFailure(simulate400());
     assertTrue(calledUpgradeRequired.called);
   }
 
   @Override
   public void tearDown() {
     deleteTestAccount();
--- a/mobile/android/tests/background/junit3/src/testhelpers/MockGlobalSession.java
+++ b/mobile/android/tests/background/junit3/src/testhelpers/MockGlobalSession.java
@@ -4,30 +4,35 @@
 package org.mozilla.gecko.background.testhelpers;
 
 import java.io.IOException;
 import java.util.HashMap;
 
 import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
+import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.CompletedStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
 
 public class MockGlobalSession extends MockPrefsGlobalSession {
 
-  public MockGlobalSession(String username, String password,
-      KeyBundle syncKeyBundle, GlobalSessionCallback callback)
+  public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException {
+    this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback);
+  }
+
+  public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback)
           throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
-    super(username, password, null, syncKeyBundle, callback, /* context */ null, null, null);
+    super(config, callback, null, null, null);
   }
 
   @Override
   public boolean engineIsEnabled(String engine, EngineSettings engineSettings) {
     return false;
   }
 
   @Override
--- a/mobile/android/tests/background/junit3/src/testhelpers/MockPrefsGlobalSession.java
+++ b/mobile/android/tests/background/junit3/src/testhelpers/MockPrefsGlobalSession.java
@@ -3,17 +3,19 @@
 
 package org.mozilla.gecko.background.testhelpers;
 
 import java.io.IOException;
 
 import org.json.simple.parser.ParseException;
 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.Utils;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -22,40 +24,43 @@ import android.os.Bundle;
 /**
  * GlobalSession touches the Android prefs system. Stub that out.
  */
 public class MockPrefsGlobalSession extends GlobalSession {
 
   public MockSharedPreferences prefs;
 
   public MockPrefsGlobalSession(
-      String username, String password, String prefsPath,
+      SyncConfiguration config, GlobalSessionCallback callback, Context context,
+      Bundle extras, ClientsDataDelegate clientsDelegate)
+      throws SyncConfigurationException, IllegalArgumentException, IOException,
+      ParseException, NonObjectJSONException {
+    super(config, callback, context, extras, clientsDelegate, callback);
+  }
+
+  public static MockPrefsGlobalSession getSession(
+      String username, String password,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       Bundle extras, ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
       ParseException, NonObjectJSONException {
-    this(username, new BasicAuthHeaderProvider(username, password), prefsPath, syncKeyBundle, callback, context, extras, clientsDelegate);
+    return getSession(username, new BasicAuthHeaderProvider(username, password), null,
+         syncKeyBundle, callback, context, extras, clientsDelegate);
   }
 
-  public MockPrefsGlobalSession(
+  public static MockPrefsGlobalSession getSession(
       String username, AuthHeaderProvider authHeaderProvider, String prefsPath,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       Bundle extras, ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
       ParseException, NonObjectJSONException {
-    super(username, authHeaderProvider, prefsPath, syncKeyBundle,
-        callback, context, extras, clientsDelegate, callback);
-  }
 
-  @Override
-  public SharedPreferences getPrefs(String name, int mode) {
-    if (prefs == null) {
-      prefs = new MockSharedPreferences();
-    }
-    return prefs;
+    final SharedPreferences prefs = new MockSharedPreferences();
+    final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs);
+    config.syncKeyBundle = syncKeyBundle;
+    return new MockPrefsGlobalSession(config, callback, context, extras, clientsDelegate);
   }
 
   @Override
   public Context getContext() {
     return null;
   }
-
 }
--- a/netwerk/dns/effective_tld_names.dat
+++ b/netwerk/dns/effective_tld_names.dat
@@ -7247,19 +7247,16 @@ best
 country
 
 // KRED : 2013-12-19 KredTLD Pty Ltd
 kred
 
 // feedback : 2013-12-19 Top Level Spectrum, Inc.
 feedback
 
-// works : 2013-12-19 Top Level Domain Holdings Limited
-works
-
 // work : 2013-12-19 Top Level Domain Holdings Limited
 work
 
 // luxe : 2014-01-09 Top Level Domain Holdings Limited 
 luxe
 
 // ryukyu : 2014-01-09 BusinessRalliart inc. 
 ryukyu
--- a/nsprpub/TAG-INFO
+++ b/nsprpub/TAG-INFO
@@ -1,1 +1,1 @@
-NSPR_4_10_3_BETA2
+NSPR_4_10_3_BETA3
--- a/nsprpub/config/prdepend.h
+++ b/nsprpub/config/prdepend.h
@@ -5,9 +5,8 @@
 
 /*
  * A dummy header file that is a dependency for all the object files.
  * Used to force a full recompilation of NSPR in Mozilla's Tinderbox
  * depend builds.  See comments in rules.mk.
  */
 
 #error "Do not include this header file."
-
--- a/nsprpub/pr/src/pthreads/ptio.c
+++ b/nsprpub/pr/src/pthreads/ptio.c
@@ -3395,42 +3395,46 @@ failed:
 }  /* PR_AllocFileDesc */
 
 #if !defined(_PR_INET6) || defined(_PR_INET6_PROBE)
 PR_EXTERN(PRStatus) _pr_push_ipv6toipv4_layer(PRFileDesc *fd);
 #if defined(_PR_INET6_PROBE)
 extern PRBool _pr_ipv6_is_present(void);
 PR_IMPLEMENT(PRBool) _pr_test_ipv6_socket()
 {
-PRInt32 osfd;
-
 #if defined(DARWIN)
     /*
      * Disable IPv6 if Darwin version is less than 7.0.0 (OS X 10.3).  IPv6 on
      * lesser versions is not ready for general use (see bug 222031).
      */
     {
         struct utsname u;
         if (uname(&u) != 0 || atoi(u.release) < 7)
             return PR_FALSE;
     }
 #endif
 
+#if defined(LINUX)
+    /* If /proc/net/if_inet6 exists, the Linux kernel supports IPv6. */
+    int rv = access("/proc/net/if_inet6", F_OK);
+    return (rv == 0);
+#else
     /*
      * HP-UX only: HP-UX IPv6 Porting Guide (dated February 2001)
      * suggests that we call open("/dev/ip6", O_RDWR) to determine
      * whether IPv6 APIs and the IPv6 stack are on the system.
      * Our portable test below seems to work fine, so I am using it.
      */
-    osfd = socket(AF_INET6, SOCK_STREAM, 0);
+    PRInt32 osfd = socket(AF_INET6, SOCK_STREAM, 0);
     if (osfd != -1) {
         close(osfd);
         return PR_TRUE;
     }
     return PR_FALSE;
+#endif
 }
 #endif	/* _PR_INET6_PROBE */
 #endif
 
 PR_IMPLEMENT(PRFileDesc*) PR_Socket(PRInt32 domain, PRInt32 type, PRInt32 proto)
 {
     PRIntn osfd;
     PRDescType ftype;
new file mode 100644
--- /dev/null
+++ b/testing/marionette/client/marionette/expected.py
@@ -0,0 +1,265 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import errors
+import types
+
+"""This file provides a set of expected conditions for common use
+cases when writing Marionette tests.
+
+The conditions rely on explicit waits that retries conditions a number
+of times until they are either successfully met, or they time out.
+
+"""
+
+class element_present(object):
+    """Checks that a web element is present in the DOM of the current
+    context.  This does not necessarily mean that the element is
+    visible.
+
+    You can select which element to be checked for presence by
+    supplying a locator:
+
+        el = Wait(marionette).until(expected.element_present(By.ID, "foo"))
+
+    Or by using a function/lambda returning an element:
+
+        el = Wait(marionette).until(expected.element_present(lambda m: m.find_element(By.ID, "foo")))
+
+    :param args: locator or function returning web element
+    :returns: the web element once it is located, or False
+
+    """