merge m-c to elm
authorTim Taubert <ttaubert@mozilla.com>
Wed, 18 Dec 2013 17:54:23 +0100
changeset 178510 882f69c9cadc20b1633ee843ddedc21c24213018
parent 178035 7ec385eb27f569bda8977d25dbf2ba21d6008b51 (current diff)
parent 178509 755f7983b4e27cd63717a4d50299762ab7242d73 (diff)
child 178511 1e5821e13a77c56d138cf04d3243be25295b079f
push id462
push userraliiev@mozilla.com
push dateTue, 22 Apr 2014 00:22:30 +0000
treeherdermozilla-release@ac5db8c74ac0 [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 m-c to elm
browser/base/content/sync/addDevice.js
browser/base/content/sync/addDevice.xul
browser/base/content/sync/genericChange.js
browser/base/content/sync/genericChange.xul
browser/base/content/sync/key.xhtml
browser/base/content/sync/quota.js
browser/base/content/sync/quota.xul
browser/base/content/sync/setup.js
browser/base/content/sync/setup.xul
browser/base/content/test/general/browser.ini
browser/locales/en-US/chrome/browser/syncGenericChange.properties
browser/locales/en-US/chrome/browser/syncKey.dtd
browser/locales/en-US/chrome/browser/syncQuota.dtd
browser/locales/en-US/chrome/browser/syncQuota.properties
browser/locales/en-US/chrome/browser/syncSetup.dtd
browser/locales/en-US/chrome/browser/syncSetup.properties
browser/themes/linux/syncCommon.css
browser/themes/linux/syncQuota.css
browser/themes/linux/syncSetup.css
browser/themes/osx/syncCommon.css
browser/themes/osx/syncQuota.css
browser/themes/osx/syncSetup.css
browser/themes/windows/syncCommon.css
browser/themes/windows/syncQuota.css
browser/themes/windows/syncSetup.css
mobile/android/base/BrowserApp.java
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1309,17 +1309,16 @@ pref("social.allowMultipleWorkers", true
 pref("dom.identity.enabled", false);
 
 // Turn on the CSP 1.0 parser for Content Security Policy headers
 pref("security.csp.speccompliant", true);
 
 // Block insecure active content on https pages
 pref("security.mixed_content.block_active_content", true);
 
-
 // Override the Gecko-default value of false for Firefox.
 pref("plain_text.wrap_long_lines", true);
 
 // If this turns true, Moz*Gesture events are not called stopPropagation()
 // before content.
 pref("dom.debug.propagate_gesture_events_through_content", false);
 
 // The request URL of the GeoLocation backend.
@@ -1327,10 +1326,14 @@ pref("geo.wifi.uri", "https://www.google
 
 // Necko IPC security checks only needed for app isolation for cookies/cache/etc:
 // currently irrelevant for desktop e10s
 pref("network.disable.ipc.security", true);
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
 
+// The URL where remote content that composes the UI for Firefox Accounts should
+// be fetched. Must use HTTPS.
+pref("firefox.accounts.remoteUrl", "https://accounts.dev.lcip.org/flow?moar_native=true");
+
 // The URL of the Firefox Accounts auth server backend
 pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
new file mode 100644
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.css
@@ -0,0 +1,21 @@
+* {
+  margin: 0;
+  padding: 0;
+}
+
+html, body {
+  height: 100%;
+}
+
+#content {
+  width: 100%;
+  height: 100%;
+  border: 0;
+  display: flex;
+}
+
+#remote {
+  width: 100%;
+  height: 100%;
+  border: 0;
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.js
@@ -0,0 +1,133 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/FxAccounts.jsm");
+
+function log(msg) {
+  //dump("FXA: " + msg + "\n");
+};
+
+function error(msg) {
+  console.log("Firefox Account Error: " + msg + "\n");
+};
+
+let wrapper = {
+  iframe: null,
+
+  init: function () {
+    let iframe = document.getElementById("remote");
+    this.iframe = iframe;
+    iframe.addEventListener("load", this);
+
+    try {
+      iframe.src = fxAccounts.getAccountsURI();
+    } catch (e) {
+      error("Couldn't init Firefox Account wrapper: " + e.message);
+    }
+  },
+
+  handleEvent: function (evt) {
+    switch (evt.type) {
+      case "load":
+        this.iframe.contentWindow.addEventListener("FirefoxAccountsCommand", this);
+        this.iframe.removeEventListener("load", this);
+        break;
+      case "FirefoxAccountsCommand":
+        this.handleRemoteCommand(evt);
+        break;
+    }
+  },
+
+  /**
+   * onLogin handler receives user credentials from the jelly after a
+   * sucessful login and stores it in the fxaccounts service
+   *
+   * @param accountData the user's account data and credentials
+   */
+  onLogin: function (accountData) {
+    log("Received: 'login'. Data:" + JSON.stringify(accountData));
+
+    fxAccounts.setSignedInUser(accountData).then(
+      () => {
+        this.injectData("message", { status: "login" });
+        // until we sort out a better UX, just leave the jelly page in place.
+        // If the account email is not yet verified, it will tell the user to
+        // go check their email, but then it will *not* change state after
+        // the verification completes (the browser will begin syncing, but
+        // won't notify the user). If the email has already been verified,
+        // the jelly will say "Welcome! You are successfully signed in as
+        // EMAIL", but it won't then say "syncing started".
+      },
+      (err) => this.injectData("message", { status: "error", error: err })
+    );
+  },
+
+  /**
+   * onSessionStatus sends the currently signed in user's credentials
+   * to the jelly.
+   */
+  onSessionStatus: function () {
+    log("Received: 'session_status'.");
+
+    fxAccounts.getSignedInUser().then(
+      (accountData) => this.injectData("message", { status: "session_status", data: accountData }),
+      (err) => this.injectData("message", { status: "error", error: err })
+    );
+  },
+
+  /**
+   * onSignOut handler erases the current user's session from the fxaccounts service
+   */
+  onSignOut: function () {
+    log("Received: 'sign_out'.");
+
+    fxAccounts.signOut().then(
+      () => this.injectData("message", { status: "sign_out" }),
+      (err) => this.injectData("message", { status: "error", error: err })
+    );
+  },
+
+  handleRemoteCommand: function (evt) {
+    log('command: ' + evt.detail.command);
+    let data = evt.detail.data;
+
+    switch (evt.detail.command) {
+      case "login":
+        this.onLogin(data);
+        break;
+      case "session_status":
+        this.onSessionStatus(data);
+        break;
+      case "sign_out":
+        this.onSignOut(data);
+        break;
+      default:
+        log("Unexpected remote command received: " + evt.detail.command + ". Ignoring command.");
+        break;
+    }
+  },
+
+  injectData: function (type, content) {
+    let authUrl;
+    try {
+      authUrl = fxAccounts.getAccountsURI();
+    } catch (e) {
+      error("Couldn't inject data: " + e.message);
+      return;
+    }
+    let data = {
+      type: type,
+      content: content
+    };
+    this.iframe.contentWindow.postMessage(data, authUrl);
+  },
+};
+
+wrapper.init();
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/aboutaccounts/aboutaccounts.xhtml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<!DOCTYPE html [
+  <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+  %htmlDTD;
+  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+  %brandDTD;
+  <!ENTITY % aboutAccountsDTD SYSTEM "chrome://browser/locale/aboutAccounts.dtd">
+  %aboutAccountsDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+   <title>&aboutAccounts.pageTitle;</title>
+   <link rel="icon" type="image/png" id="favicon"
+         href="chrome://branding/content/icon32.png"/>
+   <link rel="stylesheet"
+     href="chrome://browser/content/aboutaccounts/aboutaccounts.css"
+     type="text/css" />
+  </head>
+  <body>
+    <iframe mozframetype="content" id="remote" />
+    <script type="text/javascript;version=1.8"
+      src="chrome://browser/content/aboutaccounts/aboutaccounts.js" />
+  </body>
+</html>
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -447,17 +447,17 @@
                         key="key_openAddons"
                         command="Tools:Addons"/>
 #ifdef MOZ_SERVICES_SYNC
               <!-- only one of sync-setup or sync-menu will be showing at once -->
               <menuitem id="sync-setup"
                         label="&syncSetup.label;"
                         accesskey="&syncSetup.accesskey;"
                         observes="sync-setup-state"
-                        oncommand="gSyncUI.openSetup()"/>
+                        oncommand="gSyncUI.openAccountsPage()"/>
               <menuitem id="sync-syncnowitem"
                         label="&syncSyncNowItem.label;"
                         accesskey="&syncSyncNowItem.accesskey;"
                         observes="sync-syncnow-state"
                         oncommand="gSyncUI.doSync(event);"/>
 #endif
               <menuseparator id="devToolsSeparator"/>
               <menu id="webDeveloperMenu"
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -200,17 +200,17 @@ let gSyncUI = {
 
   onQuotaNotice: function onQuotaNotice(subject, data) {
     let title = this._stringBundle.GetStringFromName("warning.sync.quota.label");
     let description = this._stringBundle.GetStringFromName("warning.sync.quota.description");
     let buttons = [];
     buttons.push(new Weave.NotificationButton(
       this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"),
       this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"),
-      function() { gSyncUI.openQuotaDialog(); return true; }
+      function() { gSyncUI.openAccountsPage(); return true; }
     ));
 
     let notification = new Weave.Notification(
       title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons);
     Weave.Notifications.replaceTitle(notification);
   },
 
   _getAppName: function () {
@@ -248,79 +248,36 @@ let gSyncUI = {
     Weave.Notifications.replaceTitle(notification);
   },
 
   openServerStatus: function () {
     let statusURL = Services.prefs.getCharPref("services.sync.statusURL");
     window.openUILinkIn(statusURL, "tab");
   },
 
+  openAccountsPage: function () {
+    switchToTabHavingURI("about:accounts", true);
+  },
+
   // Commands
   doSync: function SUI_doSync() {
     setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0);
   },
 
   handleToolbarButton: function SUI_handleStatusbarButton() {
     if (this._needsSetup())
-      this.openSetup();
+      this.openAccountsPage();
     else
       this.doSync();
   },
 
-  //XXXzpao should be part of syncCommon.js - which we might want to make a module...
-  //        To be fixed in a followup (bug 583366)
-
-  /**
-   * Invoke the Sync setup wizard.
-   *
-   * @param wizardType
-   *        Indicates type of wizard to launch:
-   *          null    -- regular set up wizard
-   *          "pair"  -- pair a device first
-   *          "reset" -- reset sync
-   */
-
-  openSetup: function SUI_openSetup(wizardType) {
-    let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
-    if (win)
-      win.focus();
-    else {
-      window.openDialog("chrome://browser/content/sync/setup.xul",
-                        "weaveSetup", "centerscreen,chrome,resizable=no",
-                        wizardType);
-    }
-  },
-
-  openAddDevice: function () {
-    if (!Weave.Utils.ensureMPUnlocked())
-      return;
-
-    let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
-    if (win)
-      win.focus();
-    else
-      window.openDialog("chrome://browser/content/sync/addDevice.xul",
-                        "syncAddDevice", "centerscreen,chrome,resizable=no");
-  },
-
-  openQuotaDialog: function SUI_openQuotaDialog() {
-    let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
-    if (win)
-      win.focus();
-    else
-      Services.ww.activeWindow.openDialog(
-        "chrome://browser/content/sync/quota.xul", "",
-        "centerscreen,chrome,dialog,modal");
-  },
-
   openPrefs: function SUI_openPrefs() {
     openPreferences("paneSync");
   },
 
-
   // Helpers
   _updateLastSyncTime: function SUI__updateLastSyncTime() {
     if (!gBrowser)
       return;
 
     let syncButton = document.getElementById("sync-button");
     if (!syncButton)
       return;
@@ -401,17 +358,17 @@ let gSyncUI = {
     else if (Weave.Status.sync == Weave.OVER_QUOTA) {
       description = this._stringBundle.GetStringFromName(
         "error.sync.quota.description");
       buttons.push(new Weave.NotificationButton(
         this._stringBundle.GetStringFromName(
           "error.sync.viewQuotaButton.label"),
         this._stringBundle.GetStringFromName(
           "error.sync.viewQuotaButton.accesskey"),
-        function() { gSyncUI.openQuotaDialog(); return true; } )
+        function() { gSyncUI.openAccountsPage(); return true; } )
       );
     }
     else if (Weave.Status.enforceBackoff) {
       priority = Weave.Notifications.PRIORITY_INFO;
       buttons.push(new Weave.NotificationButton(
         this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
         this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
         function() { gSyncUI.openServerStatus(); return true; }
deleted file mode 100644
--- a/browser/base/content/sync/addDevice.js
+++ /dev/null
@@ -1,157 +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/. */
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cu = Components.utils;
-
-Cu.import("resource://services-sync/main.js");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-const PIN_PART_LENGTH = 4;
-
-const ADD_DEVICE_PAGE       = 0;
-const SYNC_KEY_PAGE         = 1;
-const DEVICE_CONNECTED_PAGE = 2;
-
-let gSyncAddDevice = {
-
-  init: function init() {
-    this.pin1.setAttribute("maxlength", PIN_PART_LENGTH);
-    this.pin2.setAttribute("maxlength", PIN_PART_LENGTH);
-    this.pin3.setAttribute("maxlength", PIN_PART_LENGTH);
-
-    this.nextFocusEl = {pin1: this.pin2,
-                        pin2: this.pin3,
-                        pin3: this.wizard.getButton("next")};
-
-    this.throbber = document.getElementById("pairDeviceThrobber");
-    this.errorRow = document.getElementById("errorRow");
-
-    // Kick off a sync. That way the server will have the most recent data from
-    // this computer and it will show up immediately on the new device.
-    Weave.Service.scheduler.scheduleNextSync(0);
-  },
-
-  onPageShow: function onPageShow() {
-    this.wizard.getButton("back").hidden = true;
-
-    switch (this.wizard.pageIndex) {
-      case ADD_DEVICE_PAGE:
-        this.onTextBoxInput();
-        this.wizard.canRewind = false;
-        this.wizard.getButton("next").hidden = false;
-        this.pin1.focus();
-        break;
-      case SYNC_KEY_PAGE:
-        this.wizard.canAdvance = false;
-        this.wizard.canRewind = true;
-        this.wizard.getButton("back").hidden = false;
-        this.wizard.getButton("next").hidden = true;
-        document.getElementById("weavePassphrase").value =
-          Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
-        break;
-      case DEVICE_CONNECTED_PAGE:
-        this.wizard.canAdvance = true;
-        this.wizard.canRewind = false;
-        this.wizard.getButton("cancel").hidden = true;
-        break;
-    }
-  },
-
-  onWizardAdvance: function onWizardAdvance() {
-    switch (this.wizard.pageIndex) {
-      case ADD_DEVICE_PAGE:
-        this.startTransfer();
-        return false;
-      case DEVICE_CONNECTED_PAGE:
-        window.close();
-        return false;
-    }
-    return true;
-  },
-
-  startTransfer: function startTransfer() {
-    this.errorRow.hidden = true;
-    // When onAbort is called, Weave may already be gone.
-    const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
-
-    let self = this;
-    let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
-      onPaired: function onPaired() {
-        let credentials = {account:   Weave.Service.identity.account,
-                           password:  Weave.Service.identity.basicPassword,
-                           synckey:   Weave.Service.identity.syncKey,
-                           serverURL: Weave.Service.serverURL};
-        jpakeclient.sendAndComplete(credentials);
-      },
-      onComplete: function onComplete() {
-        delete self._jpakeclient;
-        self.wizard.pageIndex = DEVICE_CONNECTED_PAGE;
-
-        // Schedule a Sync for soonish to fetch the data uploaded by the
-        // device with which we just paired.
-        Weave.Service.scheduler.scheduleNextSync(Weave.Service.scheduler.activeInterval);
-      },
-      onAbort: function onAbort(error) {
-        delete self._jpakeclient;
-
-        // Aborted by user, ignore.
-        if (error == JPAKE_ERROR_USERABORT) {
-          return;
-        }
-
-        self.errorRow.hidden = false;
-        self.throbber.hidden = true;
-        self.pin1.value = self.pin2.value = self.pin3.value = "";
-        self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false;
-        self.pin1.focus();
-      }
-    });
-    this.throbber.hidden = false;
-    this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true;
-    this.wizard.canAdvance = false;
-
-    let pin = this.pin1.value + this.pin2.value + this.pin3.value;
-    let expectDelay = false;
-    jpakeclient.pairWithPIN(pin, expectDelay);
-  },
-
-  onWizardBack: function onWizardBack() {
-    if (this.wizard.pageIndex != SYNC_KEY_PAGE)
-      return true;
-
-    this.wizard.pageIndex = ADD_DEVICE_PAGE;
-    return false;
-  },
-
-  onWizardCancel: function onWizardCancel() {
-    if (this._jpakeclient) {
-      this._jpakeclient.abort();
-      delete this._jpakeclient;
-    }
-    return true;
-  },
-
-  onTextBoxInput: function onTextBoxInput(textbox) {
-    if (textbox && textbox.value.length == PIN_PART_LENGTH)
-      this.nextFocusEl[textbox.id].focus();
-
-    this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH
-                              && this.pin2.value.length == PIN_PART_LENGTH
-                              && this.pin3.value.length == PIN_PART_LENGTH);
-  },
-
-  goToSyncKeyPage: function goToSyncKeyPage() {
-    this.wizard.pageIndex = SYNC_KEY_PAGE;
-  }
-
-};
-// onWizardAdvance() and onPageShow() are run before init() so we'll set
-// these up as lazy getters.
-["wizard", "pin1", "pin2", "pin3"].forEach(function (id) {
-  XPCOMUtils.defineLazyGetter(gSyncAddDevice, id, function() {
-    return document.getElementById(id);
-  });
-});
deleted file mode 100644
--- a/browser/base/content/sync/addDevice.xul
+++ /dev/null
@@ -1,129 +0,0 @@
-<?xml version="1.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/. -->
-
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?>
-
-<!DOCTYPE window [
-<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
-<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
-<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd">
-%brandDTD;
-%syncBrandDTD;
-%syncSetupDTD;
-]>
-<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
-        id="wizard"
-        title="&pairDevice.title.label;"
-        windowtype="Sync:AddDevice"
-        persist="screenX screenY"
-        onwizardnext="return gSyncAddDevice.onWizardAdvance();"
-        onwizardback="return gSyncAddDevice.onWizardBack();"
-        onwizardcancel="gSyncAddDevice.onWizardCancel();"
-        onload="gSyncAddDevice.init();">
-
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/addDevice.js"/>
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/utils.js"/>
-  <script type="application/javascript"
-          src="chrome://browser/content/utilityOverlay.js"/>
-  <script type="application/javascript"
-          src="chrome://global/content/printUtils.js"/>
-
-  <wizardpage id="addDevicePage"
-              label="&pairDevice.title.label;"
-              onpageshow="gSyncAddDevice.onPageShow();">
-    <description>
-      &pairDevice.dialog.description.label;
-      <label class="text-link"
-             value="&addDevice.showMeHow.label;"
-             href="https://services.mozilla.com/sync/help/add-device"/>
-    </description>
-    <separator class="groove-thin"/>
-    <description>
-      &addDevice.dialog.enterCode.label;
-    </description>
-    <separator class="groove-thin"/>
-    <vbox align="center">
-      <textbox id="pin1"
-               class="pin"
-               oninput="gSyncAddDevice.onTextBoxInput(this);"
-               onfocus="this.select();"
-               />
-      <textbox id="pin2"
-               class="pin"
-               oninput="gSyncAddDevice.onTextBoxInput(this);"
-               onfocus="this.select();"
-               />
-      <textbox id="pin3"
-               class="pin"
-               oninput="gSyncAddDevice.onTextBoxInput(this);"
-               onfocus="this.select();" 
-              />
-    </vbox>
-    <separator class="groove-thin"/>
-    <vbox id="pairDeviceThrobber" align="center" hidden="true">
-      <image/>
-    </vbox>
-    <hbox id="errorRow" pack="center" hidden="true">
-      <image class="statusIcon" status="error"/>
-      <label class="status"
-             value="&addDevice.dialog.tryAgain.label;"/>
-    </hbox>
-    <spacer flex="3"/>
-    <label class="text-link"
-           value="&addDevice.dontHaveDevice.label;"
-           onclick="gSyncAddDevice.goToSyncKeyPage();"/>
-  </wizardpage>
-
-  <!-- Need a non-empty label here, otherwise we get a default label on Mac -->
-  <wizardpage id="syncKeyPage"
-              label=" "
-              onpageshow="gSyncAddDevice.onPageShow();">
-    <description>
-      &addDevice.dialog.recoveryKey.label;
-    </description>
-    <spacer/>
-
-    <groupbox>
-      <label value="&recoveryKeyEntry.label;"
-             accesskey="&recoveryKeyEntry.accesskey;"
-             control="weavePassphrase"/>
-      <textbox id="weavePassphrase"
-               readonly="true"/>
-    </groupbox>
-
-    <groupbox align="center">
-      <description>&recoveryKeyBackup.description;</description>
-      <hbox>
-        <button id="printSyncKeyButton"
-                label="&button.syncKeyBackup.print.label;"
-                accesskey="&button.syncKeyBackup.print.accesskey;"
-                oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/>
-        <button id="saveSyncKeyButton"
-                label="&button.syncKeyBackup.save.label;"
-                accesskey="&button.syncKeyBackup.save.accesskey;"
-                oncommand="gSyncUtils.passphraseSave('weavePassphrase');"/>
-      </hbox>
-    </groupbox>
-  </wizardpage>
-
-  <wizardpage id="deviceConnectedPage"
-              label="&addDevice.dialog.connected.label;"
-              onpageshow="gSyncAddDevice.onPageShow();">
-    <vbox align="center">
-      <image id="successPageIcon"/>
-    </vbox>
-    <separator/>
-    <description class="normal">
-      &addDevice.dialog.successful.label;
-    </description>
-  </wizardpage>
-
-</wizard>
deleted file mode 100644
--- a/browser/base/content/sync/genericChange.js
+++ /dev/null
@@ -1,235 +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/. */
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-
-Components.utils.import("resource://services-sync/main.js");
-Components.utils.import("resource://gre/modules/Services.jsm");
-
-let Change = {
-  _dialog: null,
-  _dialogType: null,
-  _status: null,
-  _statusIcon: null,
-  _firstBox: null,
-  _secondBox: null,
-
-  get _passphraseBox() {
-    delete this._passphraseBox;
-    return this._passphraseBox = document.getElementById("passphraseBox");
-  },
-
-  get _currentPasswordInvalid() {
-    return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
-  },
-
-  get _updatingPassphrase() {
-    return this._dialogType == "UpdatePassphrase";
-  },
-
-  onLoad: function Change_onLoad() {
-    /* Load labels */
-    let introText = document.getElementById("introText");
-    let introText2 = document.getElementById("introText2");
-    let warningText = document.getElementById("warningText");
-
-    // load some other elements & info from the window
-    this._dialog = document.getElementById("change-dialog");
-    this._dialogType = window.arguments[0];
-    this._duringSetup = window.arguments[1];
-    this._status = document.getElementById("status");
-    this._statusIcon = document.getElementById("statusIcon");
-    this._statusRow = document.getElementById("statusRow");
-    this._firstBox = document.getElementById("textBox1");
-    this._secondBox = document.getElementById("textBox2");
-
-    this._dialog.getButton("finish").disabled = true;
-    this._dialog.getButton("back").hidden = true;
-
-    this._stringBundle =
-      Services.strings.createBundle("chrome://browser/locale/syncGenericChange.properties");
-
-    switch (this._dialogType) {
-      case "UpdatePassphrase":
-      case "ResetPassphrase":
-        document.getElementById("textBox1Row").hidden = true;
-        document.getElementById("textBox2Row").hidden = true;
-        document.getElementById("passphraseLabel").value
-          = this._str("new.recoverykey.label");
-        document.getElementById("passphraseSpacer").hidden = false;
-
-        if (this._updatingPassphrase) {
-          document.getElementById("passphraseHelpBox").hidden = false;
-          document.title = this._str("new.recoverykey.title");
-          introText.textContent = this._str("new.recoverykey.introText");
-          this._dialog.getButton("finish").label
-            = this._str("new.recoverykey.acceptButton");
-        }
-        else {
-          document.getElementById("generatePassphraseButton").hidden = false;
-          document.getElementById("passphraseBackupButtons").hidden = false;
-          this._passphraseBox.setAttribute("readonly", "true");
-          let pp = Weave.Service.identity.syncKey;
-          if (Weave.Utils.isPassphrase(pp))
-             pp = Weave.Utils.hyphenatePassphrase(pp);
-          this._passphraseBox.value = pp;
-          this._passphraseBox.focus();
-          document.title = this._str("change.recoverykey.title");
-          introText.textContent = this._str("change.synckey.introText2");
-          warningText.textContent = this._str("change.recoverykey.warningText");
-          this._dialog.getButton("finish").label
-            = this._str("change.recoverykey.acceptButton");
-          if (this._duringSetup) {
-            this._dialog.getButton("finish").disabled = false;
-          }
-        }
-        break;
-      case "ChangePassword":
-        document.getElementById("passphraseRow").hidden = true;
-        let box1label = document.getElementById("textBox1Label");
-        let box2label = document.getElementById("textBox2Label");
-        box1label.value = this._str("new.password.label");
-
-        if (this._currentPasswordInvalid) {
-          document.title = this._str("new.password.title");
-          introText.textContent = this._str("new.password.introText");
-          this._dialog.getButton("finish").label
-            = this._str("new.password.acceptButton");
-          document.getElementById("textBox2Row").hidden = true;
-        }
-        else {
-          document.title = this._str("change.password.title");
-          box2label.value = this._str("new.password.confirm");
-          introText.textContent = this._str("change.password3.introText");
-          warningText.textContent = this._str("change.password.warningText");
-          this._dialog.getButton("finish").label
-            = this._str("change.password.acceptButton");
-        }
-        break;
-    }
-    document.getElementById("change-page")
-            .setAttribute("label", document.title);
-  },
-
-  _clearStatus: function _clearStatus() {
-    this._status.value = "";
-    this._statusIcon.removeAttribute("status");
-  },
-
-  _updateStatus: function Change__updateStatus(str, state) {
-     this._updateStatusWithString(this._str(str), state);
-  },
-  
-  _updateStatusWithString: function Change__updateStatusWithString(string, state) {
-    this._statusRow.hidden = false;
-    this._status.value = string;
-    this._statusIcon.setAttribute("status", state);
-
-    let error = state == "error";
-    this._dialog.getButton("cancel").disabled = !error;
-    this._dialog.getButton("finish").disabled = !error;
-    document.getElementById("printSyncKeyButton").disabled = !error;
-    document.getElementById("saveSyncKeyButton").disabled = !error;
-
-    if (state == "success")
-      window.setTimeout(window.close, 1500);
-  },
-
-  onDialogAccept: function() {
-    switch (this._dialogType) {
-      case "UpdatePassphrase":
-      case "ResetPassphrase":
-        return this.doChangePassphrase();
-        break;
-      case "ChangePassword":
-        return this.doChangePassword();
-        break;
-    }
-  },
-
-  doGeneratePassphrase: function () {
-    let passphrase = Weave.Utils.generatePassphrase();
-    this._passphraseBox.value = Weave.Utils.hyphenatePassphrase(passphrase);
-    this._dialog.getButton("finish").disabled = false;
-  },
-
-  doChangePassphrase: function Change_doChangePassphrase() {
-    let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
-    if (this._updatingPassphrase) {
-      Weave.Service.identity.syncKey = pp;
-      if (Weave.Service.login()) {
-        this._updateStatus("change.recoverykey.success", "success");
-        Weave.Service.persistLogin();
-        Weave.Service.scheduler.delayedAutoConnect(0);
-      }
-      else {
-        this._updateStatus("new.passphrase.status.incorrect", "error");
-      }
-    }
-    else {
-      this._updateStatus("change.recoverykey.label", "active");
-
-      if (Weave.Service.changePassphrase(pp))
-        this._updateStatus("change.recoverykey.success", "success");
-      else
-        this._updateStatus("change.recoverykey.error", "error");
-    }
-
-    return false;
-  },
-
-  doChangePassword: function Change_doChangePassword() {
-    if (this._currentPasswordInvalid) {
-      Weave.Service.identity.basicPassword = this._firstBox.value;
-      if (Weave.Service.login()) {
-        this._updateStatus("change.password.status.success", "success");
-        Weave.Service.persistLogin();
-      }
-      else {
-        this._updateStatus("new.password.status.incorrect", "error");
-      }
-    }
-    else {
-      this._updateStatus("change.password.status.active", "active");
-
-      if (Weave.Service.changePassword(this._firstBox.value))
-        this._updateStatus("change.password.status.success", "success");
-      else
-        this._updateStatus("change.password.status.error", "error");
-    }
-
-    return false;
-  },
-
-  validate: function (event) {
-    let valid = false;
-    let errorString = "";
-
-    if (this._dialogType == "ChangePassword") {
-      if (this._currentPasswordInvalid)
-        [valid, errorString] = gSyncUtils.validatePassword(this._firstBox);
-      else
-        [valid, errorString] = gSyncUtils.validatePassword(this._firstBox, this._secondBox);
-    }
-    else {
-      if (!this._updatingPassphrase)
-        return;
-
-      valid = this._passphraseBox.value != "";
-    }
-
-    if (errorString == "")
-      this._clearStatus();
-    else
-      this._updateStatusWithString(errorString, "error");
-
-    this._statusRow.hidden = valid;
-    this._dialog.getButton("finish").disabled = !valid;
-  },
-
-  _str: function Change__string(str) {
-    return this._stringBundle.GetStringFromName(str);
-  }
-};
deleted file mode 100644
--- a/browser/base/content/sync/genericChange.xul
+++ /dev/null
@@ -1,123 +0,0 @@
-<?xml version="1.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/. -->
-
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?>
-
-<!DOCTYPE window [
-<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
-<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
-<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd">
-%brandDTD;
-%syncBrandDTD;
-%syncSetupDTD;
-]>
-<wizard xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
-        id="change-dialog"
-        windowtype="Weave:ChangeSomething"
-        persist="screenX screenY"
-        onwizardnext="Change.onLoad()"
-        onwizardfinish="return Change.onDialogAccept();">
-
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/genericChange.js"/>
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/utils.js"/>
-  <script type="application/javascript"
-          src="chrome://global/content/printUtils.js"/>
-
-  <wizardpage id="change-page"
-              label="">
-
-    <description id="introText">
-    </description>
-
-    <separator class="thin"/>
-
-    <groupbox>
-      <grid>
-        <columns>
-          <column align="right"/>
-          <column flex="3"/>
-          <column flex="1"/>
-        </columns>
-        <rows>
-          <row id="textBox1Row" align="center">
-            <label id="textBox1Label" control="textBox1"/>
-            <textbox id="textBox1" type="password" oninput="Change.validate()"/>
-            <spacer/>
-          </row>
-          <row id="textBox2Row" align="center">
-            <label id="textBox2Label" control="textBox2"/>
-            <textbox id="textBox2" type="password" oninput="Change.validate()"/>
-            <spacer/>
-          </row>
-        </rows>
-      </grid>
-
-      <vbox id="passphraseRow">
-        <hbox flex="1">
-          <label id="passphraseLabel" control="passphraseBox"/>
-          <spacer flex="1"/>
-          <label id="generatePassphraseButton"
-                 hidden="true"
-                 value="&syncGenerateNewKey.label;"
-                 class="text-link inline-link"
-                 onclick="event.stopPropagation();
-                          Change.doGeneratePassphrase();"/>
-        </hbox>
-        <textbox id="passphraseBox"
-                 flex="1"
-                 onfocus="this.select()"
-                 oninput="Change.validate()"/>
-      </vbox>
-
-      <vbox id="feedback" pack="center">
-        <hbox id="statusRow" align="center">
-          <image id="statusIcon" class="statusIcon"/>
-          <label id="status" class="status" value=" "/>
-        </hbox>
-      </vbox>
-    </groupbox>
-
-    <separator class="thin"/>
-
-    <hbox id="passphraseBackupButtons"
-          hidden="true"
-          pack="center">
-      <button id="printSyncKeyButton"
-              label="&button.syncKeyBackup.print.label;"
-              accesskey="&button.syncKeyBackup.print.accesskey;"
-              oncommand="gSyncUtils.passphrasePrint('passphraseBox');"/>
-      <button id="saveSyncKeyButton"
-              label="&button.syncKeyBackup.save.label;"
-              accesskey="&button.syncKeyBackup.save.accesskey;"
-              oncommand="gSyncUtils.passphraseSave('passphraseBox');"/>
-    </hbox>
-
-    <vbox id="passphraseHelpBox"
-          hidden="true">
-      <description>
-        &existingRecoveryKey.description;
-        <label class="text-link"
-               href="https://services.mozilla.com/sync/help/manual-setup">
-          &addDevice.showMeHow.label;
-        </label>
-      </description>
-    </vbox>
-
-    <spacer id="passphraseSpacer"
-            flex="1"
-            hidden="true"/>
-
-    <description id="warningText" class="data">
-    </description>
-
-    <spacer flex="1"/>
-  </wizardpage>
-</wizard>
deleted file mode 100644
--- a/browser/base/content/sync/key.xhtml
+++ /dev/null
@@ -1,54 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<!DOCTYPE html [
-  <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
-  %htmlDTD;
-  <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
-  %syncBrandDTD;
-  <!ENTITY % syncKeyDTD SYSTEM "chrome://browser/locale/syncKey.dtd">
-  %syncKeyDTD;
-  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
-  %globalDTD;
-]>
-<html xmlns="http://www.w3.org/1999/xhtml">
-<head>
-  <title>&syncKey.page.title;</title>
-  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
-  <meta name="robots" content="noindex"/>
-  <style type="text/css">
-    #synckey { font-size: 150% }
-    footer { font-size: 70% }
-    /* Bug 575675: Need to have an a:visited rule in a chrome document. */
-    a:visited { color: purple; }
-  </style>
-</head>
-
-<body dir="&locale.dir;">
-<h1>&syncKey.page.title;</h1>
-
-<p id="synckey" dir="ltr">SYNCKEY</p>
-
-<p>&syncKey.page.description2;</p>
-
-<div id="column1">
-  <h2>&syncKey.keepItSecret.heading;</h2>
-  <p>&syncKey.keepItSecret.description;</p>
-</div>
-
-<div id="column2">
-  <h2>&syncKey.keepItSafe.heading;</h2>
-  <p><em>&syncKey.keepItSafe1.description;</em>&syncKey.keepItSafe2.description;<em>&syncKey.keepItSafe3.description;</em>&syncKey.keepItSafe4a.description;</p>
-</div>
-
-<p>&syncKey.findOutMore1.label;<a href="https://services.mozilla.com">https://services.mozilla.com</a>&syncKey.findOutMore2.label;</p>
-
-<footer>
- &syncKey.footer1.label;<a id="tosLink" href="termsURL">termsURL</a>&syncKey.footer2.label;<a id="ppLink" href="privacyURL">privacyURL</a>&syncKey.footer3.label;
-</footer>
-
-</body>
-</html>
--- a/browser/base/content/sync/progress.xhtml
+++ b/browser/base/content/sync/progress.xhtml
@@ -6,19 +6,16 @@
 <!DOCTYPE html [
   <!ENTITY % htmlDTD
     PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
     "DTD/xhtml1-strict.dtd">
   %htmlDTD;
   <!ENTITY % syncProgressDTD
     SYSTEM "chrome://browser/locale/syncProgress.dtd">
     %syncProgressDTD;
-  <!ENTITY % syncSetupDTD
-    SYSTEM "chrome://browser/locale/syncSetup.dtd">
-    %syncSetupDTD;
   <!ENTITY % globalDTD 
     SYSTEM "chrome://global/locale/global.dtd">
     %globalDTD;
 ]>
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <title>&syncProgress.pageTitle;</title>
@@ -28,20 +25,20 @@
 
     <link rel="icon" type="image/png" id="favicon"
           href="chrome://browser/skin/sync-16.png"/>
 
     <script type="text/javascript;version=1.8"
             src="chrome://browser/content/sync/progress.js"/>
   </head>
   <body onload="onLoad(event)" onunload="onUnload(event)" dir="&locale.dir;">
-    <title>&setup.successPage.title;</title>
+    <title>&syncProgress.headline;</title>
     <div id="floatingBox" class="main-content">
       <div id="title">
-        <h1>&setup.successPage.title;</h1>
+        <h1>&syncProgress.headline;</h1>
       </div>
       <div id="successLogo">
         <img id="brandSyncLogo" src="chrome://browser/skin/sync-128.png" alt="&syncProgress.logoAltText;" />
       </div>
       <div id="loadingText">
         <p id="blurb">&syncProgress.textBlurb; </p>
       </div>
       <div id="progressBar">
deleted file mode 100644
--- a/browser/base/content/sync/quota.js
+++ /dev/null
@@ -1,268 +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/. */
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cr = Components.results;
-const Cu = Components.utils;
-
-Cu.import("resource://services-sync/main.js");
-Cu.import("resource://gre/modules/DownloadUtils.jsm");
-
-let gSyncQuota = {
-
-  init: function init() {
-    this.bundle = document.getElementById("quotaStrings");
-    let caption = document.getElementById("treeCaption");
-    caption.firstChild.nodeValue = this.bundle.getString("quota.treeCaption.label");
-
-    gUsageTreeView.init();
-    this.tree = document.getElementById("usageTree");
-    this.tree.view = gUsageTreeView;
-
-    this.loadData();
-  },
-
-  loadData: function loadData() {
-    this._usage_req = Weave.Service.getStorageInfo(Weave.INFO_COLLECTION_USAGE,
-                                                   function (error, usage) {
-      delete gSyncQuota._usage_req;
-      // displayUsageData handles null values, so no need to check 'error'.
-      gUsageTreeView.displayUsageData(usage);
-    });
-
-    let usageLabel = document.getElementById("usageLabel");
-    let bundle = this.bundle;
-
-    this._quota_req = Weave.Service.getStorageInfo(Weave.INFO_QUOTA,
-                                                   function (error, quota) {
-      delete gSyncQuota._quota_req;
-
-      if (error) {
-        usageLabel.value = bundle.getString("quota.usageError.label");
-        return;
-      }
-      let used = gSyncQuota.convertKB(quota[0]);
-      if (!quota[1]) {
-        // No quota on the server.
-        usageLabel.value = bundle.getFormattedString(
-          "quota.usageNoQuota.label", used);
-        return;
-      }
-      let percent = Math.round(100 * quota[0] / quota[1]);
-      let total = gSyncQuota.convertKB(quota[1]);
-      usageLabel.value = bundle.getFormattedString(
-        "quota.usagePercentage.label", [percent].concat(used).concat(total));
-    });
-  },
-
-  onCancel: function onCancel() {
-    if (this._usage_req) {
-      this._usage_req.abort();
-    }
-    if (this._quota_req) {
-      this._quota_req.abort();
-    }
-    return true;
-  },
-
-  onAccept: function onAccept() {
-    let engines = gUsageTreeView.getEnginesToDisable();
-    for each (let engine in engines) {
-      Weave.Service.engineManager.get(engine).enabled = false;
-    }
-    if (engines.length) {
-      // The 'Weave' object will disappear once the window closes.
-      let Service = Weave.Service;
-      Weave.Utils.nextTick(function() { Service.sync(); });
-    }
-    return this.onCancel();
-  },
-
-  convertKB: function convertKB(value) {
-    return DownloadUtils.convertByteUnits(value * 1024);
-  }
-
-};
-
-let gUsageTreeView = {
-
-  _ignored: {keys: true,
-             meta: true,
-             clients: true},
-
-  /*
-   * Internal data structures underlaying the tree.
-   */
-  _collections: [],
-  _byname: {},
-
-  init: function init() {
-    let retrievingLabel = gSyncQuota.bundle.getString("quota.retrieving.label");
-    for each (let engine in Weave.Service.engineManager.getEnabled()) {
-      if (this._ignored[engine.name])
-        continue;
-
-      // Some engines use the same pref, which means they can only be turned on
-      // and off together. We need to combine them here as well.
-      let existing = this._byname[engine.prefName];
-      if (existing) {
-        existing.engines.push(engine.name);
-        continue;
-      }
-
-      let obj = {name: engine.prefName,
-                 title: this._collectionTitle(engine),
-                 engines: [engine.name],
-                 enabled: true,
-                 sizeLabel: retrievingLabel};
-      this._collections.push(obj);
-      this._byname[engine.prefName] = obj;
-    }
-  },
-
-  _collectionTitle: function _collectionTitle(engine) {
-    try {
-      return gSyncQuota.bundle.getString(
-        "collection." + engine.prefName + ".label");
-    } catch (ex) {
-      return engine.Name;
-    }
-  },
-
-  /*
-   * Process the quota information as returned by info/collection_usage.
-   */
-  displayUsageData: function displayUsageData(data) {
-    for each (let coll in this._collections) {
-      coll.size = 0;
-      // If we couldn't retrieve any data, just blank out the label.
-      if (!data) {
-        coll.sizeLabel = "";
-        continue;
-      }
-
-      for each (let engineName in coll.engines)
-        coll.size += data[engineName] || 0;
-      let sizeLabel = "";
-      sizeLabel = gSyncQuota.bundle.getFormattedString(
-        "quota.sizeValueUnit.label", gSyncQuota.convertKB(coll.size));
-      coll.sizeLabel = sizeLabel;
-    }
-    let sizeColumn = this.treeBox.columns.getNamedColumn("size");
-    this.treeBox.invalidateColumn(sizeColumn);
-  },
-
-  /*
-   * Handle click events on the tree.
-   */
-  onTreeClick: function onTreeClick(event) {
-    if (event.button == 2)
-      return;
-
-    let row = {}, col = {};
-    this.treeBox.getCellAt(event.clientX, event.clientY, row, col, {});
-    if (col.value && col.value.id == "enabled")
-      this.toggle(row.value);
-  },
-
-  /*
-   * Toggle enabled state of an engine.
-   */
-  toggle: function toggle(row) {
-    // Update the tree
-    let collection = this._collections[row];
-    collection.enabled = !collection.enabled;
-    this.treeBox.invalidateRow(row);
-
-    // Display which ones will be removed 
-    let freeup = 0;
-    let toremove = [];
-    for each (collection in this._collections) {
-      if (collection.enabled)
-        continue;
-      toremove.push(collection.name);
-      freeup += collection.size;
-    }
-
-    let caption = document.getElementById("treeCaption");
-    if (!toremove.length) {
-      caption.className = "";
-      caption.firstChild.nodeValue = gSyncQuota.bundle.getString(
-        "quota.treeCaption.label");
-      return;
-    }
-
-    toremove = [this._byname[coll].title for each (coll in toremove)];
-    toremove = toremove.join(gSyncQuota.bundle.getString("quota.list.separator"));
-    caption.firstChild.nodeValue = gSyncQuota.bundle.getFormattedString(
-      "quota.removal.label", [toremove]);
-    if (freeup)
-      caption.firstChild.nodeValue += gSyncQuota.bundle.getFormattedString(
-        "quota.freeup.label", gSyncQuota.convertKB(freeup));
-    caption.className = "captionWarning";
-  },
-
-  /*
-   * Return a list of engines (or rather their pref names) that should be
-   * disabled.
-   */
-  getEnginesToDisable: function getEnginesToDisable() {
-    return [coll.name for each (coll in this._collections) if (!coll.enabled)];
-  },
-
-  // nsITreeView
-
-  get rowCount() {
-    return this._collections.length;
-  },
-
-  getRowProperties: function(index) { return ""; },
-  getCellProperties: function(row, col) { return ""; },
-  getColumnProperties: function(col) { return ""; },
-  isContainer: function(index) { return false; },
-  isContainerOpen: function(index) { return false; },
-  isContainerEmpty: function(index) { return false; },
-  isSeparator: function(index) { return false; },
-  isSorted: function() { return false; },
-  canDrop: function(index, orientation, dataTransfer) { return false; },
-  drop: function(row, orientation, dataTransfer) {},
-  getParentIndex: function(rowIndex) {},
-  hasNextSibling: function(rowIndex, afterIndex) { return false; },
-  getLevel: function(index) { return 0; },
-  getImageSrc: function(row, col) {},
-
-  getCellValue: function(row, col) {
-    return this._collections[row].enabled;
-  },
-
-  getCellText: function getCellText(row, col) {
-    let collection = this._collections[row];
-    switch (col.id) {
-      case "collection":
-        return collection.title;
-      case "size":
-        return collection.sizeLabel;
-      default:
-        return "";
-    }
-  },
-
-  setTree: function setTree(tree) {
-    this.treeBox = tree;
-  },
-
-  toggleOpenState: function(index) {},
-  cycleHeader: function(col) {},
-  selectionChanged: function() {},
-  cycleCell: function(row, col) {},
-  isEditable: function(row, col) { return false; },
-  isSelectable: function (row, col) { return false; },
-  setCellValue: function(row, col, value) {},
-  setCellText: function(row, col, value) {},
-  performAction: function(action) {},
-  performActionOnRow: function(action, row) {},
-  performActionOnCell: function(action, row, col) {}
-
-};
deleted file mode 100644
--- a/browser/base/content/sync/quota.xul
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.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/. -->
-
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncQuota.css"?>
-
-<!DOCTYPE dialog [
-<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
-<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
-<!ENTITY % syncQuotaDTD SYSTEM "chrome://browser/locale/syncQuota.dtd">
-%brandDTD;
-%syncBrandDTD;
-%syncQuotaDTD;
-]>
-<dialog id="quotaDialog"
-        windowtype="Sync:ViewQuota"
-        persist="screenX screenY width height"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
-        onload="gSyncQuota.init()"
-        buttons="accept,cancel"
-        title="&quota.dialogTitle.label;"
-        ondialogcancel="return gSyncQuota.onCancel();"
-        ondialogaccept="return gSyncQuota.onAccept();">
-
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/quota.js"/>
-
-  <stringbundleset id="stringbundleset">
-    <stringbundle id="quotaStrings"
-                  src="chrome://browser/locale/syncQuota.properties"/>
-  </stringbundleset>
-
-  <vbox flex="1">
-    <label id="usageLabel"
-           value="&quota.retrievingInfo.label;"/>
-    <separator/>
-    <tree id="usageTree"
-          seltype="single"
-          hidecolumnpicker="true"
-          onclick="gUsageTreeView.onTreeClick(event);"
-          flex="1">
-      <treecols>
-        <treecol id="enabled"
-                 type="checkbox"
-                 fixed="true"/>
-        <splitter class="tree-splitter"/>
-        <treecol id="collection"
-                 label="&quota.typeColumn.label;"
-                 flex="1"/>
-        <splitter class="tree-splitter"/>
-        <treecol id="size"
-                 label="&quota.sizeColumn.label;"
-                 flex="1"/>
-      </treecols>
-      <treechildren flex="1"/>
-    </tree>
-    <separator/>
-    <description id="treeCaption"> </description>
-  </vbox>
-
-</dialog>
deleted file mode 100644
--- a/browser/base/content/sync/setup.js
+++ /dev/null
@@ -1,1067 +0,0 @@
-// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-const Ci = Components.interfaces;
-const Cc = Components.classes;
-const Cr = Components.results;
-const Cu = Components.utils;
-
-// page consts
-
-const PAIR_PAGE                     = 0;
-const INTRO_PAGE                    = 1;
-const NEW_ACCOUNT_START_PAGE        = 2;
-const EXISTING_ACCOUNT_CONNECT_PAGE = 3;
-const EXISTING_ACCOUNT_LOGIN_PAGE   = 4;
-const OPTIONS_PAGE                  = 5;
-const OPTIONS_CONFIRM_PAGE          = 6;
-
-// Broader than we'd like, but after this changed from api-secure.recaptcha.net
-// we had no choice. At least we only do this for the duration of setup.
-// See discussion in Bugs 508112 and 653307.
-const RECAPTCHA_DOMAIN = "https://www.google.com";
-
-const PIN_PART_LENGTH = 4;
-
-Cu.import("resource://services-sync/main.js");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/PlacesUtils.jsm");
-Cu.import("resource://gre/modules/PluralForm.jsm");
-
-
-function setVisibility(element, visible) {
-  element.style.visibility = visible ? "visible" : "hidden";
-}
-
-var gSyncSetup = {
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
-                                         Ci.nsIWebProgressListener,
-                                         Ci.nsISupportsWeakReference]),
-
-  captchaBrowser: null,
-  wizard: null,
-  _disabledSites: [],
-
-  status: {
-    password: false,
-    email: false,
-    server: false
-  },
-
-  get _remoteSites() [Weave.Service.serverURL, RECAPTCHA_DOMAIN],
-
-  get _usingMainServers() {
-    if (this._settingUpNew)
-      return document.getElementById("server").selectedIndex == 0;
-    return document.getElementById("existingServer").selectedIndex == 0;
-  },
-
-  init: function () {
-    let obs = [
-      ["weave:service:change-passphrase", "onResetPassphrase"],
-      ["weave:service:login:start",       "onLoginStart"],
-      ["weave:service:login:error",       "onLoginEnd"],
-      ["weave:service:login:finish",      "onLoginEnd"]];
-
-    // Add the observers now and remove them on unload
-    let self = this;
-    let addRem = function(add) {
-      obs.forEach(function([topic, func]) {
-        //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
-        //        of `this`. Fix in a followup. (bug 583347)
-        if (add)
-          Weave.Svc.Obs.add(topic, self[func], self);
-        else
-          Weave.Svc.Obs.remove(topic, self[func], self);
-      });
-    };
-    addRem(true);
-    window.addEventListener("unload", function() addRem(false), false);
-
-    window.setTimeout(function () {
-      // Force Service to be loaded so that engines are registered.
-      // See Bug 670082.
-      Weave.Service;
-    }, 0);
-
-    this.captchaBrowser = document.getElementById("captcha");
-
-    this.wizardType = null;
-    if (window.arguments && window.arguments[0]) {
-      this.wizardType = window.arguments[0];
-    }
-    switch (this.wizardType) {
-      case null:
-        this.wizard.pageIndex = INTRO_PAGE;
-        // Fall through!
-      case "pair":
-        this.captchaBrowser.addProgressListener(this);
-        Weave.Svc.Prefs.set("firstSync", "notReady");
-        break;
-      case "reset":
-        this._resettingSync = true;
-        this.wizard.pageIndex = OPTIONS_PAGE;
-        break;
-    }
-
-    this.wizard.getButton("extra1").label =
-      this._stringBundle.GetStringFromName("button.syncOptions.label");
-
-    // Remember these values because the options pages change them temporarily.
-    this._nextButtonLabel = this.wizard.getButton("next").label;
-    this._nextButtonAccesskey = this.wizard.getButton("next")
-                                           .getAttribute("accesskey");
-    this._backButtonLabel = this.wizard.getButton("back").label;
-    this._backButtonAccesskey = this.wizard.getButton("back")
-                                           .getAttribute("accesskey");
-  },
-
-  startNewAccountSetup: function () {
-    if (!Weave.Utils.ensureMPUnlocked())
-      return false;
-    this._settingUpNew = true;
-    this.wizard.pageIndex = NEW_ACCOUNT_START_PAGE;
-  },
-
-  useExistingAccount: function () {
-    if (!Weave.Utils.ensureMPUnlocked())
-      return false;
-    this._settingUpNew = false;
-    if (this.wizardType == "pair") {
-      // We're already pairing, so there's no point in pairing again.
-      // Go straight to the manual login page.
-      this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
-    } else {
-      this.wizard.pageIndex = EXISTING_ACCOUNT_CONNECT_PAGE;
-    }
-  },
-
-  resetPassphrase: function resetPassphrase() {
-    // Apply the existing form fields so that
-    // Weave.Service.changePassphrase() has the necessary credentials.
-    Weave.Service.identity.account = document.getElementById("existingAccountName").value;
-    Weave.Service.identity.basicPassword = document.getElementById("existingPassword").value;
-
-    // Generate a new passphrase so that Weave.Service.login() will
-    // actually do something.
-    let passphrase = Weave.Utils.generatePassphrase();
-    Weave.Service.identity.syncKey = passphrase;
-
-    // Only open the dialog if username + password are actually correct.
-    Weave.Service.login();
-    if ([Weave.LOGIN_FAILED_INVALID_PASSPHRASE,
-         Weave.LOGIN_FAILED_NO_PASSPHRASE,
-         Weave.LOGIN_SUCCEEDED].indexOf(Weave.Status.login) == -1) {
-      return;
-    }
-
-    // Hide any errors about the passphrase, we know it's not right.
-    let feedback = document.getElementById("existingPassphraseFeedbackRow");
-    feedback.hidden = true;
-    let el = document.getElementById("existingPassphrase");
-    el.value = Weave.Utils.hyphenatePassphrase(passphrase);
-
-    // changePassphrase() will sync, make sure we set the "firstSync" pref
-    // according to the user's pref.
-    Weave.Svc.Prefs.reset("firstSync");
-    this.setupInitialSync();
-    gSyncUtils.resetPassphrase(true);
-  },
-
-  onResetPassphrase: function () {
-    document.getElementById("existingPassphrase").value =
-      Weave.Utils.hyphenatePassphrase(Weave.Service.identity.syncKey);
-    this.checkFields();
-    this.wizard.advance();
-  },
-
-  onLoginStart: function () {
-    this.toggleLoginFeedback(false);
-  },
-
-  onLoginEnd: function () {
-    this.toggleLoginFeedback(true);
-  },
-
-  sendCredentialsAfterSync: function () {
-    let send = function() {
-      Services.obs.removeObserver("weave:service:sync:finish", send);
-      Services.obs.removeObserver("weave:service:sync:error", send);
-      let credentials = {account:   Weave.Service.identity.account,
-                         password:  Weave.Service.identity.basicPassword,
-                         synckey:   Weave.Service.identity.syncKey,
-                         serverURL: Weave.Service.serverURL};
-      this._jpakeclient.sendAndComplete(credentials);
-    }.bind(this);
-    Services.obs.addObserver("weave:service:sync:finish", send, false);
-    Services.obs.addObserver("weave:service:sync:error", send, false);
-  },
-
-  toggleLoginFeedback: function (stop) {
-    document.getElementById("login-throbber").hidden = stop;
-    let password = document.getElementById("existingPasswordFeedbackRow");
-    let server = document.getElementById("existingServerFeedbackRow");
-    let passphrase = document.getElementById("existingPassphraseFeedbackRow");
-
-    if (!stop || (Weave.Status.login == Weave.LOGIN_SUCCEEDED)) {
-      password.hidden = server.hidden = passphrase.hidden = true;
-      return;
-    }
-
-    let feedback;
-    switch (Weave.Status.login) {
-      case Weave.LOGIN_FAILED_NETWORK_ERROR:
-      case Weave.LOGIN_FAILED_SERVER_ERROR:
-        feedback = server;
-        break;
-      case Weave.LOGIN_FAILED_LOGIN_REJECTED:
-      case Weave.LOGIN_FAILED_NO_USERNAME:
-      case Weave.LOGIN_FAILED_NO_PASSWORD:
-        feedback = password;
-        break;
-      case Weave.LOGIN_FAILED_INVALID_PASSPHRASE:
-        feedback = passphrase;
-        break;
-    }
-    this._setFeedbackMessage(feedback, false, Weave.Status.login);
-  },
-
-  setupInitialSync: function () {
-    let action = document.getElementById("mergeChoiceRadio").selectedItem.id;
-    switch (action) {
-      case "resetClient":
-        // if we're not resetting sync, we don't need to explicitly
-        // call resetClient
-        if (!this._resettingSync)
-          return;
-        // otherwise, fall through
-      case "wipeClient":
-      case "wipeRemote":
-        Weave.Svc.Prefs.set("firstSync", action);
-        break;
-    }
-  },
-
-  // fun with validation!
-  checkFields: function () {
-    this.wizard.canAdvance = this.readyToAdvance();
-  },
-
-  readyToAdvance: function () {
-    switch (this.wizard.pageIndex) {
-      case INTRO_PAGE:
-        return false;
-      case NEW_ACCOUNT_START_PAGE:
-        for (let i in this.status) {
-          if (!this.status[i])
-            return false;
-        }
-        if (this._usingMainServers)
-          return document.getElementById("tos").checked;
-
-        return true;
-      case EXISTING_ACCOUNT_LOGIN_PAGE:
-        let hasUser = document.getElementById("existingAccountName").value != "";
-        let hasPass = document.getElementById("existingPassword").value != "";
-        let hasKey = document.getElementById("existingPassphrase").value != "";
-
-        if (hasUser && hasPass && hasKey) {
-          if (this._usingMainServers)
-            return true;
-
-          if (this._validateServer(document.getElementById("existingServer"))) {
-            return true;
-          }
-        }
-        return false;
-    }
-    // Default, e.g. wizard's special page -1 etc.
-    return true;
-  },
-
-  onPINInput: function onPINInput(textbox) {
-    if (textbox && textbox.value.length == PIN_PART_LENGTH) {
-      this.nextFocusEl[textbox.id].focus();
-    }
-    this.wizard.canAdvance = (this.pin1.value.length == PIN_PART_LENGTH &&
-                              this.pin2.value.length == PIN_PART_LENGTH &&
-                              this.pin3.value.length == PIN_PART_LENGTH);
-  },
-
-  onEmailInput: function () {
-    // Check account validity when the user stops typing for 1 second.
-    if (this._checkAccountTimer)
-      window.clearTimeout(this._checkAccountTimer);
-    this._checkAccountTimer = window.setTimeout(function () {
-      gSyncSetup.checkAccount();
-    }, 1000);
-  },
-
-  checkAccount: function() {
-    delete this._checkAccountTimer;
-    let value = Weave.Utils.normalizeAccount(
-      document.getElementById("weaveEmail").value);
-    if (!value) {
-      this.status.email = false;
-      this.checkFields();
-      return;
-    }
-
-    let re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-    let feedback = document.getElementById("emailFeedbackRow");
-    let valid = re.test(value);
-
-    let str = "";
-    if (!valid) {
-      str = "invalidEmail.label";
-    } else {
-      let availCheck = Weave.Service.checkAccount(value);
-      valid = availCheck == "available";
-      if (!valid) {
-        if (availCheck == "notAvailable")
-          str = "usernameNotAvailable.label";
-        else
-          str = availCheck;
-      }
-    }
-
-    this._setFeedbackMessage(feedback, valid, str);
-    this.status.email = valid;
-    if (valid)
-      Weave.Service.identity.account = value;
-    this.checkFields();
-  },
-
-  onPasswordChange: function () {
-    let password = document.getElementById("weavePassword");
-    let pwconfirm = document.getElementById("weavePasswordConfirm");
-    let [valid, errorString] = gSyncUtils.validatePassword(password, pwconfirm);
-
-    let feedback = document.getElementById("passwordFeedbackRow");
-    this._setFeedback(feedback, valid, errorString);
-
-    this.status.password = valid;
-    this.checkFields();
-  },
-
-  onPageShow: function() {
-    switch (this.wizard.pageIndex) {
-      case PAIR_PAGE:
-        this.wizard.getButton("back").hidden = true;
-        this.wizard.getButton("extra1").hidden = true;
-        this.onPINInput();
-        this.pin1.focus();
-        break;
-      case INTRO_PAGE:
-        // We may not need the captcha in the Existing Account branch of the
-        // wizard. However, we want to preload it to avoid any flickering while
-        // the Create Account page is shown.
-        this.loadCaptcha();
-        this.wizard.getButton("next").hidden = true;
-        this.wizard.getButton("back").hidden = true;
-        this.wizard.getButton("extra1").hidden = true;
-        this.checkFields();
-        break;
-      case NEW_ACCOUNT_START_PAGE:
-        this.wizard.getButton("extra1").hidden = false;
-        this.wizard.getButton("next").hidden = false;
-        this.wizard.getButton("back").hidden = false;
-        this.onServerCommand();
-        this.wizard.canRewind = true;
-        this.checkFields();
-        break;
-      case EXISTING_ACCOUNT_CONNECT_PAGE:
-        Weave.Svc.Prefs.set("firstSync", "existingAccount");
-        this.wizard.getButton("next").hidden = false;
-        this.wizard.getButton("back").hidden = false;
-        this.wizard.getButton("extra1").hidden = false;
-        this.wizard.canAdvance = false;
-        this.wizard.canRewind = true;
-        this.startEasySetup();
-        break;
-      case EXISTING_ACCOUNT_LOGIN_PAGE:
-        this.wizard.getButton("next").hidden = false;
-        this.wizard.getButton("back").hidden = false;
-        this.wizard.getButton("extra1").hidden = false;
-        this.wizard.canRewind = true;
-        this.checkFields();
-        break;
-      case OPTIONS_PAGE:
-        this.wizard.canRewind = false;
-        this.wizard.canAdvance = true;
-        if (!this._resettingSync) {
-          this.wizard.getButton("next").label =
-            this._stringBundle.GetStringFromName("button.syncOptionsDone.label");
-          this.wizard.getButton("next").removeAttribute("accesskey");
-        }
-        this.wizard.getButton("next").hidden = false;
-        this.wizard.getButton("back").hidden = true;
-        this.wizard.getButton("cancel").hidden = !this._resettingSync;
-        this.wizard.getButton("extra1").hidden = true;
-        document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
-        document.getElementById("syncOptions").collapsed = this._resettingSync;
-        document.getElementById("mergeOptions").collapsed = this._settingUpNew;
-        break;
-      case OPTIONS_CONFIRM_PAGE:
-        this.wizard.canRewind = true;
-        this.wizard.canAdvance = true;
-        this.wizard.getButton("back").label =
-          this._stringBundle.GetStringFromName("button.syncOptionsCancel.label");
-        this.wizard.getButton("back").removeAttribute("accesskey");
-        this.wizard.getButton("back").hidden = this._resettingSync;
-        this.wizard.getButton("next").hidden = false;
-        this.wizard.getButton("finish").hidden = true;
-        break;
-    }
-  },
-
-  onWizardAdvance: function () {
-    // Check pageIndex so we don't prompt before the Sync setup wizard appears.
-    // This is a fallback in case the Master Password gets locked mid-wizard.
-    if ((this.wizard.pageIndex >= 0) &&
-        !Weave.Utils.ensureMPUnlocked()) {
-      return false;
-    }
-
-    switch (this.wizard.pageIndex) {
-      case PAIR_PAGE:
-        this.startPairing();
-        return false;
-      case NEW_ACCOUNT_START_PAGE:
-        // If the user selects Next (e.g. by hitting enter) when we haven't
-        // executed the delayed checks yet, execute them immediately.
-        if (this._checkAccountTimer) {
-          this.checkAccount();
-        }
-        if (this._checkServerTimer) {
-          this.checkServer();
-        }
-        if (!this.wizard.canAdvance) {
-          return false;
-        }
-
-        let doc = this.captchaBrowser.contentDocument;
-        let getField = function getField(field) {
-          let node = doc.getElementById("recaptcha_" + field + "_field");
-          return node && node.value;
-        };
-
-        // Display throbber
-        let feedback = document.getElementById("captchaFeedback");
-        let image = feedback.firstChild;
-        let label = image.nextSibling;
-        image.setAttribute("status", "active");
-        label.value = this._stringBundle.GetStringFromName("verifying.label");
-        setVisibility(feedback, true);
-
-        let password = document.getElementById("weavePassword").value;
-        let email = Weave.Utils.normalizeAccount(
-          document.getElementById("weaveEmail").value);
-        let challenge = getField("challenge");
-        let response = getField("response");
-
-        let error = Weave.Service.createAccount(email, password,
-                                                challenge, response);
-
-        if (error == null) {
-          Weave.Service.identity.account = email;
-          Weave.Service.identity.basicPassword = password;
-          Weave.Service.identity.syncKey = Weave.Utils.generatePassphrase();
-          this._handleNoScript(false);
-          Weave.Svc.Prefs.set("firstSync", "newAccount");
-          this.wizardFinish();
-          return false;
-        }
-
-        image.setAttribute("status", "error");
-        label.value = Weave.Utils.getErrorString(error);
-        return false;
-      case EXISTING_ACCOUNT_LOGIN_PAGE:
-        Weave.Service.identity.account = Weave.Utils.normalizeAccount(
-          document.getElementById("existingAccountName").value);
-        Weave.Service.identity.basicPassword =
-          document.getElementById("existingPassword").value;
-        let pp = document.getElementById("existingPassphrase").value;
-        Weave.Service.identity.syncKey = Weave.Utils.normalizePassphrase(pp);
-        if (Weave.Service.login()) {
-          this.wizardFinish();
-        }
-        return false;
-      case OPTIONS_PAGE:
-        let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
-        // No confirmation needed on new account setup or merge option
-        // with existing account.
-        if (this._settingUpNew || (!this._resettingSync && desc == 0))
-          return this.returnFromOptions();
-        return this._handleChoice();
-      case OPTIONS_CONFIRM_PAGE:
-        if (this._resettingSync) {
-          this.wizardFinish();
-          return false;
-        }
-        return this.returnFromOptions();
-    }
-    return true;
-  },
-
-  onWizardBack: function () {
-    switch (this.wizard.pageIndex) {
-      case NEW_ACCOUNT_START_PAGE:
-      case EXISTING_ACCOUNT_LOGIN_PAGE:
-        this.wizard.pageIndex = INTRO_PAGE;
-        return false;
-      case EXISTING_ACCOUNT_CONNECT_PAGE:
-        this.abortEasySetup();
-        this.wizard.pageIndex = INTRO_PAGE;
-        return false;
-      case EXISTING_ACCOUNT_LOGIN_PAGE:
-        // If we were already pairing on entry, we went straight to the manual
-        // login page. If subsequently we go back, return to the page that lets
-        // us choose whether we already have an account.
-        if (this.wizardType == "pair") {
-          this.wizard.pageIndex = INTRO_PAGE;
-          return false;
-        }
-        return true;
-      case OPTIONS_CONFIRM_PAGE:
-        // Backing up from the confirmation page = resetting first sync to merge.
-        document.getElementById("mergeChoiceRadio").selectedIndex = 0;
-        return this.returnFromOptions();
-    }
-    return true;
-  },
-
-  wizardFinish: function () {
-    this.setupInitialSync();
-
-    if (this.wizardType == "pair") {
-      this.completePairing();
-    }
-
-    if (!this._resettingSync) {
-      function isChecked(element) {
-        return document.getElementById(element).hasAttribute("checked");
-      }
-
-      let prefs = ["engine.bookmarks", "engine.passwords", "engine.history",
-                   "engine.tabs", "engine.prefs", "engine.addons"];
-      for (let i = 0;i < prefs.length;i++) {
-        Weave.Svc.Prefs.set(prefs[i], isChecked(prefs[i]));
-      }
-      this._handleNoScript(false);
-      if (Weave.Svc.Prefs.get("firstSync", "") == "notReady")
-        Weave.Svc.Prefs.reset("firstSync");
-
-      Weave.Service.persistLogin();
-      Weave.Svc.Obs.notify("weave:service:setup-complete");
-
-      gSyncUtils.openFirstSyncProgressPage();
-    }
-    Weave.Utils.nextTick(Weave.Service.sync, Weave.Service);
-    window.close();
-  },
-
-  onWizardCancel: function () {
-    if (this._resettingSync)
-      return;
-
-    this.abortEasySetup();
-    this._handleNoScript(false);
-    Weave.Service.startOver();
-  },
-
-  onSyncOptions: function () {
-    this._beforeOptionsPage = this.wizard.pageIndex;
-    this.wizard.pageIndex = OPTIONS_PAGE;
-  },
-
-  returnFromOptions: function() {
-    this.wizard.getButton("next").label = this._nextButtonLabel;
-    this.wizard.getButton("next").setAttribute("accesskey",
-                                               this._nextButtonAccesskey);
-    this.wizard.getButton("back").label = this._backButtonLabel;
-    this.wizard.getButton("back").setAttribute("accesskey",
-                                               this._backButtonAccesskey);
-    this.wizard.getButton("cancel").hidden = false;
-    this.wizard.getButton("extra1").hidden = false;
-    this.wizard.pageIndex = this._beforeOptionsPage;
-    return false;
-  },
-
-  startPairing: function startPairing() {
-    this.pairDeviceErrorRow.hidden = true;
-    // When onAbort is called, Weave may already be gone.
-    const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
-
-    let self = this;
-    let jpakeclient = this._jpakeclient = new Weave.JPAKEClient({
-      onPaired: function onPaired() {
-        self.wizard.pageIndex = INTRO_PAGE;
-      },
-      onComplete: function onComplete() {
-        // This method will never be called since SendCredentialsController
-        // will take over after the wizard completes.
-      },
-      onAbort: function onAbort(error) {
-        delete self._jpakeclient;
-
-        // Aborted by user, ignore. The window is almost certainly going to close
-        // or is already closed.
-        if (error == JPAKE_ERROR_USERABORT) {
-          return;
-        }
-
-        self.pairDeviceErrorRow.hidden = false;
-        self.pairDeviceThrobber.hidden = true;
-        self.pin1.value = self.pin2.value = self.pin3.value = "";
-        self.pin1.disabled = self.pin2.disabled = self.pin3.disabled = false;
-        if (self.wizard.pageIndex == PAIR_PAGE) {
-          self.pin1.focus();
-        }
-      }
-    });
-    this.pairDeviceThrobber.hidden = false;
-    this.pin1.disabled = this.pin2.disabled = this.pin3.disabled = true;
-    this.wizard.canAdvance = false;
-
-    let pin = this.pin1.value + this.pin2.value + this.pin3.value;
-    let expectDelay = true;
-    jpakeclient.pairWithPIN(pin, expectDelay);
-  },
-
-  completePairing: function completePairing() {
-    if (!this._jpakeclient) {
-      // The channel was aborted while we were setting up the account
-      // locally. XXX TODO should we do anything here, e.g. tell
-      // the user on the last wizard page that it's ok, they just
-      // have to pair again?
-      return;
-    }
-    let controller = new Weave.SendCredentialsController(this._jpakeclient,
-                                                         Weave.Service);
-    this._jpakeclient.controller = controller;
-  },
-
-  startEasySetup: function () {
-    // Don't do anything if we have a client already (e.g. we went to
-    // Sync Options and just came back).
-    if (this._jpakeclient)
-      return;
-
-    // When onAbort is called, Weave may already be gone
-    const JPAKE_ERROR_USERABORT = Weave.JPAKE_ERROR_USERABORT;
-
-    let self = this;
-    this._jpakeclient = new Weave.JPAKEClient({
-      displayPIN: function displayPIN(pin) {
-        document.getElementById("easySetupPIN1").value = pin.slice(0, 4);
-        document.getElementById("easySetupPIN2").value = pin.slice(4, 8);
-        document.getElementById("easySetupPIN3").value = pin.slice(8);
-      },
-
-      onPairingStart: function onPairingStart() {},
-
-      onComplete: function onComplete(credentials) {
-        Weave.Service.identity.account = credentials.account;
-        Weave.Service.identity.basicPassword = credentials.password;
-        Weave.Service.identity.syncKey = credentials.synckey;
-        Weave.Service.serverURL = credentials.serverURL;
-        gSyncSetup.wizardFinish();
-      },
-
-      onAbort: function onAbort(error) {
-        delete self._jpakeclient;
-
-        // Ignore if wizard is aborted.
-        if (error == JPAKE_ERROR_USERABORT)
-          return;
-
-        // Automatically go to manual setup if we couldn't acquire a channel.
-        if (error == Weave.JPAKE_ERROR_CHANNEL) {
-          self.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
-          return;
-        }
-
-        // Restart on all other errors.
-        self.startEasySetup();
-      }
-    });
-    this._jpakeclient.receiveNoPIN();
-  },
-
-  abortEasySetup: function () {
-    document.getElementById("easySetupPIN1").value = "";
-    document.getElementById("easySetupPIN2").value = "";
-    document.getElementById("easySetupPIN3").value = "";
-    if (!this._jpakeclient)
-      return;
-
-    this._jpakeclient.abort();
-    delete this._jpakeclient;
-  },
-
-  manualSetup: function () {
-    this.abortEasySetup();
-    this.wizard.pageIndex = EXISTING_ACCOUNT_LOGIN_PAGE;
-  },
-
-  // _handleNoScript is needed because it blocks the captcha. So we temporarily
-  // allow the necessary sites so that we can verify the user is in fact a human.
-  // This was done with the help of Giorgio (NoScript author). See bug 508112.
-  _handleNoScript: function (addExceptions) {
-    // if NoScript isn't installed, or is disabled, bail out.
-    let ns = Cc["@maone.net/noscript-service;1"];
-    if (ns == null)
-      return;
-
-    ns = ns.getService().wrappedJSObject;
-    if (addExceptions) {
-      this._remoteSites.forEach(function(site) {
-        site = ns.getSite(site);
-        if (!ns.isJSEnabled(site)) {
-          this._disabledSites.push(site); // save status
-          ns.setJSEnabled(site, true); // allow site
-        }
-      }, this);
-    }
-    else {
-      this._disabledSites.forEach(function(site) {
-        ns.setJSEnabled(site, false);
-      });
-      this._disabledSites = [];
-    }
-  },
-
-  onExistingServerCommand: function () {
-    let control = document.getElementById("existingServer");
-    if (control.selectedIndex == 0) {
-      control.removeAttribute("editable");
-      Weave.Svc.Prefs.reset("serverURL");
-    } else {
-      control.setAttribute("editable", "true");
-      // Force a style flush to ensure that the binding is attached.
-      control.clientTop;
-      control.value = "";
-      control.inputField.focus();
-    }
-    document.getElementById("existingServerFeedbackRow").hidden = true;
-    this.checkFields();
-  },
-
-  onExistingServerInput: function () {
-    // Check custom server validity when the user stops typing for 1 second.
-    if (this._existingServerTimer)
-      window.clearTimeout(this._existingServerTimer);
-    this._existingServerTimer = window.setTimeout(function () {
-      gSyncSetup.checkFields();
-    }, 1000);
-  },
-
-  onServerCommand: function () {
-    setVisibility(document.getElementById("TOSRow"), this._usingMainServers);
-    let control = document.getElementById("server");
-    if (!this._usingMainServers) {
-      control.setAttribute("editable", "true");
-      // Force a style flush to ensure that the binding is attached.
-      control.clientTop;
-      control.value = "";
-      control.inputField.focus();
-      // checkServer() will call checkAccount() and checkFields().
-      this.checkServer();
-      return;
-    }
-    control.removeAttribute("editable");
-    Weave.Svc.Prefs.reset("serverURL");
-    if (this._settingUpNew) {
-      this.loadCaptcha();
-    }
-    this.checkAccount();
-    this.status.server = true;
-    document.getElementById("serverFeedbackRow").hidden = true;
-    this.checkFields();
-  },
-
-  onServerInput: function () {
-    // Check custom server validity when the user stops typing for 1 second.
-    if (this._checkServerTimer)
-      window.clearTimeout(this._checkServerTimer);
-    this._checkServerTimer = window.setTimeout(function () {
-      gSyncSetup.checkServer();
-    }, 1000);
-  },
-
-  checkServer: function () {
-    delete this._checkServerTimer;
-    let el = document.getElementById("server");
-    let valid = false;
-    let feedback = document.getElementById("serverFeedbackRow");
-    let str = "";
-    if (el.value) {
-      valid = this._validateServer(el);
-      let str = valid ? "" : "serverInvalid.label";
-      this._setFeedbackMessage(feedback, valid, str);
-    }
-    else
-      this._setFeedbackMessage(feedback, true);
-
-    // Recheck account against the new server.
-    if (valid)
-      this.checkAccount();
-
-    this.status.server = valid;
-    this.checkFields();
-  },
-
-  _validateServer: function (element) {
-    let valid = false;
-    let val = element.value;
-    if (!val)
-      return false;
-
-    let uri = Weave.Utils.makeURI(val);
-
-    if (!uri)
-      uri = Weave.Utils.makeURI("https://" + val);
-
-    if (uri && this._settingUpNew) {
-      function isValid(uri) {
-        Weave.Service.serverURL = uri.spec;
-        let check = Weave.Service.checkAccount("a");
-        return (check == "available" || check == "notAvailable");
-      }
-
-      if (uri.schemeIs("http")) {
-        uri.scheme = "https";
-        if (isValid(uri))
-          valid = true;
-        else
-          // setting the scheme back to http
-          uri.scheme = "http";
-      }
-      if (!valid)
-        valid = isValid(uri);
-
-      if (valid) {
-        this.loadCaptcha();
-      }
-    }
-    else if (uri) {
-      valid = true;
-      Weave.Service.serverURL = uri.spec;
-    }
-
-    if (valid)
-      element.value = Weave.Service.serverURL;
-    else
-      Weave.Svc.Prefs.reset("serverURL");
-
-    return valid;
-  },
-
-  _handleChoice: function () {
-    let desc = document.getElementById("mergeChoiceRadio").selectedIndex;
-    document.getElementById("chosenActionDeck").selectedIndex = desc;
-    switch (desc) {
-      case 1:
-        if (this._case1Setup)
-          break;
-
-        let places_db = PlacesUtils.history
-                                   .QueryInterface(Ci.nsPIPlacesDatabase)
-                                   .DBConnection;
-        if (Weave.Service.engineManager.get("history").enabled) {
-          let daysOfHistory = 0;
-          let stm = places_db.createStatement(
-            "SELECT ROUND(( " +
-              "strftime('%s','now','localtime','utc') - " +
-              "( " +
-                "SELECT visit_date FROM moz_historyvisits " +
-                "ORDER BY visit_date ASC LIMIT 1 " +
-                ")/1000000 " +
-              ")/86400) AS daysOfHistory ");
-
-          if (stm.step())
-            daysOfHistory = stm.getInt32(0);
-          // Support %S for historical reasons (see bug 600141)
-          document.getElementById("historyCount").value =
-            PluralForm.get(daysOfHistory,
-                           this._stringBundle.GetStringFromName("historyDaysCount.label"))
-                      .replace("%S", daysOfHistory)
-                      .replace("#1", daysOfHistory);
-        } else {
-          document.getElementById("historyCount").hidden = true;
-        }
-
-        if (Weave.Service.engineManager.get("bookmarks").enabled) {
-          let bookmarks = 0;
-          let stm = places_db.createStatement(
-            "SELECT count(*) AS bookmarks " +
-            "FROM moz_bookmarks b " +
-            "LEFT JOIN moz_bookmarks t ON " +
-            "b.parent = t.id WHERE b.type = 1 AND t.parent <> :tag");
-          stm.params.tag = PlacesUtils.tagsFolderId;
-          if (stm.executeStep())
-            bookmarks = stm.row.bookmarks;
-          // Support %S for historical reasons (see bug 600141)
-          document.getElementById("bookmarkCount").value =
-            PluralForm.get(bookmarks,
-                           this._stringBundle.GetStringFromName("bookmarksCount.label"))
-                      .replace("%S", bookmarks)
-                      .replace("#1", bookmarks);
-        } else {
-          document.getElementById("bookmarkCount").hidden = true;
-        }
-
-        if (Weave.Service.engineManager.get("passwords").enabled) {
-          let logins = Services.logins.getAllLogins({});
-          // Support %S for historical reasons (see bug 600141)
-          document.getElementById("passwordCount").value =
-            PluralForm.get(logins.length,
-                           this._stringBundle.GetStringFromName("passwordsCount.label"))
-                      .replace("%S", logins.length)
-                      .replace("#1", logins.length);
-        } else {
-          document.getElementById("passwordCount").hidden = true;
-        }
-
-        if (!Weave.Service.engineManager.get("prefs").enabled) {
-          document.getElementById("prefsWipe").hidden = true;
-        }
-
-        let addonsEngine = Weave.Service.engineManager.get("addons");
-        if (addonsEngine.enabled) {
-          let ids = addonsEngine._store.getAllIDs();
-          let blessedcount = 0;
-          for each (let i in ids) {
-            if (i) {
-              blessedcount++;
-            }
-          }
-          // bug 600141 does not apply, as this does not have to support existing strings
-          document.getElementById("addonCount").value =
-            PluralForm.get(blessedcount,
-                           this._stringBundle.GetStringFromName("addonsCount.label"))
-                      .replace("#1", blessedcount);
-        } else {
-          document.getElementById("addonCount").hidden = true;
-        }
-
-        this._case1Setup = true;
-        break;
-      case 2:
-        if (this._case2Setup)
-          break;
-        let count = 0;
-        function appendNode(label) {
-          let box = document.getElementById("clientList");
-          let node = document.createElement("label");
-          node.setAttribute("value", label);
-          node.setAttribute("class", "data indent");
-          box.appendChild(node);
-        }
-
-        for each (let name in Weave.Service.clientsEngine.stats.names) {
-          // Don't list the current client
-          if (name == Weave.Service.clientsEngine.localName)
-            continue;
-
-          // Only show the first several client names
-          if (++count <= 5)
-            appendNode(name);
-        }
-        if (count > 5) {
-          // Support %S for historical reasons (see bug 600141)
-          let label =
-            PluralForm.get(count - 5,
-                           this._stringBundle.GetStringFromName("additionalClientCount.label"))
-                      .replace("%S", count - 5)
-                      .replace("#1", count - 5);
-          appendNode(label);
-        }
-        this._case2Setup = true;
-        break;
-    }
-
-    return true;
-  },
-
-  // sets class and string on a feedback element
-  // if no property string is passed in, we clear label/style
-  _setFeedback: function (element, success, string) {
-    element.hidden = success || !string;
-    let classname = success ? "success" : "error";
-    let image = element.getElementsByAttribute("class", "statusIcon")[0];
-    image.setAttribute("status", classname);
-    let label = element.getElementsByAttribute("class", "status")[0];
-    label.value = string;
-  },
-
-  // shim
-  _setFeedbackMessage: function (element, success, string) {
-    let str = "";
-    if (string) {
-      try {
-        str = this._stringBundle.GetStringFromName(string);
-      } catch(e) {}
-
-      if (!str)
-        str = Weave.Utils.getErrorString(string);
-    }
-    this._setFeedback(element, success, str);
-  },
-
-  loadCaptcha: function loadCaptcha() {
-    let captchaURI = Weave.Service.miscAPI + "captcha_html";
-    // First check for NoScript and whitelist the right sites.
-    this._handleNoScript(true);
-    if (this.captchaBrowser.currentURI.spec != captchaURI) {
-      this.captchaBrowser.loadURI(captchaURI);
-    }
-  },
-
-  onStateChange: function(webProgress, request, stateFlags, status) {
-    // We're only looking for the end of the frame load
-    if ((stateFlags & Ci.nsIWebProgressListener.STATE_STOP) == 0)
-      return;
-    if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) == 0)
-      return;
-    if ((stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) == 0)
-      return;
-
-    // If we didn't find a captcha, assume it's not needed and don't show it.
-    let responseStatus = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
-    setVisibility(this.captchaBrowser, responseStatus != 404);
-    //XXX TODO we should really log any responseStatus other than 200
-  },
-  onProgressChange: function() {},
-  onStatusChange: function() {},
-  onSecurityChange: function() {},
-  onLocationChange: function () {}
-};
-
-// Define lazy getters for various XUL elements.
-//
-// onWizardAdvance() and onPageShow() are run before init(), so we'll even
-// define things that will almost certainly be used (like 'wizard') as a lazy
-// getter here.
-["wizard",
- "pin1",
- "pin2",
- "pin3",
- "pairDeviceErrorRow",
- "pairDeviceThrobber"].forEach(function (id) {
-  XPCOMUtils.defineLazyGetter(gSyncSetup, id, function() {
-    return document.getElementById(id);
-  });
-});
-XPCOMUtils.defineLazyGetter(gSyncSetup, "nextFocusEl", function () {
-  return {pin1: this.pin2,
-          pin2: this.pin3,
-          pin3: this.wizard.getButton("next")};
-});
-XPCOMUtils.defineLazyGetter(gSyncSetup, "_stringBundle", function() {
-  return Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
-});
deleted file mode 100644
--- a/browser/base/content/sync/setup.xul
+++ /dev/null
@@ -1,490 +0,0 @@
-<?xml version="1.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/. -->
-
-<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncSetup.css" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/syncCommon.css" type="text/css"?>
-
-<!DOCTYPE window [
-<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
-<!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
-<!ENTITY % syncSetupDTD SYSTEM "chrome://browser/locale/syncSetup.dtd">
-%brandDTD;
-%syncBrandDTD;
-%syncSetupDTD;
-]>
-<wizard id="wizard"
-        title="&accountSetupTitle.label;"
-        windowtype="Weave:AccountSetup"
-        persist="screenX screenY"
-        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
-        xmlns:html="http://www.w3.org/1999/xhtml"
-        onwizardnext="return gSyncSetup.onWizardAdvance()"
-        onwizardback="return gSyncSetup.onWizardBack()"
-        onwizardcancel="gSyncSetup.onWizardCancel()"
-        onload="gSyncSetup.init()">
-
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/setup.js"/>
-  <script type="application/javascript"
-          src="chrome://browser/content/sync/utils.js"/>
-  <script type="application/javascript"
-          src="chrome://browser/content/utilityOverlay.js"/>
-  <script type="application/javascript"
-          src="chrome://global/content/printUtils.js"/>
-
-  <wizardpage id="addDevicePage"
-              label="&pairDevice.title.label;"
-              onpageshow="gSyncSetup.onPageShow()">
-    <description>
-      &pairDevice.dialog.description.label;
-      <label class="text-link"
-             value="&addDevice.showMeHow.label;"
-             href="https://services.mozilla.com/sync/help/add-device"/>
-    </description>
-    <separator class="groove-thin"/>
-    <description>
-      &addDevice.dialog.enterCode.label;
-    </description>
-    <separator class="groove-thin"/>
-    <vbox align="center">
-      <textbox id="pin1"
-               class="pin"
-               oninput="gSyncSetup.onPINInput(this);"
-               onfocus="this.select();"
-               />
-      <textbox id="pin2"
-               class="pin"
-               oninput="gSyncSetup.onPINInput(this);"
-               onfocus="this.select();"
-               />
-      <textbox id="pin3"
-               class="pin"
-               oninput="gSyncSetup.onPINInput(this);"
-               onfocus="this.select();" 
-               />
-    </vbox>
-    <separator class="groove-thin"/>
-    <vbox id="pairDeviceThrobber" align="center" hidden="true">
-      <image/>
-    </vbox>
-    <hbox id="pairDeviceErrorRow" pack="center" hidden="true">
-      <image class="statusIcon" status="error"/>
-      <label class="status"
-             value="&addDevice.dialog.tryAgain.label;"/>
-    </hbox>
-  </wizardpage>
-
-  <wizardpage id="pickSetupType"
-              label="&syncBrand.fullName.label;"
-              onpageshow="gSyncSetup.onPageShow()">
-    <vbox align="center" flex="1">
-      <description style="padding: 0 7em;">
-        &setup.pickSetupType.description2;
-      </description>
-      <spacer flex="3"/>
-      <button id="newAccount"
-              class="accountChoiceButton"
-              label="&button.createNewAccount.label;"
-              oncommand="gSyncSetup.startNewAccountSetup()"
-              align="center"/>
-      <spacer flex="1"/>
-    </vbox>
-    <separator class="groove"/>
-    <vbox align="center" flex="1">
-      <spacer flex="1"/>
-      <button id="existingAccount"
-              class="accountChoiceButton"
-              label="&button.haveAccount.label;"
-              oncommand="gSyncSetup.useExistingAccount()"/>
-      <spacer flex="3"/>
-    </vbox>
-  </wizardpage>
-
-  <wizardpage label="&setup.newAccountDetailsPage.title.label;"
-              id="newAccountStart"
-              onextra1="gSyncSetup.onSyncOptions()"
-              onpageshow="gSyncSetup.onPageShow();">
-    <grid>
-      <columns>
-        <column/>
-        <column class="inputColumn" flex="1"/>
-      </columns>
-      <rows>
-        <row id="emailRow" align="center">
-          <label value="&setup.emailAddress.label;"
-                 accesskey="&setup.emailAddress.accesskey;"
-                 control="weaveEmail"/>
-          <textbox id="weaveEmail"
-                   oninput="gSyncSetup.onEmailInput()"/>
-        </row>
-        <row id="emailFeedbackRow" align="center" hidden="true">
-          <spacer/>
-          <hbox>
-            <image class="statusIcon"/>
-            <label class="status" value=" "/>
-          </hbox>
-        </row>
-        <row id="passwordRow" align="center">
-          <label value="&setup.choosePassword.label;"
-                 accesskey="&setup.choosePassword.accesskey;"
-                 control="weavePassword"/>
-          <textbox id="weavePassword"
-                   type="password"
-                   onchange="gSyncSetup.onPasswordChange()"/>
-        </row>
-        <row id="confirmRow" align="center">
-          <label value="&setup.confirmPassword.label;"
-                 accesskey="&setup.confirmPassword.accesskey;"
-                 control="weavePasswordConfirm"/>
-          <textbox id="weavePasswordConfirm"
-                   type="password"
-                   onchange="gSyncSetup.onPasswordChange()"/>
-        </row>
-        <row id="passwordFeedbackRow" align="center" hidden="true">
-          <spacer/>
-          <hbox>
-            <image class="statusIcon"/>
-            <label class="status" value=" "/>
-          </hbox>
-        </row>
-        <row align="center">
-          <label control="server"
-                 value="&server.label;"/>
-          <menulist id="server"
-                    oncommand="gSyncSetup.onServerCommand()"
-                    oninput="gSyncSetup.onServerInput()">
-            <menupopup>
-              <menuitem label="&serverType.default.label;"
-                        value="main"/>
-              <menuitem label="&serverType.custom2.label;"
-                        value="custom"/>
-            </menupopup>
-          </menulist>
-        </row>
-        <row id="serverFeedbackRow" align="center" hidden="true">
-          <spacer/>
-          <hbox>
-            <image class="statusIcon"/>
-            <label class="status" value=" "/>
-          </hbox>
-        </row>
-        <row id="TOSRow" align="center">
-          <spacer/>
-          <hbox align="center">
-            <checkbox id="tos"
-                      accesskey="&setup.tosAgree1.accesskey;"
-                      oncommand="this.focus(); gSyncSetup.checkFields();"/>
-            <description id="tosDesc"
-                         flex="1"
-                         onclick="document.getElementById('tos').focus();
-                                  document.getElementById('tos').click()">
-              &setup.tosAgree1.label;
-              <label class="text-link inline-link"
-                     onclick="event.stopPropagation();gSyncUtils.openToS();">
-                &setup.tosLink.label;
-              </label>
-              &setup.tosAgree2.label;
-              <label class="text-link inline-link"
-                     onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();">
-                &setup.ppLink.label;
-              </label>
-              &setup.tosAgree3.label;
-            </description>
-          </hbox>
-        </row>
-      </rows>
-    </grid>
-    <spacer flex="1"/>
-    <vbox flex="1" align="center">
-      <browser height="150"
-               width="500"
-               id="captcha"
-               type="content"
-               disablehistory="true"/>
-      <spacer flex="1"/>
-      <hbox id="captchaFeedback">
-        <image class="statusIcon"/>
-        <label class="status" value=" "/>
-      </hbox>
-    </vbox>
-  </wizardpage>
-
-  <wizardpage id="addDevice"
-              label="&pairDevice.title.label;"
-              onextra1="gSyncSetup.onSyncOptions()"
-              onpageshow="gSyncSetup.onPageShow()">
-    <description>
-      &pairDevice.setup.description.label;
-      <label class="text-link"
-             value="&addDevice.showMeHow.label;"
-             href="https://services.mozilla.com/sync/help/easy-setup"/>
-    </description>
-    <label value="&addDevice.setup.enterCode.label;"
-           control="easySetupPIN1"/>
-    <spacer flex="1"/>
-    <vbox align="center" flex="1">
-      <textbox id="easySetupPIN1"
-               class="pin"
-               value=""
-               readonly="true"
-               />
-      <textbox id="easySetupPIN2"
-               class="pin"
-               value=""
-               readonly="true"
-               />
-      <textbox id="easySetupPIN3"
-               class="pin"
-               value=""
-               readonly="true"
-               />
-    </vbox>
-    <spacer flex="3"/>
-    <label class="text-link"
-           value="&addDevice.dontHaveDevice.label;"
-           onclick="gSyncSetup.manualSetup();"/>
-  </wizardpage>
-
-  <wizardpage id="existingAccount"
-              label="&setup.signInPage.title.label;"
-              onextra1="gSyncSetup.onSyncOptions()"
-              onpageshow="gSyncSetup.onPageShow()">
-      <grid>
-        <columns>
-          <column/>
-          <column class="inputColumn" flex="1"/>
-        </columns>
-        <rows>
-          <row id="existingAccountRow" align="center">
-            <label id="existingAccountLabel"
-                   value="&signIn.account2.label;"
-                   accesskey="&signIn.account2.accesskey;"
-                   control="existingAccount"/>
-            <textbox id="existingAccountName"
-                     oninput="gSyncSetup.checkFields(event)"
-                     onchange="gSyncSetup.checkFields(event)"/>
-          </row>
-          <row id="existingPasswordRow" align="center">
-            <label id="existingPasswordLabel"
-                   value="&signIn.password.label;"
-                   accesskey="&signIn.password.accesskey;"
-                   control="existingPassword"/>
-            <textbox id="existingPassword"
-                     type="password"
-                     onkeyup="gSyncSetup.checkFields(event)"
-                     onchange="gSyncSetup.checkFields(event)"/>
-          </row>
-          <row id="existingPasswordFeedbackRow" align="center" hidden="true">
-            <spacer/>
-            <hbox>
-              <image class="statusIcon"/>
-              <label class="status" value=" "/>
-            </hbox>
-          </row>
-          <row align="center">
-            <spacer/>
-            <label class="text-link"
-                   value="&resetPassword.label;"
-                   onclick="gSyncUtils.resetPassword(); return false;"/>
-          </row>
-          <row align="center">
-            <label control="existingServer"
-                   value="&server.label;"/>
-            <menulist id="existingServer"
-                      oncommand="gSyncSetup.onExistingServerCommand()"
-                      oninput="gSyncSetup.onExistingServerInput()">
-              <menupopup>
-                <menuitem label="&serverType.default.label;"
-                          value="main"/>
-                <menuitem label="&serverType.custom2.label;"
-                          value="custom"/>
-              </menupopup>
-            </menulist>
-          </row>
-          <row id="existingServerFeedbackRow" align="center" hidden="true">
-            <spacer/>
-            <hbox>
-              <image class="statusIcon"/>
-              <vbox>
-                <label class="status" value=" "/>
-              </vbox>
-            </hbox>
-          </row>
-        </rows>
-      </grid>
-
-    <groupbox>
-      <label id="existingPassphraseLabel"
-             value="&signIn.recoveryKey.label;"
-             accesskey="&signIn.recoveryKey.accesskey;"
-             control="existingPassphrase"/>
-      <textbox id="existingPassphrase"
-               oninput="gSyncSetup.checkFields()"/>
-      <hbox id="login-throbber" hidden="true">
-        <image/>
-        <label value="&verifying.label;"/>
-      </hbox>
-      <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true">
-        <hbox>
-          <image class="statusIcon"/>
-          <label class="status" value=" "/>
-        </hbox>
-      </vbox>
-    </groupbox>
-
-    <vbox id="passphraseHelpBox">
-      <description>
-        &existingRecoveryKey.description;
-        <label class="text-link"
-               href="https://services.mozilla.com/sync/help/manual-setup">
-          &addDevice.showMeHow.label;
-        </label>
-        <spacer id="passphraseHelpSpacer"/>
-        <label class="text-link"
-               onclick="gSyncSetup.resetPassphrase(); return false;">
-          &resetSyncKey.label;
-        </label>
-      </description>
-    </vbox>
-  </wizardpage>
-
-  <wizardpage id="syncOptionsPage"
-              label="&setup.optionsPage.title;"
-              onpageshow="gSyncSetup.onPageShow()">
-    <groupbox id="syncOptions">
-    <grid>
-      <columns>
-        <column/>
-        <column flex="1" style="-moz-margin-end: 2px"/>
-      </columns>
-      <rows>
-        <row align="center">
-          <label value="&syncDeviceName.label;"
-                 accesskey="&syncDeviceName.accesskey;"
-                 control="syncComputerName"/>
-          <textbox id="syncComputerName" flex="1"
-                   onchange="gSyncUtils.changeName(this)"/>
-        </row>
-        <row>
-          <label value="&syncMy.label;" />
-          <vbox>
-            <checkbox label="&engine.addons.label;"
-                      accesskey="&engine.addons.accesskey;"
-                      id="engine.addons"
-                      checked="true"/>
-            <checkbox label="&engine.bookmarks.label;"
-                      accesskey="&engine.bookmarks.accesskey;"
-                      id="engine.bookmarks"
-                      checked="true"/>
-            <checkbox label="&engine.passwords.label;"
-                      accesskey="&engine.passwords.accesskey;"
-                      id="engine.passwords"
-                      checked="true"/>
-            <checkbox label="&engine.prefs.label;"
-                      accesskey="&engine.prefs.accesskey;"
-                      id="engine.prefs"
-                      checked="true"/>
-            <checkbox label="&engine.history.label;"
-                      accesskey="&engine.history.accesskey;"
-                      id="engine.history"
-                      checked="true"/>
-            <checkbox label="&engine.tabs.label;"
-                      accesskey="&engine.tabs.accesskey;"
-                      id="engine.tabs"
-                      checked="true"/>
-          </vbox>
-        </row>
-      </rows>
-    </grid>
-    </groupbox>
-
-    <groupbox id="mergeOptions">
-      <radiogroup id="mergeChoiceRadio" pack="start">
-        <grid>
-          <columns>
-            <column/>
-            <column flex="1"/>
-          </columns>
-          <rows flex="1">
-            <row align="center">
-              <radio id="resetClient"
-                     class="mergeChoiceButton"
-                     aria-labelledby="resetClientLabel"/>
-              <label id="resetClientLabel" control="resetClient">
-                <html:strong>&choice2.merge.recommended.label;</html:strong>
-                &choice2a.merge.main.label;
-              </label>
-            </row>
-            <row align="center">
-              <radio id="wipeClient"
-                     class="mergeChoiceButton"
-                     aria-labelledby="wipeClientLabel"/>
-              <label id="wipeClientLabel"
-                     control="wipeClient">
-                &choice2a.client.main.label;
-              </label>
-            </row>
-            <row align="center">
-              <radio id="wipeRemote"
-                     class="mergeChoiceButton"
-                     aria-labelledby="wipeRemoteLabel"/>
-              <label id="wipeRemoteLabel"
-                     control="wipeRemote">
-                &choice2a.server.main.label;
-              </label>
-            </row>
-          </rows>
-        </grid>
-      </radiogroup>
-    </groupbox>
-  </wizardpage>
-
-  <wizardpage id="syncOptionsConfirm"
-              label="&setup.optionsConfirmPage.title;"
-              onpageshow="gSyncSetup.onPageShow()">
-      <deck id="chosenActionDeck">
-        <vbox id="chosenActionMerge" class="confirm">
-          <description class="normal">
-            &confirm.merge2.label;
-          </description>
-        </vbox>
-        <vbox id="chosenActionWipeClient" class="confirm">
-          <description class="normal">
-            &confirm.client3.label;
-          </description>
-          <separator class="thin"/>
-          <vbox id="dataList">
-            <label class="data indent" id="bookmarkCount"/>
-            <label class="data indent" id="historyCount"/>
-            <label class="data indent" id="passwordCount"/>
-            <label class="data indent" id="addonCount"/>
-            <label class="data indent" id="prefsWipe"
-                   value="&engine.prefs.label;"/>
-          </vbox>
-          <separator class="thin"/>
-          <description class="normal">
-            &confirm.client2.moreinfo.label;
-          </description>
-        </vbox>
-        <vbox id="chosenActionWipeServer" class="confirm">
-          <description class="normal">
-            &confirm.server2.label;
-          </description>
-          <separator class="thin"/>
-          <vbox id="clientList">
-          </vbox>
-        </vbox>
-      </deck>
-  </wizardpage>
-  <!-- In terms of the wizard flow shown to the user, the 'syncOptionsConfirm'
-       page above is not the last wizard page. To prevent the wizard binding from
-       assuming that it is, we're inserting this dummy page here. This also means
-      that the wizard needs to always be closed manually via wizardFinish(). -->
-  <wizardpage>
-  </wizardpage>
-</wizard>
-
--- a/browser/base/content/sync/utils.js
+++ b/browser/base/content/sync/utils.js
@@ -1,218 +1,36 @@
 /* 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/. */
 
-// Equivalent to 0600 permissions; used for saved Sync Recovery Key.
-// This constant can be replaced when the equivalent values are available to
-// chrome JS; see Bug 433295 and Bug 757351.
-const PERMISSIONS_RWUSR = 0x180;
-
 // Weave should always exist before before this file gets included.
 let gSyncUtils = {
-  get bundle() {
-    delete this.bundle;
-    return this.bundle = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
-  },
-
   // opens in a new window if we're in a modal prefwindow world, in a new tab otherwise
   _openLink: function (url) {
     let thisDocEl = document.documentElement,
         openerDocEl = window.opener && window.opener.document.documentElement;
-    if (thisDocEl.id == "accountSetup" && window.opener &&
-        openerDocEl.id == "BrowserPreferences" && !openerDocEl.instantApply)
-      openUILinkIn(url, "window");
-    else if (thisDocEl.id == "BrowserPreferences" && !thisDocEl.instantApply)
+    if (thisDocEl.id == "BrowserPreferences" && !thisDocEl.instantApply) {
       openUILinkIn(url, "window");
-    else if (document.documentElement.id == "change-dialog")
-      Services.wm.getMostRecentWindow("navigator:browser")
-              .openUILinkIn(url, "tab");
-    else
+    } else {
       openUILinkIn(url, "tab");
+    }
   },
 
   changeName: function changeName(input) {
     // Make sure to update to a modified name, e.g., empty-string -> default
     Weave.Service.clientsEngine.localName = input.value;
     input.value = Weave.Service.clientsEngine.localName;
   },
 
-  openChange: function openChange(type, duringSetup) {
-    // Just re-show the dialog if it's already open
-    let openedDialog = Services.wm.getMostRecentWindow("Sync:" + type);
-    if (openedDialog != null) {
-      openedDialog.focus();
-      return;
-    }
-
-    // Open up the change dialog
-    let changeXUL = "chrome://browser/content/sync/genericChange.xul";
-    let changeOpt = "centerscreen,chrome,resizable=no";
-    Services.ww.activeWindow.openDialog(changeXUL, "", changeOpt,
-                                        type, duringSetup);
-  },
-
-  changePassword: function () {
-    if (Weave.Utils.ensureMPUnlocked())
-      this.openChange("ChangePassword");
-  },
-
-  resetPassphrase: function (duringSetup) {
-    if (Weave.Utils.ensureMPUnlocked())
-      this.openChange("ResetPassphrase", duringSetup);
-  },
-
-  updatePassphrase: function () {
-    if (Weave.Utils.ensureMPUnlocked())
-      this.openChange("UpdatePassphrase");
-  },
-
-  resetPassword: function () {
-    this._openLink(Weave.Service.pwResetURL);
-  },
-
   openToS: function () {
     this._openLink(Weave.Svc.Prefs.get("termsURL"));
   },
 
   openPrivacyPolicy: function () {
     this._openLink(Weave.Svc.Prefs.get("privacyURL"));
   },
 
-  openFirstSyncProgressPage: function () {
-    this._openLink("about:sync-progress");
-  },
-
-  /**
-   * Prepare an invisible iframe with the passphrase backup document.
-   * Used by both the print and saving methods.
-   *
-   * @param elid : ID of the form element containing the passphrase.
-   * @param callback : Function called once the iframe has loaded.
-   */
-  _preparePPiframe: function(elid, callback) {
-    let pp = document.getElementById(elid).value;
-
-    // Create an invisible iframe whose contents we can print.
-    let iframe = document.createElement("iframe");
-    iframe.setAttribute("src", "chrome://browser/content/sync/key.xhtml");
-    iframe.collapsed = true;
-    document.documentElement.appendChild(iframe);
-    iframe.contentWindow.addEventListener("load", function() {
-      iframe.contentWindow.removeEventListener("load", arguments.callee, false);
-
-      // Insert the Sync Key into the page.
-      let el = iframe.contentDocument.getElementById("synckey");
-      el.firstChild.nodeValue = pp;
-
-      // Insert the TOS and Privacy Policy URLs into the page.
-      let termsURL = Weave.Svc.Prefs.get("termsURL");
-      el = iframe.contentDocument.getElementById("tosLink");
-      el.setAttribute("href", termsURL);
-      el.firstChild.nodeValue = termsURL;
-
-      let privacyURL = Weave.Svc.Prefs.get("privacyURL");
-      el = iframe.contentDocument.getElementById("ppLink");
-      el.setAttribute("href", privacyURL);
-      el.firstChild.nodeValue = privacyURL;
-
-      callback(iframe);
-    }, false);
-  },
-
-  /**
-   * Print passphrase backup document.
-   * 
-   * @param elid : ID of the form element containing the passphrase.
-   */
-  passphrasePrint: function(elid) {
-    this._preparePPiframe(elid, function(iframe) {
-      let webBrowserPrint = iframe.contentWindow
-                                  .QueryInterface(Ci.nsIInterfaceRequestor)
-                                  .getInterface(Ci.nsIWebBrowserPrint);
-      let printSettings = PrintUtils.getPrintSettings();
-
-      // Display no header/footer decoration except for the date.
-      printSettings.headerStrLeft
-        = printSettings.headerStrCenter
-        = printSettings.headerStrRight
-        = printSettings.footerStrLeft
-        = printSettings.footerStrCenter = "";
-      printSettings.footerStrRight = "&D";
-
-      try {
-        webBrowserPrint.print(printSettings, null);
-      } catch (ex) {
-        // print()'s return codes are expressed as exceptions. Ignore.
-      }
-    });
-  },
-
-  /**
-   * Save passphrase backup document to disk as HTML file.
-   * 
-   * @param elid : ID of the form element containing the passphrase.
-   */
-  passphraseSave: function(elid) {
-    let dialogTitle = this.bundle.GetStringFromName("save.recoverykey.title");
-    let defaultSaveName = this.bundle.GetStringFromName("save.recoverykey.defaultfilename");
-    this._preparePPiframe(elid, function(iframe) {
-      let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-      let fpCallback = function fpCallback_done(aResult) {
-        if (aResult == Ci.nsIFilePicker.returnOK ||
-            aResult == Ci.nsIFilePicker.returnReplace) {
-          let stream = Cc["@mozilla.org/network/file-output-stream;1"].
-                       createInstance(Ci.nsIFileOutputStream);
-          stream.init(fp.file, -1, PERMISSIONS_RWUSR, 0);
-
-          let serializer = new XMLSerializer();
-          let output = serializer.serializeToString(iframe.contentDocument);
-          output = output.replace(/<!DOCTYPE (.|\n)*?]>/,
-            '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ' +
-            '"DTD/xhtml1-strict.dtd">');
-          output = Weave.Utils.encodeUTF8(output);
-          stream.write(output, output.length);
-        }
-      };
-
-      fp.init(window, dialogTitle, Ci.nsIFilePicker.modeSave);
-      fp.appendFilters(Ci.nsIFilePicker.filterHTML);
-      fp.defaultString = defaultSaveName;
-      fp.open(fpCallback);
-      return false;
-    });
-  },
-
-  /**
-   * validatePassword
-   *
-   * @param el1 : the first textbox element in the form
-   * @param el2 : the second textbox element, if omitted it's an update form
-   * 
-   * returns [valid, errorString]
-   */
-  validatePassword: function (el1, el2) {
-    let valid = false;
-    let val1 = el1.value;
-    let val2 = el2 ? el2.value : "";
-    let error = "";
-
-    if (!el2)
-      valid = val1.length >= Weave.MIN_PASS_LENGTH;
-    else if (val1 && val1 == Weave.Service.identity.username)
-      error = "change.password.pwSameAsUsername";
-    else if (val1 && val1 == Weave.Service.identity.account)
-      error = "change.password.pwSameAsEmail";
-    else if (val1 && val1 == Weave.Service.identity.basicPassword)
-      error = "change.password.pwSameAsPassword";
-    else if (val1 && val2) {
-      if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH)
-        valid = true;
-      else if (val1.length < Weave.MIN_PASS_LENGTH)
-        error = "change.password.tooShort";
-      else if (val1 != val2)
-        error = "change.password.mismatch";
-    }
-    let errorString = error ? Weave.Utils.getErrorString(error) : "";
-    return [valid, errorString];
+  openAccountsPage: function () {
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    win.switchToTabHavingURI("about:accounts", true);
   }
 };
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/Makefile.in
@@ -0,0 +1,66 @@
+# 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/.
+
+# test_contextmenu.html and test_contextmenu_input are disabled on Linux due to bug 513558
+ifndef MOZ_WIDGET_GTK
+MOCHITEST_FILES += \
+        audio.ogg \
+        privateBrowsingMode.js \
+        subtst_contextmenu.html \
+        contextmenu_common.js \
+        test_contextmenu.html \
+        test_contextmenu_input.html \
+        $(NULL)
+endif
+
+# The following tests are disabled because they are unreliable:
+#   browser_bug423833.js is bug 428712
+#   browser_sanitize-download-history.js is bug 432425
+#
+# browser_sanitizeDialog_treeView.js is disabled until the tree view is added
+# back to the clear recent history dialog (sanitize.xul), if it ever is (bug
+# 480169)
+
+# browser_drag.js is disabled, as it needs to be updated for the new behavior from bug 320638.
+
+# browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
+
+# Disable tests on Windows due to frequent failures (bugs 825739, 841341)
+ifneq (windows,$(MOZ_WIDGET_TOOLKIT))
+MOCHITEST_BROWSER_FILES += \
+                 browser_bookmark_titles.js \
+                 browser_popupNotification.js \
+                 $(NULL)
+endif
+
+ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+MOCHITEST_BROWSER_FILES += \
+		browser_bug462289.js \
+		$(NULL)
+else
+MOCHITEST_BROWSER_FILES += \
+		browser_bug565667.js \
+		$(NULL)
+endif
+
+ifdef MOZ_DATA_REPORTING
+MOCHITEST_BROWSER_FILES += \
+  browser_datareporting_notification.js \
+  $(NULL)
+endif
+
+ifdef MOZ_CRASHREPORTER
+MOCHITEST_BROWSER_FILES += \
+  browser_pluginCrashCommentAndURL.js \
+  pluginCrashCommentAndURL.html \
+  browser_CTP_crashreporting.js \
+  $(NULL)
+endif
+
+# browser_CTP_context_menu.js fails intermittently on Linux (bug 909342)
+ifndef MOZ_WIDGET_GTK
+MOCHITEST_BROWSER_FILES += \
+  browser_CTP_context_menu.js \
+  $(NULL)
+endif
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/accounts_testRemoteCommands.html
@@ -0,0 +1,81 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+
+<script type="text/javascript;version=1.8">
+
+function init() {
+  window.addEventListener("message", function process(e) {doTest(e)}, false);
+  // unless we relinquish the eventloop,
+  // tests will run before the chrome event handlers are ready
+  setTimeout(doTest, 0);
+}
+
+function checkStatusValue(payload, expectedValue) {
+  return payload.status == expectedValue;
+}
+
+let tests = [
+{
+  info: "Check account log in",
+  event: "login",
+  data: {
+    email: "foo@example.com",
+    uid: "1234@lcip.org",
+    assertion: "foobar",
+    sessionToken: "dead",
+    kA: "beef",
+    kB: "cafe",
+    isVerified: true
+  },
+  payloadType: "message",
+  validateResponse: function(payload) {
+    return checkStatusValue(payload, "login");
+  },
+},
+];
+
+let currentTest = -1;
+function doTest(evt) {
+  if (evt) {
+    if (currentTest < 0 || !evt.data.content)
+      return; // not yet testing
+
+    let test = tests[currentTest];
+    if (evt.data.type != test.payloadType)
+      return; // skip unrequested events
+
+    let error = JSON.stringify(evt.data.content);
+    let pass = false;
+    try {
+      pass = test.validateResponse(evt.data.content)
+    } catch (e) {}
+    reportResult(test.info, pass, error);
+  }
+  // start the next test if there are any left
+  if (tests[++currentTest])
+    sendToBrowser(tests[currentTest].event, tests[currentTest].data);
+  else
+    reportFinished();
+}
+
+function reportResult(info, pass, error) {
+  let data = {type: "testResult", info: info, pass: pass, error: error};
+  window.parent.postMessage(data, "*");
+}
+
+function reportFinished(cmd) {
+  let data = {type: "testsComplete", count: tests.length};
+  window.parent.postMessage(data, "*");
+}
+
+function sendToBrowser(type, data) {
+  let event = new CustomEvent("FirefoxAccountsCommand", {detail: {command: type, data: data}, bubbles: true});
+  document.dispatchEvent(event);
+}
+
+</script>
+  </head>
+  <body onload="init()">
+  </body>
+</html>
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -1,11 +1,12 @@
 [DEFAULT]
 support-files =
   POSTSearchEngine.xml
+  accounts_testRemoteCommands.html
   alltabslistener.html
   app_bug575561.html
   app_subframe_bug575561.html
   authenticate.sjs
   blockNoPlugins.xml
   blockPluginHard.xml
   blockPluginVulnerableNoUpdate.xml
   blockPluginVulnerableUpdatable.xml
@@ -106,16 +107,17 @@ skip-if = toolkit == "gtk2" || toolkit =
 run-if = crashreporter
 [browser_CTP_data_urls.js]
 [browser_CTP_drag_drop.js]
 [browser_CTP_hideBar.js]
 [browser_CTP_multi_allow.js]
 [browser_CTP_nonplugins.js]
 [browser_CTP_resize.js]
 [browser_URLBarSetURI.js]
+[browser_aboutAccounts.js]
 [browser_aboutHealthReport.js]
 skip-if = os == "linux" # Bug 924307
 [browser_aboutHome.js]
 skip-if = os == "linux" # Bug 945667
 [browser_aboutSyncProgress.js]
 [browser_addKeywordSearch.js]
 [browser_alltabslistener.js]
 [browser_backButtonFitts.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutAccounts.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+  "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+  "resource://gre/modules/Task.jsm");
+
+registerCleanupFunction(function() {
+  // Ensure we don't pollute prefs for next tests.
+  Services.prefs.clearUserPref("firefox.accounts.remoteUrl");
+});
+
+let gTests = [
+
+{
+  desc: "Test the remote commands",
+  setup: function ()
+  {
+    Services.prefs.setCharPref("firefox.accounts.remoteUrl",
+                               "https://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
+  },
+  run: function ()
+  {
+    let deferred = Promise.defer();
+
+    let results = 0;
+    try {
+      let win = gBrowser.contentWindow;
+      win.addEventListener("message", function testLoad(e) {
+        if (e.data.type == "testResult") {
+          ok(e.data.pass, e.data.info);
+          results++;
+        }
+        else if (e.data.type == "testsComplete") {
+          is(results, e.data.count, "Checking number of results received matches the number of tests that should have run");
+          win.removeEventListener("message", testLoad, false, true);
+          deferred.resolve();
+        }
+
+      }, false, true);
+
+    } catch(e) {
+      ok(false, "Failed to get all commands");
+      deferred.reject();
+    }
+    return deferred.promise;
+  }
+},
+
+
+]; // gTests
+
+function test()
+{
+  waitForExplicitFinish();
+
+  Task.spawn(function () {
+    for (let test of gTests) {
+      info(test.desc);
+      test.setup();
+
+      yield promiseNewTabLoadEvent("about:accounts");
+
+      yield test.run();
+
+      gBrowser.removeCurrentTab();
+    }
+
+    finish();
+  });
+}
+
+function promiseNewTabLoadEvent(aUrl, aEventType="load")
+{
+  let deferred = Promise.defer();
+  let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
+  tab.linkedBrowser.addEventListener(aEventType, function load(event) {
+    tab.linkedBrowser.removeEventListener(aEventType, load, true);
+    let iframe = tab.linkedBrowser.contentDocument.getElementById("remote");
+      iframe.addEventListener("load", function frameLoad(e) {
+        iframe.removeEventListener("load", frameLoad, false);
+        deferred.resolve();
+      }, false);
+    }, true);
+  return deferred.promise;
+}
+
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -45,16 +45,19 @@ browser.jar:
         content/browser/abouthome/restore@2x.png       (content/abouthome/restore@2x.png)
         content/browser/abouthome/restore-large@2x.png (content/abouthome/restore-large@2x.png)
         content/browser/abouthome/mozilla@2x.png       (content/abouthome/mozilla@2x.png)
 #ifdef MOZ_SERVICES_HEALTHREPORT
         content/browser/abouthealthreport/abouthealth.xhtml   (content/abouthealthreport/abouthealth.xhtml)
         content/browser/abouthealthreport/abouthealth.js      (content/abouthealthreport/abouthealth.js)
         content/browser/abouthealthreport/abouthealth.css     (content/abouthealthreport/abouthealth.css)
 #endif
+        content/browser/aboutaccounts/aboutaccounts.xhtml   (content/aboutaccounts/aboutaccounts.xhtml)
+        content/browser/aboutaccounts/aboutaccounts.js      (content/aboutaccounts/aboutaccounts.js)
+        content/browser/aboutaccounts/aboutaccounts.css     (content/aboutaccounts/aboutaccounts.css)
         content/browser/aboutRobots-icon.png          (content/aboutRobots-icon.png)
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
         content/browser/aboutSocialError.xhtml        (content/aboutSocialError.xhtml)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
@@ -72,26 +75,17 @@ browser.jar:
         content/browser/pageinfo/feeds.xml            (content/pageinfo/feeds.xml)
         content/browser/pageinfo/permissions.js       (content/pageinfo/permissions.js)
         content/browser/pageinfo/security.js          (content/pageinfo/security.js)
 #ifdef MOZ_SERVICES_SYNC
         content/browser/sync/aboutSyncTabs.xul        (content/sync/aboutSyncTabs.xul)
         content/browser/sync/aboutSyncTabs.js         (content/sync/aboutSyncTabs.js)
         content/browser/sync/aboutSyncTabs.css        (content/sync/aboutSyncTabs.css)
         content/browser/sync/aboutSyncTabs-bindings.xml  (content/sync/aboutSyncTabs-bindings.xml)
-        content/browser/sync/setup.xul                (content/sync/setup.xul)
-        content/browser/sync/addDevice.js             (content/sync/addDevice.js)
-        content/browser/sync/addDevice.xul            (content/sync/addDevice.xul)
-        content/browser/sync/setup.js                 (content/sync/setup.js)
-        content/browser/sync/genericChange.xul        (content/sync/genericChange.xul)
-        content/browser/sync/genericChange.js         (content/sync/genericChange.js)
-        content/browser/sync/key.xhtml                (content/sync/key.xhtml)
         content/browser/sync/notification.xml         (content/sync/notification.xml)
-        content/browser/sync/quota.xul                (content/sync/quota.xul)
-        content/browser/sync/quota.js                 (content/sync/quota.js)
         content/browser/sync/utils.js                 (content/sync/utils.js)
         content/browser/sync/progress.js              (content/sync/progress.js)
         content/browser/sync/progress.xhtml           (content/sync/progress.xhtml)
 #endif
         content/browser/openLocation.js               (content/openLocation.js)
         content/browser/openLocation.xul              (content/openLocation.xul)
         content/browser/safeMode.css                  (content/safeMode.css)
         content/browser/safeMode.js                   (content/safeMode.js)
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -85,16 +85,18 @@ static RedirEntry kRedirMap[] = {
   { "preferences", "chrome://browser/content/preferences/in-content/preferences.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "downloads", "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 #ifdef MOZ_SERVICES_HEALTHREPORT
   { "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
+  { "accounts", "chrome://browser/content/aboutaccounts/aboutaccounts.xhtml",
+    nsIAboutModule::ALLOW_SCRIPT },
   { "app-manager", "chrome://browser/content/devtools/app-manager/index.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "customizing", "chrome://browser/content/customizableui/aboutCustomizing.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 };
 static const int kRedirTotal = NS_ARRAY_LENGTH(kRedirMap);
 
 static nsAutoCString
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -101,16 +101,17 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-tabs", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sync-progress", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "permissions", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "accounts", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #ifdef MOZ_SERVICES_HEALTHREPORT
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "app-manager", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "customizing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN)
     { NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
 #elif defined(XP_MACOSX)
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -1,17 +1,16 @@
 /* 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/. */
 
 Components.utils.import("resource://services-sync/main.js");
 
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
-const PAGE_NEEDS_UPDATE = 2;
 
 let gSyncPane = {
   _stringBundle: null,
   prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
               "engine.tabs", "engine.history"],
 
   get page() {
     return document.getElementById("weavePrefsDeck").selectedIndex;
@@ -20,23 +19,16 @@ let gSyncPane = {
   set page(val) {
     document.getElementById("weavePrefsDeck").selectedIndex = val;
   },
 
   get _usingCustomServer() {
     return Weave.Svc.Prefs.isSet("serverURL");
   },
 
-  needsUpdate: function () {
-    this.page = PAGE_NEEDS_UPDATE;
-    let label = document.getElementById("loginError");
-    label.value = Weave.Utils.getErrorString(Weave.Status.login);
-    label.className = "error";
-  },
-
   init: function () {
     // If the Service hasn't finished initializing, wait for it.
     let xps = Components.classes["@mozilla.org/weave/service;1"]
                                 .getService(Components.interfaces.nsISupports)
                                 .wrappedJSObject;
 
     if (xps.ready) {
       this._init();
@@ -84,103 +76,25 @@ let gSyncPane = {
 
     this._stringBundle =
       Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
     this.updateWeavePrefs();
   },
 
   updateWeavePrefs: function () {
     if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
-        Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
+        Weave.Svc.Prefs.get("firstSync", "") == "notReady" ||
+        Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
+        Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
       this.page = PAGE_NO_ACCOUNT;
-    } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
-               Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
-      this.needsUpdate();
     } else {
       this.page = PAGE_HAS_ACCOUNT;
       document.getElementById("accountName").value = Weave.Service.identity.account;
       document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP").hidden = this._usingCustomServer;
     }
   },
 
-  startOver: function (showDialog) {
-    if (showDialog) {
-      let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
-                  Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + 
-                  Services.prompt.BUTTON_POS_1_DEFAULT;
-      let buttonChoice =
-        Services.prompt.confirmEx(window,
-                                  this._stringBundle.GetStringFromName("syncUnlink.title"),
-                                  this._stringBundle.GetStringFromName("syncUnlink.label"),
-                                  flags,
-                                  this._stringBundle.GetStringFromName("syncUnlinkConfirm.label"),
-                                  null, null, null, {});
-
-      // If the user selects cancel, just bail
-      if (buttonChoice == 1)
-        return;
-    }
-
-    Weave.Service.startOver();
-    this.updateWeavePrefs();
-  },
-
-  updatePass: function () {
-    if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED)
-      gSyncUtils.changePassword();
-    else
-      gSyncUtils.updatePassphrase();
-  },
-
-  resetPass: function () {
-    if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED)
-      gSyncUtils.resetPassword();
-    else
-      gSyncUtils.resetPassphrase();
-  },
-
-  /**
-   * Invoke the Sync setup wizard.
-   * 
-   * @param wizardType
-   *        Indicates type of wizard to launch:
-   *          null    -- regular set up wizard
-   *          "pair"  -- pair a device first
-   *          "reset" -- reset sync
-   */
-  openSetup: function (wizardType) {
-    let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
-    if (win)
-      win.focus();
-    else {
-      window.openDialog("chrome://browser/content/sync/setup.xul",
-                        "weaveSetup", "centerscreen,chrome,resizable=no",
-                        wizardType);
-    }
-  },
-
-  openQuotaDialog: function () {
-    let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
-    if (win)
-      win.focus();
-    else 
-      window.openDialog("chrome://browser/content/sync/quota.xul", "",
-                        "centerscreen,chrome,dialog,modal");
-  },
-
-  openAddDevice: function () {
-    if (!Weave.Utils.ensureMPUnlocked())
-      return;
-    
-    let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
-    if (win)
-      win.focus();
-    else 
-      window.openDialog("chrome://browser/content/sync/addDevice.xul",
-                        "syncAddDevice", "centerscreen,chrome,resizable=no");
-  },
-
-  resetSync: function () {
-    this.openSetup("reset");
-  },
+  openAccountsPage: function () {
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    win.switchToTabHavingURI("about:accounts", true);
+  }
 };
-
--- a/browser/components/preferences/in-content/sync.xul
+++ b/browser/components/preferences/in-content/sync.xul
@@ -31,58 +31,32 @@
 <deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
   <vbox id="noAccount" align="center">
     <spacer flex="1"/>
     <description id="syncDesc">
       &weaveDesc.label;
     </description>
     <separator/>
     <label class="text-link"
-           onclick="event.stopPropagation(); gSyncPane.openSetup(null);"
+           onclick="event.stopPropagation(); gSyncPane.openAccountsPage();"
            value="&setupButton.label;"/>
-    <separator/>
-    <label class="text-link"
-           onclick="event.stopPropagation(); gSyncPane.openSetup('pair');"
-           value="&pairDevice.label;"/>
     <spacer flex="3"/>
   </vbox>
 
   <vbox id="hasAccount">
     <groupbox class="syncGroupBox">
       <!-- label is set to account name -->
-      <caption id="accountCaption" align="center">
+      <caption id="accountCaption" align="center" flex="1">
         <image id="accountCaptionImage"/>
-        <label id="accountName" value=""/>
-      </caption>
+        <label id="accountName" value="" flex="1"/>
 
-      <hbox>
-        <button type="menu"
-                label="&manageAccount.label;"
-                accesskey="&manageAccount.accesskey;">
-          <menupopup>
-            <menuitem label="&viewQuota.label;"
-                      oncommand="gSyncPane.openQuotaDialog();"/>
-            <menuseparator/>
-            <menuitem label="&changePassword2.label;"
-                      oncommand="gSyncUtils.changePassword();"/>
-            <menuitem label="&myRecoveryKey.label;"
-                      oncommand="gSyncUtils.resetPassphrase();"/>
-            <menuseparator/>
-            <menuitem label="&resetSync2.label;"
-                      oncommand="gSyncPane.resetSync();"/>
-          </menupopup>
-        </button>
-      </hbox>
-
-      <hbox>
-        <label id="syncAddDeviceLabel"
-               class="text-link"
-               onclick="gSyncPane.openAddDevice(); return false;"
-               value="&pairDevice.label;"/>
-      </hbox>
+        <label class="text-link"
+               onclick="event.stopPropagation();gSyncUtils.openAccountsPage();"
+               value="&accountsLink.label;"/>
+      </caption>
 
       <vbox>
         <label value="&syncMy.label;" />
         <richlistbox id="syncEnginesList"
                      orient="vertical"
                      onselect="if (this.selectedCount) this.clearSelection();">
           <richlistitem>
             <checkbox label="&engine.addons.label;"
@@ -129,40 +103,19 @@
             <label value="&syncDeviceName.label;"
                    accesskey="&syncDeviceName.accesskey;"
                    control="syncComputerName"/>
             <textbox id="syncComputerName"
                      onchange="gSyncUtils.changeName(this)"/>
           </row>
         </rows>
       </grid>
-      <hbox>
-        <label class="text-link"
-               onclick="gSyncPane.startOver(true); return false;"
-               value="&unlinkDevice.label;"/>
-      </hbox>
     </groupbox>
     <hbox id="tosPP" pack="center">
       <label class="text-link"
              onclick="event.stopPropagation();gSyncUtils.openToS();"
              value="&prefs.tosLink.label;"/>
       <label class="text-link"
              onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"
              value="&prefs.ppLink.label;"/>
     </hbox>
   </vbox>
-
-  <vbox id="needsUpdate" align="center" pack="center">
-    <hbox>
-      <label id="loginError" value=""/>
-      <label class="text-link"
-             onclick="gSyncPane.updatePass(); return false;"
-             value="&updatePass.label;"/>
-      <label class="text-link"
-             onclick="gSyncPane.resetPass(); return false;"
-             value="&resetPass.label;"/>
-    </hbox>
-    <label class="text-link"
-           onclick="gSyncPane.startOver(true); return false;"
-           value="&unlinkDevice.label;"/>
-  </vbox>
-
 </deck>
--- a/browser/components/preferences/sync.js
+++ b/browser/components/preferences/sync.js
@@ -2,17 +2,16 @@
  * 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/. */
 
 Components.utils.import("resource://services-sync/main.js");
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 const PAGE_NO_ACCOUNT = 0;
 const PAGE_HAS_ACCOUNT = 1;
-const PAGE_NEEDS_UPDATE = 2;
 
 let gSyncPane = {
   _stringBundle: null,
   prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
               "engine.tabs", "engine.history"],
 
   get page() {
     return document.getElementById("weavePrefsDeck").selectedIndex;
@@ -21,23 +20,16 @@ let gSyncPane = {
   set page(val) {
     document.getElementById("weavePrefsDeck").selectedIndex = val;
   },
 
   get _usingCustomServer() {
     return Weave.Svc.Prefs.isSet("serverURL");
   },
 
-  needsUpdate: function () {
-    this.page = PAGE_NEEDS_UPDATE;
-    let label = document.getElementById("loginError");
-    label.value = Weave.Utils.getErrorString(Weave.Status.login);
-    label.className = "error";
-  },
-
   init: function () {
     // If the Service hasn't finished initializing, wait for it.
     let xps = Components.classes["@mozilla.org/weave/service;1"]
                                 .getService(Components.interfaces.nsISupports)
                                 .wrappedJSObject;
 
     if (xps.ready) {
       this._init();
@@ -84,109 +76,25 @@ let gSyncPane = {
 
     this._stringBundle =
       Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
     this.updateWeavePrefs();
   },
 
   updateWeavePrefs: function () {
     if (Weave.Status.service == Weave.CLIENT_NOT_CONFIGURED ||
-        Weave.Svc.Prefs.get("firstSync", "") == "notReady") {
+        Weave.Svc.Prefs.get("firstSync", "") == "notReady" ||
+        Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
+        Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
       this.page = PAGE_NO_ACCOUNT;
-    } else if (Weave.Status.login == Weave.LOGIN_FAILED_INVALID_PASSPHRASE ||
-               Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
-      this.needsUpdate();
     } else {
       this.page = PAGE_HAS_ACCOUNT;
       document.getElementById("accountName").value = Weave.Service.identity.account;
       document.getElementById("syncComputerName").value = Weave.Service.clientsEngine.localName;
       document.getElementById("tosPP").hidden = this._usingCustomServer;
     }
   },
 
-  startOver: function (showDialog) {
-    if (showDialog) {
-      let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
-                  Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + 
-                  Services.prompt.BUTTON_POS_1_DEFAULT;
-      let buttonChoice =
-        Services.prompt.confirmEx(window,
-                                  this._stringBundle.GetStringFromName("syncUnlink.title"),
-                                  this._stringBundle.GetStringFromName("syncUnlink.label"),
-                                  flags,
-                                  this._stringBundle.GetStringFromName("syncUnlinkConfirm.label"),
-                                  null, null, null, {});
-
-      // If the user selects cancel, just bail
-      if (buttonChoice == 1) {
-        return;
-      }
-    }
-
-    Weave.Service.startOver();
-    this.updateWeavePrefs();
-  },
-
-  updatePass: function () {
-    if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
-      gSyncUtils.changePassword();
-    } else {
-      gSyncUtils.updatePassphrase();
-    }
-  },
-
-  resetPass: function () {
-    if (Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED) {
-      gSyncUtils.resetPassword();
-    } else {
-      gSyncUtils.resetPassphrase();
-    }
-  },
-
-  /**
-   * Invoke the Sync setup wizard.
-   *
-   * @param wizardType
-   *        Indicates type of wizard to launch:
-   *          null    -- regular set up wizard
-   *          "pair"  -- pair a device first
-   *          "reset" -- reset sync
-   */
-  openSetup: function (wizardType) {
-    let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
-    if (win) {
-      win.focus();
-    } else {
-      window.openDialog("chrome://browser/content/sync/setup.xul",
-                        "weaveSetup", "centerscreen,chrome,resizable=no",
-                        wizardType);
-    }
-  },
-
-  openQuotaDialog: function () {
-    let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
-    if (win) {
-      win.focus();
-    } else {
-      window.openDialog("chrome://browser/content/sync/quota.xul", "",
-                        "centerscreen,chrome,dialog,modal");
-    }
-  },
-
-  openAddDevice: function () {
-    if (!Weave.Utils.ensureMPUnlocked()) {
-      return;
-    }
-
-    let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
-    if (win) {
-      win.focus();
-    } else {
-      window.openDialog("chrome://browser/content/sync/addDevice.xul",
-                        "syncAddDevice", "centerscreen,chrome,resizable=no");
-    }
-  },
-
-  resetSync: function () {
-    this.openSetup("reset");
-  },
+  openAccountsPage: function () {
+    let win = Services.wm.getMostRecentWindow("navigator:browser");
+    win.switchToTabHavingURI("about:accounts", true);
+  }
 };
-
--- a/browser/components/preferences/sync.xul
+++ b/browser/components/preferences/sync.xul
@@ -40,58 +40,32 @@
       <deck id="weavePrefsDeck">
         <vbox id="noAccount" align="center">
           <spacer flex="1"/>
           <description id="syncDesc">
             &weaveDesc.label;
           </description>
           <separator/>
           <label class="text-link"
-                 onclick="event.stopPropagation(); gSyncPane.openSetup(null);"
+                 onclick="event.stopPropagation(); gSyncPane.openAccountsPage();"
                  value="&setupButton.label;"/>
-          <separator/>
-          <label class="text-link"
-                 onclick="event.stopPropagation(); gSyncPane.openSetup('pair');"
-                 value="&pairDevice.label;"/>
           <spacer flex="3"/>
         </vbox>
 
         <vbox id="hasAccount">
           <groupbox class="syncGroupBox">
             <!-- label is set to account name -->
-            <caption id="accountCaption" align="center">
+            <caption id="accountCaption" align="center" flex="1">
               <image id="accountCaptionImage"/>
-              <label id="accountName" value=""/>
-            </caption>
+              <label id="accountName" value="" flex="1"/>
 
-            <hbox>
-              <button type="menu"
-                      label="&manageAccount.label;"
-                      accesskey="&manageAccount.accesskey;">
-                <menupopup>
-                  <menuitem label="&viewQuota.label;"
-                            oncommand="gSyncPane.openQuotaDialog();"/>
-                  <menuseparator/>
-                  <menuitem label="&changePassword2.label;"
-                            oncommand="gSyncUtils.changePassword();"/>
-                  <menuitem label="&myRecoveryKey.label;"
-                            oncommand="gSyncUtils.resetPassphrase();"/>
-                  <menuseparator/>
-                  <menuitem label="&resetSync2.label;"
-                            oncommand="gSyncPane.resetSync();"/>
-                </menupopup>
-              </button>
-            </hbox>
-
-            <hbox>
-              <label id="syncAddDeviceLabel"
-                     class="text-link"
-                     onclick="gSyncPane.openAddDevice(); return false;"
-                     value="&pairDevice.label;"/>
-            </hbox>
+              <label class="text-link"
+                     onclick="event.stopPropagation();gSyncUtils.openAccountsPage();"
+                     value="&accountsLink.label;"/>
+            </caption>
 
             <vbox>
               <label value="&syncMy.label;" />
               <richlistbox id="syncEnginesList"
                            orient="vertical"
                            onselect="if (this.selectedCount) this.clearSelection();">
                 <richlistitem>
                   <checkbox label="&engine.addons.label;"
@@ -138,41 +112,21 @@
                   <label value="&syncDeviceName.label;"
                          accesskey="&syncDeviceName.accesskey;"
                          control="syncComputerName"/>
                   <textbox id="syncComputerName"
                            onchange="gSyncUtils.changeName(this)"/>
                 </row>
               </rows>
             </grid>
-            <hbox>
-              <label class="text-link"
-                     onclick="gSyncPane.startOver(true); return false;"
-                     value="&unlinkDevice.label;"/>
-            </hbox>
           </groupbox>
           <hbox id="tosPP" pack="center">
             <label class="text-link"
                    onclick="event.stopPropagation();gSyncUtils.openToS();"
                    value="&prefs.tosLink.label;"/>
             <label class="text-link"
                    onclick="event.stopPropagation();gSyncUtils.openPrivacyPolicy();"
                    value="&prefs.ppLink.label;"/>
           </hbox>
         </vbox>
-
-        <vbox id="needsUpdate" align="center" pack="center">
-          <hbox>
-            <label id="loginError" value=""/>
-            <label class="text-link"
-                   onclick="gSyncPane.updatePass(); return false;"
-                   value="&updatePass.label;"/>
-            <label class="text-link"
-                   onclick="gSyncPane.resetPass(); return false;"
-                   value="&resetPass.label;"/>
-          </hbox>
-          <label class="text-link"
-                 onclick="gSyncPane.startOver(true); return false;"
-                 value="&unlinkDevice.label;"/>
-        </vbox>
       </deck>
   </prefpane>
 </overlay>
new file mode 100644
--- /dev/null
+++ b/browser/locales/en-US/chrome/browser/aboutAccounts.dtd
@@ -0,0 +1,5 @@
+<!-- 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/. -->
+
+<!ENTITY aboutAccounts.pageTitle "&brandShortName; Accounts">
--- a/browser/locales/en-US/chrome/browser/preferences/sync.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/sync.dtd
@@ -3,30 +3,17 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!-- The page shown when not logged in... -->
 <!ENTITY setupButton.label          "Set Up &syncBrand.fullName.label;">
 <!ENTITY setupButton.accesskey      "S">
 <!ENTITY weaveDesc.label            "&syncBrand.fullName.label; lets you access your history, bookmarks, passwords and open tabs across all your devices.">
 
 <!-- The page shown when logged in... -->
-
-<!-- Login error feedback -->
-<!ENTITY updatePass.label             "Update">
-<!ENTITY resetPass.label              "Reset">
-
-<!-- Manage Account -->
-<!ENTITY manageAccount.label          "Manage Account">
-<!ENTITY manageAccount.accesskey      "n">
-<!ENTITY viewQuota.label              "View Quota">
-<!ENTITY changePassword2.label        "Change Password…">
-<!ENTITY myRecoveryKey.label          "My Recovery Key">
-<!ENTITY resetSync2.label             "Reset Sync…">
-
-<!ENTITY pairDevice.label             "Pair a Device">
+<!ENTITY accountsLink.label         "Manage Account">
 
 <!ENTITY syncMy.label               "Sync My">
 <!ENTITY engine.bookmarks.label     "Bookmarks">
 <!ENTITY engine.bookmarks.accesskey "m">
 <!ENTITY engine.tabs.label          "Tabs">
 <!ENTITY engine.tabs.accesskey      "T">
 <!ENTITY engine.history.label       "History">
 <!ENTITY engine.history.accesskey   "r">
@@ -35,13 +22,12 @@
 <!ENTITY engine.prefs.label         "Preferences">
 <!ENTITY engine.prefs.accesskey     "S">
 <!ENTITY engine.addons.label        "Add-ons">
 <!ENTITY engine.addons.accesskey    "A">
 
 <!-- Device Settings -->
 <!ENTITY syncDeviceName.label       "Device Name:">
 <!ENTITY syncDeviceName.accesskey   "c">
-<!ENTITY unlinkDevice.label           "Unlink This Device">
 
 <!-- Footer stuff -->
 <!ENTITY prefs.tosLink.label        "Terms of Service">
 <!ENTITY prefs.ppLink.label         "Privacy Policy">
deleted file mode 100644
--- a/browser/locales/en-US/chrome/browser/syncGenericChange.properties
+++ /dev/null
@@ -1,37 +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/.
-
-#LOCALIZATION NOTE (change.password.title): This (and associated change.password/passphrase) are used when the user elects to change their password.
-change.password.title = Change your Password
-change.password.acceptButton = Change Password
-change.password.status.active = Changing your password…
-change.password.status.success = Your password has been changed.
-change.password.status.error = There was an error changing your password.
-
-change.password3.introText = Your password must be at least 8 characters long.  It cannot be the same as either your user name or your Recovery Key.
-change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password.
-
-change.recoverykey.title = My Recovery Key
-change.recoverykey.acceptButton = Change Recovery Key
-change.recoverykey.label = Changing Recovery Key and uploading local data, please wait…
-change.recoverykey.error = There was an error while changing your Recovery Key!
-change.recoverykey.success = Your Recovery Key was successfully changed!
-
-change.synckey.introText2 = To ensure your total privacy, all of your data is encrypted prior to being uploaded. The key to decrypt your data is not uploaded.
-# LOCALIZATION NOTE (change.recoverykey.warningText) "Sync" should match &syncBrand.shortName.label; from syncBrand.dtd
-change.recoverykey.warningText = Note: Changing this will erase all data stored on the Sync server and upload new data secured by this Recovery Key. Your other devices will not sync until the new Recovery Key is entered for that device.
-
-new.recoverykey.label = Your Recovery Key
-
-# LOCALIZATION NOTE (new.password.title): This (and associated new.password/passphrase) are used on a second computer when it detects that your password or passphrase has been changed on a different device.
-new.password.title            = Update Password
-new.password.introText        = Your password was rejected by the server, please update your password.
-new.password.label            = Enter your new password
-new.password.confirm          = Confirm your new password
-new.password.acceptButton     = Update Password
-new.password.status.incorrect = Password incorrect, please try again.
-
-new.recoverykey.title          = Update Recovery Key
-new.recoverykey.introText      = Your Recovery Key was changed using another device, please enter your updated Recovery Key.
-new.recoverykey.acceptButton     = Update Recovery Key
deleted file mode 100644
--- a/browser/locales/en-US/chrome/browser/syncKey.dtd
+++ /dev/null
@@ -1,18 +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/. -->
-
-<!ENTITY syncKey.page.title               "Your &syncBrand.fullName.label; Key">
-<!ENTITY syncKey.page.description2        "This key is used to decode the data in your &syncBrand.fullName.label; account. You will need to enter the key each time you configure &syncBrand.fullName.label; on a new device.">
-<!ENTITY syncKey.keepItSecret.heading     "Keep it secret">
-<!ENTITY syncKey.keepItSecret.description "Your &syncBrand.fullName.label; account is encrypted to protect your privacy. Without this key, it would take years for anyone to decode your personal information. You are the only person who holds this key. This means you're the only one who can access your &syncBrand.fullName.label; data.">
-<!ENTITY syncKey.keepItSafe.heading       "Keep it safe">
-<!ENTITY syncKey.keepItSafe1.description  "Do not lose this key.">
-<!ENTITY syncKey.keepItSafe2.description  " We don't keep a copy of your key (that wouldn't be keeping it secret!) so ">
-<!ENTITY syncKey.keepItSafe3.description  "we can't help you recover it">
-<!ENTITY syncKey.keepItSafe4a.description " if it's lost. You'll need to use this key any time you connect a new device to &syncBrand.fullName.label;.">
-<!ENTITY syncKey.findOutMore1.label       "Find out more about &syncBrand.fullName.label; and your privacy at ">
-<!ENTITY syncKey.findOutMore2.label       ".">
-<!ENTITY syncKey.footer1.label            "&syncBrand.fullName.label; Terms of Service are available at ">
-<!ENTITY syncKey.footer2.label            ". The Privacy Policy is available at ">
-<!ENTITY syncKey.footer3.label            ".">
--- a/browser/locales/en-US/chrome/browser/syncProgress.dtd
+++ b/browser/locales/en-US/chrome/browser/syncProgress.dtd
@@ -3,13 +3,13 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!ENTITY % brandDTD
     SYSTEM "chrome://branding/locale/brand.dtd">
     %brandDTD;
 
 <!-- These strings are used in the sync progress upload page -->
 <!ENTITY syncProgress.pageTitle    "Your First Sync">
+<!ENTITY syncProgress.headline     "Setup Complete">
 <!ENTITY syncProgress.textBlurb    "Your data is now being encrypted and uploaded in the background. You can close this tab and continue using &brandShortName;.">
 <!ENTITY syncProgress.closeButton  "Close">
 <!ENTITY syncProgress.logoAltText  "&brandShortName; logo">
 <!ENTITY syncProgress.diffText     "&brandShortName; will now automatically sync in the background.  You can close this tab and continue using &brandShortName;.">
-
deleted file mode 100644
--- a/browser/locales/en-US/chrome/browser/syncQuota.dtd
+++ /dev/null
@@ -1,8 +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/. -->
-
-<!ENTITY quota.dialogTitle.label    "Server Quota">
-<!ENTITY quota.retrievingInfo.label "Retrieving quota information…">
-<!ENTITY quota.typeColumn.label     "Type">
-<!ENTITY quota.sizeColumn.label     "Size">
deleted file mode 100644
--- a/browser/locales/en-US/chrome/browser/syncQuota.properties
+++ /dev/null
@@ -1,42 +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/.
-
-collection.addons.label    = Add-ons
-collection.bookmarks.label = Bookmarks
-collection.history.label   = History
-collection.passwords.label = Passwords
-collection.prefs.label     = Preferences
-collection.tabs.label      = Tabs
-
-# LOCALIZATION NOTE (quota.usageNoQuota.label): %1$S and %2$S are numeric value
-# and unit (as defined in the download manager) of the amount of space occupied
-# on the server
-quota.usageNoQuota.label    = You are currently using %1$S %2$S.
-# LOCALIZATION NOTE (quota.usagePercentage.label):
-# %1$S is the percentage of space used,
-# %2$S and %3$S numeric value and unit (as defined in the download manager)
-# of the amount of space used,
-# %3$S and %4$S numeric value and unit (as defined in the download manager)
-# of the total space available.
-quota.usagePercentage.label = You are using %1$S%% (%2$S %3$S) of your allowed %4$S %5$S.
-quota.usageError.label      = Could not retrieve quota information.
-quota.retrieving.label      = Retrieving…
-# LOCALIZATION NOTE (quota.sizeValueUnit.label): %1$S is the amount of space
-# occupied by the engine, %2$K the corresponding unit (e.g. kB) as defined in
-# the download manager.
-quota.sizeValueUnit.label   = %1$S %2$S
-quota.remove.label          = Remove
-quota.treeCaption.label     = Uncheck items to stop syncing them and free up space on the server.
-# LOCALIZATION NOTE (quota.removal.label): %S is a list of engines that will be
-# disabled and whose data will be removed once the user confirms.
-quota.removal.label         = Firefox Sync will remove the following data: %S.
-# LOCALIZATION NOTE (quota.list.separator): This is the separator string used
-# for the list of engines (incl. spaces where appropriate)
-quota.list.separator        = ,\u0020
-# LOCALIZATION NOTE (quota.freeup.label): %1$S and %2$S are numeric value
-# and unit (as defined in the download manager) of the amount of space freed
-# up by disabling the unchecked engines.  If displayed this string is
-# concatenated directly to quota.removal.label and may need to start off with
-# whitespace.
-quota.freeup.label          = \u0020This will free up %1$S %2$S.
deleted file mode 100644
--- a/browser/locales/en-US/chrome/browser/syncSetup.dtd
+++ /dev/null
@@ -1,114 +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/. -->
-
-<!ENTITY accountSetupTitle.label    "&syncBrand.fullName.label; Setup">
-
-<!-- First page of the wizard -->
-
-<!ENTITY setup.pickSetupType.description2 "Welcome! If you've never used &syncBrand.fullName.label; before, you will need to create a new account.">
-<!ENTITY button.createNewAccount.label "Create a New Account">
-<!ENTITY button.haveAccount.label      "I Have an Account">
-
-<!ENTITY setup.choicePage.title.label     "Have you used &syncBrand.fullName.label; before?">
-<!ENTITY setup.choicePage.new.label       "I've never used &syncBrand.shortName.label; before">
-<!ENTITY setup.choicePage.existing2.label "I'm already using &syncBrand.shortName.label; on another device">
-
-<!-- New Account AND Existing Account -->
-<!ENTITY server.label               "Server">
-<!ENTITY serverType.default.label      "Default: Mozilla &syncBrand.fullName.label; server">
-<!ENTITY serverType.custom2.label   "Use a custom server…">
-<!ENTITY signIn.account2.label      "Account">
-<!ENTITY signIn.account2.accesskey  "A">
-<!ENTITY signIn.password.label      "Password">
-<!ENTITY signIn.password.accesskey  "P">
-<!ENTITY signIn.recoveryKey.label       "Recovery Key">
-<!ENTITY signIn.recoveryKey.accesskey   "K">
-
-<!-- New Account Page 1: Basic Account Info -->
-<!ENTITY setup.newAccountDetailsPage.title.label "Account Details">
-<!ENTITY setup.emailAddress.label     "Email Address">
-<!ENTITY setup.emailAddress.accesskey "E">
-<!ENTITY setup.choosePassword.label      "Choose a Password">
-<!ENTITY setup.choosePassword.accesskey  "P">
-<!ENTITY setup.confirmPassword.label     "Confirm Password">
-<!ENTITY setup.confirmPassword.accesskey "m">
-
-<!-- LOCALIZATION NOTE: tosAgree1, tosLink, tosAgree2, ppLink, tosAgree3 are
-     joined with implicit white space, so spaces in the strings aren't necessary -->
-<!ENTITY setup.tosAgree1.label      "I agree to the">
-<!ENTITY setup.tosAgree1.accesskey  "a">
-<!ENTITY setup.tosLink.label        "Terms of Service">
-<!ENTITY setup.tosAgree2.label      "and the">
-<!ENTITY setup.ppLink.label         "Privacy Policy">
-<!ENTITY setup.tosAgree3.label      "">
-<!ENTITY setup.tosAgree2.accesskey  "">
-
-<!-- My Recovery Key dialog -->
-<!ENTITY setup.newRecoveryKeyPage.title.label "&brandShortName; Cares About Your Privacy">
-<!ENTITY setup.newRecoveryKeyPage.description.label "To ensure your total privacy, all of your data is encrypted prior to being uploaded. The Recovery Key which is necessary to decrypt your data is not uploaded.">
-<!ENTITY recoveryKeyEntry.label        "Your Recovery Key">
-<!ENTITY recoveryKeyEntry.accesskey    "K">
-<!ENTITY syncGenerateNewKey.label  "Generate a new key">
-<!ENTITY recoveryKeyBackup.description "Your Recovery Key is required to access &syncBrand.fullName.label; on other machines. Please create a backup copy. We cannot help you recover your Recovery Key.">
-
-<!ENTITY button.syncKeyBackup.print.label     "Print…">
-<!ENTITY button.syncKeyBackup.print.accesskey "P">
-<!ENTITY button.syncKeyBackup.save.label      "Save…">
-<!ENTITY button.syncKeyBackup.save.accesskey  "S">
-
-<!-- Existing Account Page 1: Pair a Device (incl. Pair a Device dialog strings) -->
-<!ENTITY pairDevice.title.label             "Pair a Device">
-<!ENTITY addDevice.showMeHow.label          "Show me how.">
-<!ENTITY addDevice.dontHaveDevice.label     "I don't have the device with me">
-<!ENTITY pairDevice.setup.description.label  "To activate, select &#x0022;Pair a Device&#x0022; on your other device.">
-<!ENTITY addDevice.setup.enterCode.label    "Then, enter this code:">
-<!ENTITY pairDevice.dialog.description.label "To activate your new device, select &#x0022;Set Up Sync&#x0022; on the device.">
-<!ENTITY addDevice.dialog.enterCode.label   "Enter the code that the device provides:">
-<!ENTITY addDevice.dialog.tryAgain.label    "Please try again.">
-<!ENTITY addDevice.dialog.successful.label  "The device has been successfully added. The initial synchronization can take several minutes and will finish in the background.">
-<!ENTITY addDevice.dialog.recoveryKey.label     "To activate your device you will need to enter your Recovery Key. Please print or save this key and take it with you.">
-<!ENTITY addDevice.dialog.connected.label   "Device Connected">
-
-<!-- Existing Account Page 2: Manual Login -->
-<!ENTITY setup.signInPage.title.label "Sign In">
-<!ENTITY existingRecoveryKey.description "You can get a copy of your Recovery Key by going to &syncBrand.shortName.label; Options on your other device, and selecting  &#x0022;My Recovery Key&#x0022; under &#x0022;Manage Account&#x0022;.">
-<!ENTITY verifying.label              "Verifying…">
-<!ENTITY resetPassword.label          "Reset Password">
-<!ENTITY resetSyncKey.label           "I have lost my other device.">
-
-<!-- Sync Options -->
-<!ENTITY setup.optionsPage.title      "Sync Options">
-<!ENTITY syncDeviceName.label       "Device Name:">
-<!ENTITY syncDeviceName.accesskey   "c">
-
-<!ENTITY syncMy.label               "Sync My">
-<!ENTITY engine.bookmarks.label     "Bookmarks">
-<!ENTITY engine.bookmarks.accesskey "m">
-<!ENTITY engine.tabs.label          "Tabs">
-<!ENTITY engine.tabs.accesskey      "T">
-<!ENTITY engine.history.label       "History">
-<!ENTITY engine.history.accesskey   "r">
-<!ENTITY engine.passwords.label     "Passwords">
-<!ENTITY engine.passwords.accesskey "P">
-<!ENTITY engine.prefs.label         "Preferences">
-<!ENTITY engine.prefs.accesskey     "S">
-<!ENTITY engine.addons.label        "Add-ons">
-<!ENTITY engine.addons.accesskey    "A">
-
-<!ENTITY choice2a.merge.main.label       "Merge this device's data with my &syncBrand.shortName.label; data">
-<!ENTITY choice2.merge.recommended.label "Recommended:">
-<!ENTITY choice2a.client.main.label      "Replace all data on this device with my &syncBrand.shortName.label; data">
-<!ENTITY choice2a.server.main.label      "Replace all other devices with this device's data">
-
-<!-- Confirm Merge Options -->
-<!ENTITY setup.optionsConfirmPage.title "Confirm">
-<!ENTITY confirm.merge2.label    "&syncBrand.fullName.label; will now merge all this device's browser data into your Sync account.">
-<!ENTITY confirm.client3.label         "Warning: The following &brandShortName; data on this device  will be deleted:">
-<!ENTITY confirm.client2.moreinfo.label "&brandShortName; will then copy your &syncBrand.fullName.label; data to this device.">
-<!ENTITY confirm.server2.label         "Warning: The following devices will be overwritten with your local data:">
-
-<!-- New & Existing Account: Setup Complete -->
-<!ENTITY setup.successPage.title "Setup Complete">
-<!ENTITY changeOptions.label "You can change this preference by selecting Sync Options below.">
-<!ENTITY continueUsing.label "You may now continue using &brandShortName;.">
deleted file mode 100644
--- a/browser/locales/en-US/chrome/browser/syncSetup.properties
+++ /dev/null
@@ -1,51 +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/.
-
-button.syncOptions.label       = Sync Options
-button.syncOptionsDone.label   = Done
-button.syncOptionsCancel.label = Cancel
-
-invalidEmail.label          = Invalid email address
-serverInvalid.label         = Please enter a valid server URL
-usernameNotAvailable.label  = Already in use
-
-verifying.label = Verifying…
-
-# LOCALIZATION NOTE (additionalClientCount.label):
-# Semicolon-separated list of plural forms. See:
-# http://developer.mozilla.org/en/docs/Localization_and_Plurals
-# #1 is the number of additional clients (was %S for a short while, use #1 instead, even if both work)
-additionalClientCount.label = and #1 additional device;and #1 additional devices
-# LOCALIZATION NOTE (bookmarksCount.label):
-# Semicolon-separated list of plural forms. See:
-# http://developer.mozilla.org/en/docs/Localization_and_Plurals
-# #1 is the number of bookmarks (was %S for a short while, use #1 instead, even if both work)
-bookmarksCount.label        = #1 bookmark;#1 bookmarks
-# LOCALIZATION NOTE (historyDaysCount.label):
-# Semicolon-separated list of plural forms. See:
-# http://developer.mozilla.org/en/docs/Localization_and_Plurals
-# #1 is the number of days (was %S for a short while, use #1 instead, even if both work)
-historyDaysCount.label      = #1 day of history;#1 days of history
-# LOCALIZATION NOTE (passwordsCount.label):
-# Semicolon-separated list of plural forms. See:
-# http://developer.mozilla.org/en/docs/Localization_and_Plurals
-# #1 is the number of passwords (was %S for a short while, use #1 instead, even if both work)
-passwordsCount.label        = #1 password;#1 passwords
-# LOCALIZATION NOTE (addonsCount.label): Semicolon-separated list of plural forms.
-# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
-# #1 is the number of add-ons, see the link above for forms
-addonsCount.label        = #1 addon;#1 addons
-
-save.recoverykey.title = Save Recovery Key
-save.recoverykey.defaultfilename = Firefox Recovery Key.html
-
-newAccount.action.label = Firefox Sync is now set up to automatically sync all of your browser data.
-newAccount.change.label = You can choose exactly what to sync by selecting Sync Options below.
-resetClient.change2.label = Firefox Sync will now merge all this device's browser data into your Sync account.
-wipeClient.change2.label = Firefox Sync will now replace all of the browser data on this device with the data in your Sync account.
-wipeRemote.change2.label = Firefox Sync will now replace all of the browser data in your Sync account with the data on this device.
-existingAccount.change.label = You can change this preference by selecting Sync Options below.
-
-# Several other strings are used (via Weave.Status.login), but they come from
-#  /services/sync
--- a/browser/locales/jar.mn
+++ b/browser/locales/jar.mn
@@ -1,16 +1,17 @@
 #filter substitution
 # 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/.
 
 
 @AB_CD@.jar:
 % locale browser @AB_CD@ %locale/browser/
+    locale/browser/aboutAccounts.dtd               (%chrome/browser/aboutAccounts.dtd)
     locale/browser/aboutCertError.dtd              (%chrome/browser/aboutCertError.dtd)
     locale/browser/aboutDialog.dtd                 (%chrome/browser/aboutDialog.dtd)
     locale/browser/aboutPrivateBrowsing.dtd        (%chrome/browser/aboutPrivateBrowsing.dtd)
     locale/browser/aboutRobots.dtd                 (%chrome/browser/aboutRobots.dtd)
     locale/browser/aboutHome.dtd                   (%chrome/browser/aboutHome.dtd)
 #ifdef MOZ_SERVICES_HEALTHREPORT
     locale/browser/aboutHealthReport.dtd           (%chrome/browser/aboutHealthReport.dtd)
 #endif
@@ -113,22 +114,16 @@
     locale/browser/preferences/privacy.dtd            (%chrome/browser/preferences/privacy.dtd)
     locale/browser/preferences/security.dtd           (%chrome/browser/preferences/security.dtd)
 #ifdef MOZ_SERVICES_SYNC
     locale/browser/preferences/sync.dtd               (%chrome/browser/preferences/sync.dtd)
 #endif
     locale/browser/preferences/tabs.dtd               (%chrome/browser/preferences/tabs.dtd)
 #ifdef MOZ_SERVICES_SYNC
     locale/browser/syncBrand.dtd                (%chrome/browser/syncBrand.dtd)
-    locale/browser/syncSetup.dtd                (%chrome/browser/syncSetup.dtd)
-    locale/browser/syncSetup.properties         (%chrome/browser/syncSetup.properties)
-    locale/browser/syncGenericChange.properties         (%chrome/browser/syncGenericChange.properties)
-    locale/browser/syncKey.dtd                  (%chrome/browser/syncKey.dtd)
-    locale/browser/syncQuota.dtd                (%chrome/browser/syncQuota.dtd)
-    locale/browser/syncQuota.properties         (%chrome/browser/syncQuota.properties)
 #endif
 % locale browser-region @AB_CD@ %locale/browser-region/
     locale/browser-region/region.properties        (%chrome/browser-region/region.properties)
 # the following files are browser-specific overrides
     locale/browser/netError.dtd                (%chrome/overrides/netError.dtd)
     locale/browser/appstrings.properties       (%chrome/overrides/appstrings.properties)
     locale/browser/downloads/settingsChange.dtd  (%chrome/overrides/settingsChange.dtd)
 % override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -270,19 +270,16 @@ browser.jar:
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-24-throbber.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
   skin/classic/browser/sync-desktopIcon.png
   skin/classic/browser/sync-mobileIcon.png
   skin/classic/browser/sync-notification-24.png
-  skin/classic/browser/syncSetup.css
-  skin/classic/browser/syncCommon.css
-  skin/classic/browser/syncQuota.css
   skin/classic/browser/syncProgress.css
 #endif
   skin/classic/browser/notification-pluginNormal.png  (../shared/plugins/notification-pluginNormal.png)
   skin/classic/browser/notification-pluginAlert.png   (../shared/plugins/notification-pluginAlert.png)
   skin/classic/browser/notification-pluginBlocked.png (../shared/plugins/notification-pluginBlocked.png)
   skin/classic/browser/devtools/tooltip/arrow-horizontal-dark.png   (../shared/devtools/tooltip/arrow-horizontal-dark.png)
   skin/classic/browser/devtools/tooltip/arrow-horizontal-dark@2x.png   (../shared/devtools/tooltip/arrow-horizontal-dark@2x.png)
   skin/classic/browser/devtools/tooltip/arrow-vertical-dark.png   (../shared/devtools/tooltip/arrow-vertical-dark.png)
deleted file mode 100644
--- a/browser/themes/linux/syncCommon.css
+++ /dev/null
@@ -1,49 +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/. */
-
-/* The following are used by both sync/setup.xul and sync/genericChange.xul */
-.status {
-  color: -moz-dialogtext;
-}
-
-.statusIcon {
-  -moz-margin-start: 4px;
-  max-height: 16px;
-  max-width: 16px;
-}
-
-.statusIcon[status="active"] {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-.statusIcon[status="error"] {
-  list-style-image: url("moz-icon://stock/gtk-dialog-error?size=menu");
-}
-
-.statusIcon[status="success"] {
-  list-style-image: url("moz-icon://stock/gtk-dialog-info?size=menu");
-}
-
-/* .data is only used by sync/genericChange.xul, but it seems unnecessary to have
-   a separate stylesheet for it. */
-.data {
-  font-size: 90%;
-  font-weight: bold;
-}
-
-dialog#change-dialog {
-  width: 40em;
-}
-
-image#syncIcon {
-  list-style-image: url("chrome://browser/skin/sync-32.png");
-}
-
-#introText {
-  margin-top: 2px;
-}
-
-#feedback {
-  height: 2em;
-}
deleted file mode 100644
--- a/browser/themes/linux/syncQuota.css
+++ /dev/null
@@ -1,26 +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/. */
-
-#quotaDialog {
-  width: 33em;
-  height: 25em;
-}
-
-treechildren::-moz-tree-checkbox {
-  list-style-image: none;
-}
-treechildren::-moz-tree-checkbox(checked) {
-  list-style-image: url("chrome://global/skin/checkbox/cbox-check.gif");
-}
-treechildren::-moz-tree-checkbox(disabled) {
-  list-style-image: url("chrome://global/skin/checkbox/cbox-check-dis.gif");
-}
-
-#treeCaption {
-  height: 4em;
-}
-
-.captionWarning {
-  font-weight: bold;
-}
deleted file mode 100644
--- a/browser/themes/linux/syncSetup.css
+++ /dev/null
@@ -1,127 +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/. */
-
-wizard {
-  -moz-appearance: none;
-  width: 55em;
-  height: 45em;
-  padding: 0;
-  background-color: Window;
-}
-
-.wizard-page-box {
-  -moz-appearance: none;
-  padding-left: 0;
-  padding-right: 0;
-  margin: 0;
-}
-
-wizardpage {
-  -moz-box-pack: center;
-  -moz-box-align: center;
-  margin: 0;
-  padding: 0 6em;
-  background-color: Window;
-}
-
-.wizard-header {
-  -moz-appearance: none;
-  border: none;
-  padding: 2em 0 1em 0;
-  text-align: center;
-}
-.wizard-header-label {
-  font-size: 24pt;
-  font-weight: normal;
-}
-
-.wizard-buttons {
-  background-color: rgba(0,0,0,0.1);
-  padding: 1em;
-}
-
-.wizard-buttons-separator {
-  visibility: collapse;
-}
-
-.wizard-header-icon {
-  visibility: collapse;
-}
-
-.accountChoiceButton {
-  font: menu;
-}
-
-.confirm {
-  border: 1px solid black;
-  padding: 1em;
-  border-radius: 5px;
-}
-
-/* Override the text-link style from global.css */
-description > .text-link,
-description > .text-link:focus {
-  margin: 0px;
-  padding: 0px;
-  border: 0px;
-}
-
-
-.success,
-.error {
-  padding: 2px;
-  border-radius: 2px;
-}
-
-.error {
-  background-color: #FF0000 !important;
-  color: #FFFFFF !important;
-}
-
-.success {
-  background-color: #00FF00 !important;
-}
-
-.warning {
-  font-weight: bold;
-  font-size: 100%;
-  color: red;
-}
-
-.mainDesc {
-  font-weight: bold;
-  font-size: 100%;
-}
-
-.normal {
-  font-size: 100%;
-}
-
-.inputColumn {
-  -moz-margin-end: 2px
-}
-
-.pin {
-  font-size: 18pt;
-  width: 4em;
-  text-align: center; 
-}
-
-#passphraseHelpSpacer {
-  width: 0.5em;
-}
-
-#pairDeviceThrobber > image,
-#login-throbber > image {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-#captchaFeedback {
-  visibility: hidden;
-}
-
-#successPageIcon {
-  /* TODO replace this with a 128px version (bug 591122) */
-  list-style-image: url("chrome://browser/skin/sync-32.png");
-}
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -371,19 +371,16 @@ browser.jar:
   skin/classic/browser/sync-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
   skin/classic/browser/sync-desktopIcon.png
   skin/classic/browser/sync-mobileIcon.png
   skin/classic/browser/sync-notification-24.png
-  skin/classic/browser/syncSetup.css
-  skin/classic/browser/syncCommon.css
-  skin/classic/browser/syncQuota.css
   skin/classic/browser/syncProgress.css
 #endif
   skin/classic/browser/lion/keyhole-circle.png              (keyhole-circle-lion.png)
   skin/classic/browser/keyhole-circle@2x.png                (keyhole-circle-lion@2x.png)
   skin/classic/browser/Toolbar-background-noise.png         (Toolbar-background-noise.png)
   skin/classic/browser/lion/toolbarbutton-dropmarker.png    (toolbarbutton-dropmarker-lion.png)
   skin/classic/browser/toolbarbutton-dropmarker@2x.png      (toolbarbutton-dropmarker-lion@2x.png)
   skin/classic/browser/lion/tabbrowser/alltabs-box-bkgnd-icon.png      (tabbrowser/alltabs-box-bkgnd-icon-lion.png)
deleted file mode 100644
--- a/browser/themes/osx/syncCommon.css
+++ /dev/null
@@ -1,49 +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/. */
-
-/* The following are used by both sync/setup.xul and sync/genericChange.xul */
-.status {
-  color: -moz-dialogtext;
-}
-
-.statusIcon {
-  -moz-margin-start: 4px;
-  max-height: 16px;
-  max-width: 16px;
-}
-
-.statusIcon[status="active"] {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-.statusIcon[status="error"] {
-  list-style-image: url("chrome://global/skin/icons/error-16.png");
-}
-
-.statusIcon[status="success"] {
-  list-style-image: url("chrome://global/skin/icons/information-16.png");
-}
-
-/* .data is only used by sync/genericChange.xul, but it seems unnecessary to have
-   a separate stylesheet for it. */
-.data {
-  font-size: 90%;
-  font-weight: bold;
-}
-
-dialog#change-dialog {
-  width: 40em;
-}
-
-image#syncIcon {
-  list-style-image: url("chrome://browser/skin/sync-32.png");
-}
-
-#introText {
-  margin-top: 2px;
-}
-
-#feedback {
-  height: 2em;
-}
deleted file mode 100644
--- a/browser/themes/osx/syncQuota.css
+++ /dev/null
@@ -1,26 +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/. */
-
-#quotaDialog {
-  width: 33em;
-  height: 25em;
-}
-
-treechildren::-moz-tree-checkbox {
-  list-style-image: none;
-}
-treechildren::-moz-tree-checkbox(checked) {
-  list-style-image: url("chrome://global/skin/checkbox/cbox-check.gif");
-}
-treechildren::-moz-tree-checkbox(disabled) {
-  list-style-image: url("chrome://global/skin/checkbox/cbox-check-dis.gif");
-}
-
-#treeCaption {
-  height: 4em;
-}
-
-.captionWarning {
-  font-weight: bold;
-}
deleted file mode 100644
--- a/browser/themes/osx/syncSetup.css
+++ /dev/null
@@ -1,126 +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/. */
-
-wizard {
-  -moz-appearance: none;
-  width: 55em;
-  height: 45em;
-  padding: 0;
-  background-color: Window;
-}
-
-.wizard-page-box {
-  -moz-appearance: none;
-  padding-left: 0;
-  padding-right: 0;
-  margin: 0;
-}
-
-wizardpage {
-  -moz-box-pack: center;
-  -moz-box-align: center;
-  margin: 0;
-  padding: 0 6em;
-  background-color: Window;
-}
-
-.wizard-header {
-  -moz-appearance: none;
-  border: none;
-  padding: 2em 0 1em 0;
-  text-align: center;
-}
-.wizard-header-label {
-  font-size: 24pt;
-  font-weight: normal;
-}
-
-.wizard-buttons {
-  background-color: rgba(0,0,0,0.1);
-  padding: 1em;
-}
-
-.wizard-buttons-separator {
-  visibility: collapse;
-}
-
-.wizard-header-icon {
-  visibility: collapse;
-}
-
-.accountChoiceButton {
-  font: menu;
-}
-
-.confirm {
-  border: 1px solid black;
-  padding: 1em;
-  border-radius: 5px;
-}
-
-/* Override the text-link style from global.css */
-description > .text-link,
-description > .text-link:focus {
-  margin: 0px;
-  padding: 0px;
-  border: 0px;
-}
-
-.success,
-.error {
-  padding: 2px;
-  border-radius: 2px;
-}
-
-.error {
-  background-color: #FF0000 !important;
-  color: #FFFFFF !important;
-}
-
-.success {
-  background-color: #00FF00 !important;
-}
-
-.warning {
-  font-weight: bold;
-  font-size: 100%;
-  color: red;
-}
-
-.mainDesc {
-  font-weight: bold;
-  font-size: 100%;
-}
-
-.normal {
-  font-size: 100%;
-}
-
-.inputColumn {
-  -moz-margin-end: 2px
-}
-
-.pin {
-  font-size: 18pt;
-  width: 4em;
-  text-align: center; 
-}
-
-#passphraseHelpSpacer {
-  width: 0.5em;
-}
-
-#pairDeviceThrobber > image,
-#login-throbber > image {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-#captchaFeedback {
-  visibility: hidden;
-}
-
-#successPageIcon {
-  /* TODO replace this with a 128px version (bug 591122) */
-  list-style-image: url("chrome://browser/skin/sync-32.png");
-}
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -296,19 +296,16 @@ browser.jar:
         skin/classic/browser/sync-throbber.png
         skin/classic/browser/sync-16.png
         skin/classic/browser/sync-32.png
         skin/classic/browser/sync-128.png
         skin/classic/browser/sync-bg.png
         skin/classic/browser/sync-desktopIcon.png
         skin/classic/browser/sync-mobileIcon.png
         skin/classic/browser/sync-notification-24.png
-        skin/classic/browser/syncSetup.css
-        skin/classic/browser/syncCommon.css
-        skin/classic/browser/syncQuota.css
         skin/classic/browser/syncProgress.css
 #endif
         skin/classic/browser/devtools/tooltip/arrow-horizontal-dark.png   (../shared/devtools/tooltip/arrow-horizontal-dark.png)
         skin/classic/browser/devtools/tooltip/arrow-horizontal-dark@2x.png   (../shared/devtools/tooltip/arrow-horizontal-dark@2x.png)
         skin/classic/browser/devtools/tooltip/arrow-vertical-dark.png   (../shared/devtools/tooltip/arrow-vertical-dark.png)
         skin/classic/browser/devtools/tooltip/arrow-vertical-dark@2x.png   (../shared/devtools/tooltip/arrow-vertical-dark@2x.png)
         skin/classic/browser/devtools/tooltip/arrow-horizontal-light.png   (../shared/devtools/tooltip/arrow-horizontal-light.png)
         skin/classic/browser/devtools/tooltip/arrow-horizontal-light@2x.png   (../shared/devtools/tooltip/arrow-horizontal-light@2x.png)
@@ -604,19 +601,16 @@ browser.jar:
         skin/classic/aero/browser/sync-throbber.png
         skin/classic/aero/browser/sync-16.png
         skin/classic/aero/browser/sync-32.png
         skin/classic/aero/browser/sync-128.png
         skin/classic/aero/browser/sync-bg.png
         skin/classic/aero/browser/sync-desktopIcon.png
         skin/classic/aero/browser/sync-mobileIcon.png
         skin/classic/aero/browser/sync-notification-24.png
-        skin/classic/aero/browser/syncSetup.css
-        skin/classic/aero/browser/syncCommon.css
-        skin/classic/aero/browser/syncQuota.css
         skin/classic/aero/browser/syncProgress.css
 #endif
 #endif
         skin/classic/aero/browser/devtools/tooltip/arrow-horizontal-dark.png   (../shared/devtools/tooltip/arrow-horizontal-dark.png)
         skin/classic/aero/browser/devtools/tooltip/arrow-horizontal-dark@2x.png   (../shared/devtools/tooltip/arrow-horizontal-dark@2x.png)
         skin/classic/aero/browser/devtools/tooltip/arrow-vertical-dark.png   (../shared/devtools/tooltip/arrow-vertical-dark.png)
         skin/classic/aero/browser/devtools/tooltip/arrow-vertical-dark@2x.png   (../shared/devtools/tooltip/arrow-vertical-dark@2x.png)
         skin/classic/aero/browser/devtools/tooltip/arrow-horizontal-light.png   (../shared/devtools/tooltip/arrow-horizontal-light.png)
deleted file mode 100644
--- a/browser/themes/windows/syncCommon.css
+++ /dev/null
@@ -1,49 +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/. */
-
-/* The following are used by both sync/setup.xul and sync/genericChange.xul */
-.status {
-  color: -moz-dialogtext;
-}
-
-.statusIcon {
-  -moz-margin-start: 4px;
-  max-height: 16px;
-  max-width: 16px;
-}
-
-.statusIcon[status="active"] {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-.statusIcon[status="error"] {
-  list-style-image: url("chrome://global/skin/icons/error-16.png");
-}
-
-.statusIcon[status="success"] {
-  list-style-image: url("chrome://global/skin/icons/information-16.png");
-}
-
-/* .data is only used by sync/genericChange.xul, but it seems unnecessary to have
-   a separate stylesheet for it. */
-.data {
-  font-size: 90%;
-  font-weight: bold;
-}
-
-dialog#change-dialog {
-  width: 40em;
-}
-
-image#syncIcon {
-  list-style-image: url("chrome://browser/skin/sync-32.png");
-}
-
-#introText {
-  margin-top: 2px;
-}
-
-#feedback {
-  height: 2em;
-}
deleted file mode 100644
--- a/browser/themes/windows/syncQuota.css
+++ /dev/null
@@ -1,26 +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/. */
-
-#quotaDialog {
-  width: 33em;
-  height: 25em;
-}
-
-treechildren::-moz-tree-checkbox {
-  list-style-image: none;
-}
-treechildren::-moz-tree-checkbox(checked) {
-  list-style-image: url("chrome://global/skin/checkbox/cbox-check.gif");
-}
-treechildren::-moz-tree-checkbox(disabled) {
-  list-style-image: url("chrome://global/skin/checkbox/cbox-check-dis.gif");
-}
-
-#treeCaption {
-  height: 4em;
-}
-
-.captionWarning {
-  font-weight: bold;
-}
deleted file mode 100644
--- a/browser/themes/windows/syncSetup.css
+++ /dev/null
@@ -1,132 +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/. */
-
-wizard {
-  -moz-appearance: none;
-  width: 55em;
-  height: 45em;
-  padding: 0;
-  background-color: Window;
-}
-
-.wizard-page-box {
-  -moz-appearance: none;
-  padding-left: 0;
-  padding-right: 0;
-  margin: 0;
-}
-
-wizardpage {
-  -moz-box-pack: center;
-  -moz-box-align: center;
-  margin: 0;
-  padding: 0 6em;
-  background-color: Window;
-}
-
-.wizard-header {
-  -moz-appearance: none;
-  border: none;
-  padding: 2em 0 1em 0;
-  text-align: center;
-}
-.wizard-header-label {
-  font-size: 24pt;
-  font-weight: normal;
-}
-
-.wizard-buttons {
-  background-color: rgba(0,0,0,0.1);
-  padding: 1em;
-}
-
-.wizard-buttons-separator {
-  visibility: collapse;
-}
-
-.wizard-header-icon {
-  visibility: collapse;
-}
-
-.accountChoiceButton {
-  font: menu;
-}
-
-.confirm {
-  border: 1px solid black;
-  padding: 1em;
-  border-radius: 5px;
-}
-
-/* Override the text-link style from global.css */
-description > .text-link,
-description > .text-link:focus {
-  margin: 0px;
-  padding: 0px;
-  border: 0px;
-}
-
-
-.success,
-.error {
-  padding: 2px;
-  border-radius: 2px;
-}
-
-.error {
-  background-color: #FF0000 !important;
-  color: #FFFFFF !important;
-}
-
-.success {
-  background-color: #00FF00 !important;
-}
-
-.warning {
-  font-weight: bold;
-  font-size: 100%;
-  color: red;
-}
-
-.mainDesc {
-  font-weight: bold;
-  font-size: 100%;
-}
-
-.normal {
-  font-size: 100%;
-}
-
-.inputColumn {
-  -moz-margin-end: 2px
-}
-
-.pin {
-  font-size: 18pt;
-  width: 4em;
-  text-align: center; 
-}
-
-#passphraseHelpSpacer {
-  width: 0.5em;
-}
-
-#pairDeviceThrobber > image,
-#login-throbber > image {
-  list-style-image: url("chrome://global/skin/icons/loading_16.png");
-}
-
-#captchaFeedback {
-  visibility: hidden;
-}
-
-#successPageIcon {
-  /* TODO replace this with a 128px version (bug 591122) */
-  list-style-image: url("chrome://browser/skin/sync-32.png");
-}
-
-#tosDesc {
-  margin-left: -7px;
-  margin-bottom: 3px;
-}
\ No newline at end of file
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -789,16 +789,19 @@ pref("gfx.canvas.azure.accelerated", tru
 pref("general.useragent.override.youtube.com", "Android; Tablet;#Android; Mobile;");
 
 // When true, phone number linkification is enabled.
 pref("browser.ui.linkify.phone", false);
 
 // Enables/disables Spatial Navigation
 pref("snav.enabled", true);
 
+// URL to fetch about:accounts web content from.
+pref("firefox.accounts.remoteUrl", "https://accounts.dev.lcip.org/mobile");
+
 // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
 // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
 // repackager of this code using an alternate snippet url, please keep your users safe
 pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
 
 // How frequently we check for new snippets, in seconds (1 day)
 pref("browser.snippets.updateInterval", 86400);
 
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -8,16 +8,17 @@
 #ifdef MOZ_ANDROID_SHARED_ID
       android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
 #endif
       >
     <uses-sdk android:minSdkVersion="8"
               android:targetSdkVersion="16"/>
 
 #include ../services/manifests/AnnouncementsAndroidManifest_permissions.xml.in
+#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
 #include ../services/manifests/HealthReportAndroidManifest_permissions.xml.in
 #include ../services/manifests/SyncAndroidManifest_permissions.xml.in
 
 #ifndef RELEASE_BUILD
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
 #endif
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -211,16 +212,17 @@
                   android:theme="@style/Gecko">
           <intent-filter>
             <action android:name="org.mozilla.gecko.restart"/>
             <action android:name="org.mozilla.gecko.restart_update"/>
           </intent-filter>
         </activity>
 
 #include ../services/manifests/AnnouncementsAndroidManifest_activities.xml.in
+#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
 #include ../services/manifests/SyncAndroidManifest_activities.xml.in
 #include ../services/manifests/HealthReportAndroidManifest_activities.xml.in
 
 #if MOZ_CRASHREPORTER
   <activity android:name="org.mozilla.gecko.CrashReporter"
             android:label="@string/crash_reporter_title"
             android:icon="@drawable/crash_reporter"
             android:theme="@style/Gecko"
@@ -241,41 +243,45 @@
 
         <activity android:name="org.mozilla.gecko.preferences.GeckoPreferences"
                   android:theme="@style/Gecko.Preferences"
                   android:label="@string/settings_title"
                   android:configChanges="orientation|screenSize"
                   android:excludeFromRecents="true"/>
 
         <provider android:name="org.mozilla.gecko.db.BrowserProvider"
+                  android:label="@string/sync_configure_engines_title_bookmarks"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.browser"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER">
 
             <path-permission android:pathPrefix="/search_suggest_query"
                              android:readPermission="android.permission.GLOBAL_SEARCH" />
 
         </provider>
 
         <!--
           Ensure that passwords provider runs in its own process. (Bug 718760.)
           Process name is per-application to avoid loading CPs from multiple
           Fennec versions into the same process. (Bug 749727.)
           Process name is a mangled version to avoid a Talos bug. (Bug 750548.)
           -->
         <provider android:name="org.mozilla.gecko.db.PasswordsProvider"
+                  android:label="@string/sync_configure_engines_title_passwords"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.passwords"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.PASSWORD_PROVIDER"
                   android:process="@MANGLED_ANDROID_PACKAGE_NAME@.PasswordsProvider"/>
 
         <provider android:name="org.mozilla.gecko.db.FormHistoryProvider"
+                  android:label="@string/sync_configure_engines_title_history"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.formhistory"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.FORMHISTORY_PROVIDER"
                   android:protectionLevel="signature"/>
 
         <provider android:name="org.mozilla.gecko.db.TabsProvider"
+                  android:label="@string/sync_configure_engines_title_tabs"
                   android:authorities="@ANDROID_PACKAGE_NAME@.db.tabs"
                   android:permission="@ANDROID_PACKAGE_NAME@.permissions.BROWSER_PROVIDER"/>
 
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.updater.UpdateService"
             android:process="@MANGLED_ANDROID_PACKAGE_NAME@.UpdateService">
         </service>
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -171,16 +171,18 @@ abstract public class BrowserApp extends
     private boolean mDynamicToolbarCanScroll = false;
 
     private Integer mPrefObserverId;
 
     private SharedPreferencesHelper mSharedPreferencesHelper;
 
     private OrderedBroadcastHelper mOrderedBroadcastHelper;
 
+    private FirefoxAccountsHelper mFirefoxAccountsHelper;
+
     private BrowserHealthReporter mBrowserHealthReporter;
 
     // The tab to be selected on editing mode exit.
     private Integer mTargetTabForEditingMode = null;
 
     // The animator used to toggle HomePager visibility has a race where if the HomePager is shown
     // (starting the animation), the HomePager is hidden, and the HomePager animation completes,
     // both the web content and the HomePager will be hidden. This flag is used to prevent the
@@ -535,16 +537,17 @@ abstract public class BrowserApp extends
         registerEventListener("Telemetry:Gather");
         registerEventListener("Settings:Show");
         registerEventListener("Updater:Launch");
 
         Distribution.init(this);
         JavaAddonManager.getInstance().init(getApplicationContext());
         mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
         mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
+        mFirefoxAccountsHelper = new FirefoxAccountsHelper(getApplicationContext());
         mBrowserHealthReporter = new BrowserHealthReporter();
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 nfc.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
                     @Override
                     public NdefMessage createNdefMessage(NfcEvent event) {
@@ -827,16 +830,21 @@ abstract public class BrowserApp extends
             mSharedPreferencesHelper = null;
         }
 
         if (mOrderedBroadcastHelper != null) {
             mOrderedBroadcastHelper.uninit();
             mOrderedBroadcastHelper = null;
         }
 
+        if (mFirefoxAccountsHelper != null) {
+            mFirefoxAccountsHelper.uninit();
+            mFirefoxAccountsHelper = null;
+        }
+
         if (mBrowserHealthReporter != null) {
             mBrowserHealthReporter.uninit();
             mBrowserHealthReporter = null;
         }
 
         unregisterEventListener("CharEncoding:Data");
         unregisterEventListener("CharEncoding:State");
         unregisterEventListener("Feedback:LastUrl");
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/FirefoxAccountsHelper.java
@@ -0,0 +1,126 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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;
+
+import org.mozilla.gecko.util.EventDispatcher;
+import org.mozilla.gecko.util.GeckoEventListener;
+
+import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.accounts.Account;
+import android.content.Context;
+
+import android.util.Log;
+
+/**
+ * Helper class to manage Firefox Accounts.
+ *
+ * Listens for messages starting "FxAccount:" from Javascript, and creates a new
+ * Android account object in response to "FxAccount:{Verified,Login}".
+ *
+ * "FxAccount:Create" is ignored: it corresponds to a user creating a Firefox
+ * Account on the server; but they have not yet completed an email verification
+ * loop.  The provided data will not include the users keys.
+ *
+ * "FxAccount:Verified" corresponds to a user signing up: creating a Firefox
+ * Account on the server, and then completing an email verification loop while
+ * still in the about:accounts window.
+ *
+ * "FxAccount:Login" corresponds to a user signing in to an existing verified
+ * Firefox Account.
+ */
+public final class FirefoxAccountsHelper
+             implements GeckoEventListener
+{
+    public static final String LOGTAG = "FxAcctsHelper";
+
+    // For extra debugging.  Not final so it can be changed by an add-on.
+    public static boolean LOG_PERSONAL_INFORMATION = false;
+
+    public static final String EVENT_CREATE   = "FxAccount:Create";
+    public static final String EVENT_LOGIN    = "FxAccount:Login";
+    public static final String EVENT_VERIFIED = "FxAccount:Verified";
+
+    protected final Context mContext;
+
+    public FirefoxAccountsHelper(Context context) {
+        mContext = context;
+
+        EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
+        if (dispatcher == null) {
+            Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
+            return;
+        }
+        dispatcher.registerEventListener(EVENT_CREATE, this);
+        dispatcher.registerEventListener(EVENT_LOGIN, this);
+        dispatcher.registerEventListener(EVENT_VERIFIED, this);
+    }
+
+    public synchronized void uninit() {
+        EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
+        if (dispatcher == null) {
+            Log.e(LOGTAG, "Gecko event dispatcher must not be null", new RuntimeException());
+            return;
+        }
+        dispatcher.unregisterEventListener(EVENT_CREATE, this);
+        dispatcher.unregisterEventListener(EVENT_LOGIN, this);
+        dispatcher.unregisterEventListener(EVENT_VERIFIED, this);
+    }
+
+    @Override
+    public void handleMessage(String event, JSONObject message) {
+        Log.i(LOGTAG, "FirefoxAccountsHelper got event " + event);
+        if (!(EVENT_CREATE.equals(event) ||
+              EVENT_LOGIN.equals(event) ||
+              EVENT_VERIFIED.equals(event))) {
+            Log.e(LOGTAG, "FirefoxAccountsHelper got unexpected event " + event);
+            return;
+        }
+
+        if (EVENT_CREATE.equals(event)) {
+            Log.i(LOGTAG, "FirefoxAccountsHelper ignoring event " + event);
+            return;
+        }
+
+        try {
+            final JSONObject data = message.getJSONObject("data");
+            if (data == null) {
+                Log.e(LOGTAG, "data must not be null");
+                return;
+            }
+
+            if (LOG_PERSONAL_INFORMATION) {
+                Log.w(LOGTAG, "data: " + data.toString());
+            }
+
+            String email = data.optString("email");
+            String uid = data.optString("uid");
+            String sessionToken = data.optString("sessionToken");
+            String kA = data.optString("kA");
+            String kB = data.optString("kB");
+
+            if (LOG_PERSONAL_INFORMATION) {
+                Log.w(LOGTAG, "email: " + email);
+                Log.w(LOGTAG, "uid: " + uid);
+                Log.w(LOGTAG, "sessionToken: " + sessionToken);
+                Log.w(LOGTAG, "kA: " + kA);
+                Log.w(LOGTAG, "kB: " + kB);
+            }
+
+            Account account = FxAccountAuthenticator.addAccount(mContext, email, uid, sessionToken, kA, kB);
+            if (account == null) {
+                Log.e(LOGTAG, "Got null adding FxAccount.");
+                return;
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "Got exception in handleMessage handling event " + event, e);
+            return;
+        }
+    }
+}
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -29,16 +29,17 @@ endif
 UA_BUILDID=$(shell echo $(ANDROID_VERSION_CODE) | cut -c1-8)
 
 MOZ_BUILD_TIMESTAMP=$(shell echo `$(PYTHON) $(topsrcdir)/toolkit/xre/make-platformini.py --print-timestamp`)
 
 DEFINES += \
   -DANDROID_VERSION_CODE=$(ANDROID_VERSION_CODE) \
   -DMOZ_ANDROID_SHARED_ID="$(MOZ_ANDROID_SHARED_ID)" \
   -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE)" \
+  -DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE="$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE)" \
   -DMOZ_APP_BUILDID=$(MOZ_APP_BUILDID) \
   -DMOZ_BUILD_TIMESTAMP=$(MOZ_BUILD_TIMESTAMP) \
   -DUA_BUILDID=$(UA_BUILDID) \
   $(NULL)
 
 GARBAGE += \
   AndroidManifest.xml  \
   classes.dex  \
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -494,16 +494,21 @@ sync_java_files = [
     'background/common/log/writers/PrintLogWriter.java',
     'background/common/log/writers/SimpleTagLogWriter.java',
     'background/common/log/writers/StringLogWriter.java',
     'background/common/log/writers/TagLogWriter.java',
     'background/common/log/writers/ThreadLocalTagLogWriter.java',
     'background/datareporting/TelemetryRecorder.java',
     'background/db/CursorDumper.java',
     'background/db/Tab.java',
+    'background/fxa/FxAccount10AuthDelegate.java',
+    'background/fxa/FxAccount10CreateDelegate.java',
+    'background/fxa/FxAccountClient.java',
+    'background/fxa/FxAccountClientException.java',
+    'background/fxa/FxAccountUtils.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',
     'background/healthreport/HealthReportDatabaseStorage.java',
     'background/healthreport/HealthReportGenerator.java',
@@ -515,16 +520,39 @@ sync_java_files = [
     'background/healthreport/prune/PrunePolicy.java',
     'background/healthreport/prune/PrunePolicyDatabaseStorage.java',
     'background/healthreport/prune/PrunePolicyStorage.java',
     'background/healthreport/upload/AndroidSubmissionClient.java',
     'background/healthreport/upload/HealthReportUploadService.java',
     'background/healthreport/upload/ObsoleteDocumentTracker.java',
     'background/healthreport/upload/SubmissionClient.java',
     'background/healthreport/upload/SubmissionPolicy.java',
+    'browserid/ASNUtils.java',
+    'browserid/BrowserIDKeyPair.java',
+    'browserid/DSACryptoImplementation.java',
+    'browserid/JSONWebTokenUtils.java',
+    'browserid/MockMyIDTokenFactory.java',
+    'browserid/RSACryptoImplementation.java',
+    'browserid/SigningPrivateKey.java',
+    'browserid/verifier/BrowserIDRemoteVerifierClient.java',
+    'browserid/verifier/BrowserIDVerifierClient.java',
+    'browserid/verifier/BrowserIDVerifierDelegate.java',
+    'browserid/verifier/BrowserIDVerifierException.java',
+    'browserid/VerifyingPublicKey.java',
+    'fxa/activities/FxAccountSetupActivity.java',
+    'fxa/authenticator/FxAccountAuthenticator.java',
+    'fxa/authenticator/FxAccountAuthenticatorService.java',
+    'fxa/sync/FxAccount.java',
+    'fxa/sync/FxAccountBookmarksSyncService.java',
+    'fxa/sync/FxAccountGlobalSession.java',
+    'fxa/sync/FxAccountHistorySyncService.java',
+    'fxa/sync/FxAccountPasswordsSyncService.java',
+    'fxa/sync/FxAccountSyncAdapter.java',
+    'fxa/sync/FxAccountSyncService.java',
+    'fxa/sync/FxAccountTabsSyncService.java',
     'sync/AlreadySyncingException.java',
     'sync/CollectionKeys.java',
     'sync/CommandProcessor.java',
     'sync/CommandRunner.java',
     'sync/config/AccountPickler.java',
     'sync/config/activities/SelectEnginesActivity.java',
     'sync/config/ClientRecordTerminator.java',
     'sync/config/ConfigurationMigrator.java',
@@ -594,16 +622,17 @@ sync_java_files = [
     'sync/net/BrowserIDAuthHeaderProvider.java',
     'sync/net/ConnectionMonitorThread.java',
     'sync/net/HandleProgressException.java',
     'sync/net/HawkAuthHeaderProvider.java',
     'sync/net/HMACAuthHeaderProvider.java',
     'sync/net/HttpResponseObserver.java',
     'sync/net/Resource.java',
     'sync/net/ResourceDelegate.java',
+    'sync/net/SRPConstants.java',
     'sync/net/SyncResponse.java',
     'sync/net/SyncStorageCollectionRequest.java',
     'sync/net/SyncStorageCollectionRequestDelegate.java',
     'sync/net/SyncStorageRecordRequest.java',
     'sync/net/SyncStorageRequest.java',
     'sync/net/SyncStorageRequestDelegate.java',
     'sync/net/SyncStorageRequestIncrementalDelegate.java',
     'sync/net/SyncStorageResponse.java',
@@ -761,16 +790,21 @@ sync_java_files = [
     'sync/synchronizer/SynchronizerSessionDelegate.java',
     'sync/synchronizer/UnbundleError.java',
     'sync/synchronizer/UnexpectedSessionException.java',
     'sync/SynchronizerConfiguration.java',
     'sync/ThreadPool.java',
     'sync/UnexpectedJSONException.java',
     'sync/UnknownSynchronizerConfigurationVersionException.java',
     'sync/Utils.java',
+    'tokenserver/TokenServerClient.java',
+    'tokenserver/TokenServerClientDelegate.java',
+    'tokenserver/TokenServerException.java',
+    'tokenserver/TokenServerToken.java',
 ]
 
 sync_generated_java_files = [
     'org/mozilla/gecko/background/announcements/AnnouncementsConstants.java',
     'org/mozilla/gecko/background/common/GlobalConstants.java',
     'org/mozilla/gecko/background/healthreport/HealthReportConstants.java',
+    'org/mozilla/gecko/fxa/FxAccountConstants.java',
     'org/mozilla/gecko/sync/SyncConstants.java',
 ]
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/FxAccount10AuthDelegate.java
@@ -0,0 +1,218 @@
+/* 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 java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.SRPConstants;
+
+public class FxAccount10AuthDelegate implements FxAccountClient.AuthDelegate {
+  // Fixed by protocol.
+  protected final BigInteger N;
+  protected final BigInteger g;
+  protected final int modNLengthBytes;
+
+  // Configured at construction time.
+  protected final String email;
+  protected final byte[] stretchedPWBytes;
+
+  // Encapsulate state.
+  protected static class AuthState {
+    protected String srpToken;
+    protected String mainSalt;
+    protected String srpSalt;
+
+    protected BigInteger x;
+    protected BigInteger A;
+    protected byte[] Kbytes;
+    protected byte[] Mbytes;
+  }
+
+  // State should be written exactly once.
+  protected AuthState internalAuthState = null;
+
+  public FxAccount10AuthDelegate(String email, byte[] stretchedPWBytes) {
+    this.email = email;
+    this.stretchedPWBytes = stretchedPWBytes;
+    this.N = SRPConstants._2048.N;
+    this.g = SRPConstants._2048.g;
+    this.modNLengthBytes = SRPConstants._2048.byteLength;
+  }
+
+  protected BigInteger generateSecretValue() {
+    return Utils.generateBigIntegerLessThan(N);
+  }
+
+  public static class FxAccountClientMalformedAuthException extends FxAccountClientException {
+    private static final long serialVersionUID = 3585262174699395505L;
+
+    public FxAccountClientMalformedAuthException(String detailMessage) {
+      super(detailMessage);
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public JSONObject getAuthStartBody() throws FxAccountClientException {
+    try {
+      final JSONObject body = new JSONObject();
+      body.put("email", FxAccountUtils.bytes(email));
+      return body;
+    } catch (UnsupportedEncodingException e) {
+      throw new FxAccountClientException(e);
+    }
+  }
+
+  @Override
+  public void onAuthStartResponse(final ExtendedJSONObject body) throws FxAccountClientException {
+    if (this.internalAuthState != null) {
+      throw new FxAccountClientException("auth must not be written before calling onAuthStartResponse");
+    }
+
+    String srpToken = null;
+    String srpSalt = null;
+    String srpB = null;
+    String mainSalt = null;
+
+    try {
+      srpToken = body.getString("srpToken");
+      if (srpToken == null) {
+        throw new FxAccountClientMalformedAuthException("srpToken must be a non-null object");
+      }
+      ExtendedJSONObject srp = body.getObject("srp");
+      if (srp == null) {
+        throw new FxAccountClientMalformedAuthException("srp must be a non-null object");
+      }
+      srpSalt = srp.getString("salt");
+      if (srpSalt == null) {
+        throw new FxAccountClientMalformedAuthException("srp.salt must not be null");
+      }
+      srpB = srp.getString("B");
+      if (srpB == null) {
+        throw new FxAccountClientMalformedAuthException("srp.B must not be null");
+      }
+      ExtendedJSONObject passwordStretching = body.getObject("passwordStretching");
+      if (passwordStretching == null) {
+        throw new FxAccountClientMalformedAuthException("passwordStretching must be a non-null object");
+      }
+      mainSalt = passwordStretching.getString("salt");
+      if (mainSalt == null) {
+        throw new FxAccountClientMalformedAuthException("srp.passwordStretching.salt must not be null");
+      }
+      throwIfParametersAreBad(passwordStretching);
+
+      this.internalAuthState = authStateFromParameters(srpToken, mainSalt, srpSalt, srpB, generateSecretValue());
+    } catch (FxAccountClientException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new FxAccountClientException(e);
+    }
+  }
+
+  /**
+   * Expect object like:
+   * "passwordStretching": {
+   *   "type": "PBKDF2/scrypt/PBKDF2/v1",
+   *   "PBKDF2_rounds_1": 20000,
+   *   "scrypt_N": 65536,
+   *   "scrypt_r": 8,
+   *   "scrypt_p": 1,
+   *   "PBKDF2_rounds_2": 20000,
+   *   "salt": "996bc6b1aa63cd69856a2ec81cbf19d5c8a604713362df9ee15c2bf07128efab"
+   * }
+   * @param params to verify.
+   * @throws FxAccountClientMalformedAuthException
+   */
+  protected void throwIfParametersAreBad(ExtendedJSONObject params) throws FxAccountClientMalformedAuthException {
+    if (params == null ||
+        params.size() != 7 ||
+        params.getString("salt") == null ||
+        !("PBKDF2/scrypt/PBKDF2/v1".equals(params.getString("type"))) ||
+        20000 != params.getLong("PBKDF2_rounds_1") ||
+        65536 != params.getLong("scrypt_N") ||
+        8 != params.getLong("scrypt_r") ||
+        1 != params.getLong("scrypt_p") ||
+        20000 != params.getLong("PBKDF2_rounds_2")) {
+      throw new FxAccountClientMalformedAuthException("malformed passwordStretching parameters: '" + params.toJSONString() + "'.");
+    }
+  }
+
+  /**
+   * All state is written in this method.
+   */
+  protected AuthState authStateFromParameters(String srpToken, String mainSalt, String srpSalt, String srpB, BigInteger a) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+    AuthState authState = new AuthState();
+    authState.srpToken = srpToken;
+    authState.mainSalt = mainSalt;
+    authState.srpSalt = srpSalt;
+
+    authState.x = FxAccountUtils.srpVerifierLowercaseX(email.getBytes("UTF-8"), this.stretchedPWBytes, Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES));
+
+    authState.A = g.modPow(a, N);
+    String srpA = FxAccountUtils.hexModN(authState.A, N);
+    BigInteger B = new BigInteger(srpB, 16);
+
+    byte[] srpABytes = Utils.hex2Byte(srpA, modNLengthBytes);
+    byte[] srpBBytes = Utils.hex2Byte(srpB, modNLengthBytes);
+
+    // u = H(pad(A) | pad(B))
+    byte[] uBytes = Utils.sha256(Utils.concatAll(
+        srpABytes,
+        srpBBytes));
+    BigInteger u = new BigInteger(Utils.byte2Hex(uBytes, FxAccountUtils.HASH_LENGTH_HEX), 16);
+
+    // S = (B - k*g^x)^(a  u*x) % N
+    // k = H(pad(N) | pad(g))
+    int byteLength = (N.bitLength() + 7) / 8;
+    byte[] kBytes = Utils.sha256(Utils.concatAll(
+        Utils.hex2Byte(N.toString(16), byteLength),
+        Utils.hex2Byte(g.toString(16), byteLength)));
+    BigInteger k = new BigInteger(Utils.byte2Hex(kBytes, FxAccountUtils.HASH_LENGTH_HEX), 16);
+
+    BigInteger base = B.subtract(k.multiply(g.modPow(authState.x, N)).mod(N)).mod(N);
+    BigInteger pow = a.add(u.multiply(authState.x));
+    BigInteger S = base.modPow(pow, N);
+    String srpS = FxAccountUtils.hexModN(S, N);
+
+    byte[] sBytes = Utils.hex2Byte(srpS, modNLengthBytes);
+
+    // M = H(pad(A) | pad(B) | pad(S))
+    authState.Mbytes = Utils.sha256(Utils.concatAll(
+        srpABytes,
+        srpBBytes,
+        sBytes));
+
+    // K = H(pad(S))
+    authState.Kbytes = Utils.sha256(sBytes);
+
+    return authState;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public JSONObject getAuthFinishBody() throws FxAccountClientException {
+    if (internalAuthState == null) {
+      throw new FxAccountClientException("auth must be successfully written before calling getAuthFinishBody.");
+    }
+    JSONObject body = new JSONObject();
+    body.put("srpToken", internalAuthState.srpToken);
+    body.put("A", FxAccountUtils.hexModN(internalAuthState.A, N));
+    body.put("M", Utils.byte2Hex(internalAuthState.Mbytes, FxAccountUtils.HASH_LENGTH_HEX));
+    return body;
+  }
+
+  @Override
+  public byte[] getSharedBytes() throws FxAccountClientException {
+    if (internalAuthState == null) {
+      throw new FxAccountClientException("auth must be successfully finished before calling getSharedBytes.");
+    }
+    return internalAuthState.Kbytes;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/FxAccount10CreateDelegate.java
@@ -0,0 +1,57 @@
+/* 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 java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.background.fxa.FxAccountClient.CreateDelegate;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.SRPConstants;
+
+public class FxAccount10CreateDelegate implements CreateDelegate {
+  protected final String     email;
+  protected final String     mainSalt;
+  protected final String     srpSalt;
+  protected final BigInteger v;
+
+  public FxAccount10CreateDelegate(String email, byte[] stretchedPWBytes, String mainSalt, String srpSalt) throws NoSuchAlgorithmException, UnsupportedEncodingException {
+    this.email = email;
+    this.mainSalt = mainSalt;
+    this.srpSalt = srpSalt;
+    byte[] srpSaltBytes = Utils.hex2Byte(srpSalt, FxAccountUtils.SALT_LENGTH_BYTES);
+    this.v = FxAccountUtils.srpVerifierLowercaseV(email.getBytes("UTF-8"), stretchedPWBytes, srpSaltBytes, SRPConstants._2048.g, SRPConstants._2048.N);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public JSONObject getCreateBody() throws FxAccountClientException {
+    final JSONObject body = new JSONObject();
+    try {
+      body.put("email", FxAccountUtils.bytes(email));
+    } catch (UnsupportedEncodingException e) {
+      throw new FxAccountClientException(e);
+    }
+
+    final JSONObject stretching = new JSONObject();
+    stretching.put("type", "PBKDF2/scrypt/PBKDF2/v1");
+    stretching.put("PBKDF2_rounds_1", 20000);
+    stretching.put("scrypt_N", 65536);
+    stretching.put("scrypt_r", 8);
+    stretching.put("scrypt_p", 1);
+    stretching.put("PBKDF2_rounds_2", 20000);
+    stretching.put("salt", mainSalt);
+    body.put("passwordStretching", stretching);
+
+    final JSONObject srp = new JSONObject();
+    srp.put("type", "SRP-6a/SHA256/2048/v1");
+    srp.put("verifier", FxAccountUtils.hexModN(v, SRPConstants._2048.N));
+    srp.put("salt", srpSalt);
+    body.put("srp", srp);
+    return body;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/FxAccountClient.java
@@ -0,0 +1,573 @@
+/* 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 java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+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.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 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>
+ * The reference server is developed at
+ * <a href="https://github.com/mozilla/picl-idp">https://github.com/mozilla/picl-idp</a>.
+ * This implementation was developed against
+ * <a href="https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208">https://github.com/mozilla/picl-idp/commit/c7a02a0cbbb43f332058dc060bd84a21e56ec208</a>.
+ * <p>
+ * The delegate structure used is a little different from the rest of the code
+ * base. We add a <code>RequestDelegate</code> layer that processes a typed
+ * value extracted from the body of a successful response.
+ * <p>
+ * Further, we add internal <code>CreateDelegate</code> and
+ * <code>AuthDelegate</code> delegates to make it easier to modify the request
+ * bodies sent to the /create and /auth endpoints.
+ */
+public class FxAccountClient {
+  protected static final String LOG_TAG = FxAccountClient.class.getSimpleName();
+
+  protected static final String VERSION_FRAGMENT = "v1/";
+
+  protected final String serverURI;
+  protected final Executor executor;
+
+  public FxAccountClient(String serverURI, Executor executor) {
+    if (serverURI == null) {
+      throw new IllegalArgumentException("Must provide a server URI.");
+    }
+    if (executor == null) {
+      throw new IllegalArgumentException("Must provide a non-null executor.");
+    }
+    this.serverURI = (serverURI.endsWith("/") ? serverURI : serverURI + "/") + VERSION_FRAGMENT;
+    this.executor = executor;
+  }
+
+  /**
+   * 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 handleSuccess(T result);
+  }
+
+  /**
+   * A <code>CreateDelegate</code> produces the body of a /create request.
+   */
+  public interface CreateDelegate {
+    public JSONObject getCreateBody() throws FxAccountClientException;
+  }
+
+  /**
+   * A <code>AuthDelegate</code> produces the bodies of an /auth/{start,finish}
+   * request pair and exposes state generated by a successful response.
+   */
+  public interface AuthDelegate {
+    public JSONObject getAuthStartBody() throws FxAccountClientException;
+    public void onAuthStartResponse(ExtendedJSONObject body) throws FxAccountClientException;
+    public JSONObject getAuthFinishBody() throws FxAccountClientException;
+
+    public byte[] getSharedBytes() throws FxAccountClientException;
+  }
+
+  /**
+   * Thin container for two access tokens.
+   */
+  public static class TwoTokens {
+    public final byte[] keyFetchToken;
+    public final byte[] sessionToken;
+    public TwoTokens(byte[] keyFetchToken, byte[] sessionToken) {
+      this.keyFetchToken = keyFetchToken;
+      this.sessionToken = sessionToken;
+    }
+  }
+
+  /**
+   * Thin container for two cryptographic keys.
+   */
+  public static class TwoKeys {
+    public final byte[] kA;
+    public final byte[] wrapkB;
+    public TwoKeys(byte[] kA, byte[] wrapkB) {
+      this.kA = kA;
+      this.wrapkB = wrapkB;
+    }
+  }
+
+  protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        delegate.handleError(e);
+      }
+    });
+  }
+
+  /**
+   * Translate resource callbacks into request callbacks invoked on the provided
+   * executor.
+   * <p>
+   * Override <code>handleSuccess</code> to parse the body of the resource
+   * request and call the request callback. <code>handleSuccess</code> is
+   * invoked via the executor, so you don't need to delegate further.
+   */
+  protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
+    protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
+
+    protected final RequestDelegate<T> delegate;
+
+    protected final byte[] tokenId;
+    protected final byte[] reqHMACKey;
+    protected final boolean payload;
+
+    /**
+     * Create a delegate for an un-authenticated resource.
+     */
+    public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate) {
+      this(resource, delegate, null, null, false);
+    }
+
+    /**
+     * Create a delegate for a Hawk-authenticated resource.
+     */
+    public ResourceDelegate(final Resource resource, final RequestDelegate<T> delegate, final byte[] tokenId, final byte[] reqHMACKey, final boolean authenticatePayload) {
+      super(resource);
+      this.delegate = delegate;
+      this.reqHMACKey = reqHMACKey;
+      this.tokenId = tokenId;
+      this.payload = authenticatePayload;
+    }
+
+    @Override
+    public AuthHeaderProvider getAuthHeaderProvider() {
+      if (tokenId != null && reqHMACKey != null) {
+        return new HawkAuthHeaderProvider(Utils.byte2Hex(tokenId), reqHMACKey, payload);
+      }
+      return super.getAuthHeaderProvider();
+    }
+
+    @Override
+    public void handleHttpResponse(HttpResponse response) {
+      final int status = response.getStatusLine().getStatusCode();
+      switch (status) {
+      case 200:
+        invokeHandleSuccess(status, response);
+        return;
+      default:
+        invokeHandleFailure(status, response);
+        return;
+      }
+    }
+
+    protected void invokeHandleFailure(final int status, final HttpResponse response) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          delegate.handleFailure(status, response);
+        }
+      });
+    }
+
+    protected void invokeHandleSuccess(final int status, final HttpResponse response) {
+      executor.execute(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
+            ResourceDelegate.this.handleSuccess(status, response, body);
+          } catch (Exception e) {
+            delegate.handleError(e);
+          }
+        }
+      });
+    }
+
+    @Override
+    public void handleHttpProtocolException(final ClientProtocolException e) {
+      invokeHandleError(delegate, e);
+    }
+
+    @Override
+    public void handleHttpIOException(IOException e) {
+      invokeHandleError(delegate, e);
+    }
+
+    @Override
+    public void handleTransportException(GeneralSecurityException e) {
+      invokeHandleError(delegate, e);
+    }
+  }
+
+  protected <T> void post(BaseResource resource, final JSONObject requestBody, final RequestDelegate<T> delegate) {
+    try {
+      if (requestBody == null) {
+        resource.post((HttpEntity) null);
+      } else {
+        resource.post(requestBody);
+      }
+    } catch (UnsupportedEncodingException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+  }
+
+  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;
+    }
+  }
+
+  protected void createAccount(final CreateDelegate createDelegate, final RequestDelegate<String> delegate) {
+    JSONObject body = null;
+    try {
+      body = createDelegate.getCreateBody();
+    } catch (FxAccountClientException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "account/create"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<String>(resource, delegate) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        String uid = body.getString("uid");
+        if (uid == null) {
+          delegate.handleError(new FxAccountClientException("uid must be a non-null string"));
+          return;
+        }
+        delegate.handleSuccess(uid);
+      }
+    };
+    post(resource, body, delegate);
+  }
+
+  protected void authStart(final AuthDelegate authDelegate, final RequestDelegate<AuthDelegate> delegate) {
+    JSONObject body;
+    try {
+      body = authDelegate.getAuthStartBody();
+    } catch (FxAccountClientException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "auth/start"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<AuthDelegate>(resource, delegate) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          authDelegate.onAuthStartResponse(body);
+          delegate.handleSuccess(authDelegate);
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+    post(resource, body, delegate);
+  }
+
+  protected void authFinish(final AuthDelegate authDelegate, RequestDelegate<byte[]> delegate) {
+    JSONObject body;
+    try {
+      body = authDelegate.getAuthFinishBody();
+    } catch (FxAccountClientException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "auth/finish"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<byte[]>(resource, delegate) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          byte[] authToken = new byte[32];
+          unbundleBody(body, authDelegate.getSharedBytes(), FxAccountUtils.KW("auth/finish"), authToken);
+          delegate.handleSuccess(authToken);
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+    post(resource, body, delegate);
+  }
+
+  public void login(final String email, final byte[] stretchedPWBytes, final RequestDelegate<byte[]> delegate) {
+    login(new FxAccount10AuthDelegate(email, stretchedPWBytes), delegate);
+  }
+
+  protected void login(final AuthDelegate authDelegate, final RequestDelegate<byte[]> delegate) {
+    authStart(authDelegate, new RequestDelegate<AuthDelegate>() {
+      @Override
+      public void handleSuccess(AuthDelegate srpSession) {
+        authFinish(srpSession, delegate);
+      }
+
+      @Override
+      public void handleError(final Exception e) {
+        invokeHandleError(delegate, e);
+        return;
+      }
+
+      @Override
+      public void handleFailure(final int status, final HttpResponse response) {
+        executor.execute(new Runnable() {
+          @Override
+          public void run() {
+            delegate.handleFailure(status, response);
+          }
+        });
+      }
+    });
+  }
+
+  public void sessionCreate(byte[] authToken, final RequestDelegate<TwoTokens> delegate) {
+    final byte[] tokenId = new byte[32];
+    final byte[] reqHMACKey = new byte[32];
+    final byte[] requestKey = new byte[32];
+    try {
+      HKDF.deriveMany(authToken, new byte[0], FxAccountUtils.KW("authToken"), tokenId, reqHMACKey, requestKey);
+    } catch (Exception e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "session/create"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<TwoTokens>(resource, delegate, tokenId, reqHMACKey, false) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          byte[] keyFetchToken = new byte[32];
+          byte[] sessionToken = new byte[32];
+          unbundleBody(body, requestKey, FxAccountUtils.KW("session/create"), keyFetchToken, sessionToken);
+          delegate.handleSuccess(new TwoTokens(keyFetchToken, sessionToken));
+          return;
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+    post(resource, null, delegate);
+  }
+
+  public void sessionDestroy(byte[] sessionToken, final RequestDelegate<Void> delegate) {
+    final byte[] tokenId = new byte[32];
+    final byte[] reqHMACKey = new byte[32];
+    try {
+      HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
+    } catch (Exception e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "session/destroy"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey, false) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        delegate.handleSuccess(null);
+      }
+    };
+    post(resource, null, delegate);
+  }
+
+  /**
+   * Don't call this directly. Use <code>unbundleBody</code> instead.
+   */
+  protected void unbundleBytes(byte[] bundleBytes, byte[] respHMACKey, byte[] respXORKey, byte[]... rest)
+      throws InvalidKeyException, NoSuchAlgorithmException, FxAccountClientException {
+    if (bundleBytes.length < 32) {
+      throw new IllegalArgumentException("input bundle must include HMAC");
+    }
+    int len = respXORKey.length;
+    if (bundleBytes.length != len + 32) {
+      throw new IllegalArgumentException("input bundle and XOR key with HMAC have different lengths");
+    }
+    int left = len;
+    for (byte[] array : rest) {
+      left -= array.length;
+    }
+    if (left != 0) {
+      throw new IllegalArgumentException("XOR key and total output arrays have different lengths");
+    }
+
+    byte[] ciphertext = new byte[len];
+    byte[] HMAC = new byte[32];
+    System.arraycopy(bundleBytes, 0, ciphertext, 0, len);
+    System.arraycopy(bundleBytes, len, HMAC, 0, 32);
+
+    Mac hmacHasher = HKDF.makeHMACHasher(respHMACKey);
+    byte[] computedHMAC = hmacHasher.doFinal(ciphertext);
+    if (!Arrays.equals(computedHMAC, HMAC)) {
+      throw new FxAccountClientException("Bad message HMAC");
+    }
+
+    int offset = 0;
+    for (byte[] array : rest) {
+      for (int i = 0; i < array.length; i++) {
+        array[i] = (byte) (respXORKey[offset + i] ^ ciphertext[offset + i]);
+      }
+      offset += array.length;
+    }
+  }
+
+  protected void unbundleBody(ExtendedJSONObject body, byte[] requestKey, byte[] ctxInfo, byte[]... rest) throws Exception {
+    int length = 0;
+    for (byte[] array : rest) {
+      length += array.length;
+    }
+
+    if (body == null) {
+      throw new FxAccountClientException("body must be non-null");
+    }
+    String bundle = body.getString("bundle");
+    if (bundle == null) {
+      throw new FxAccountClientException("bundle must be a non-null string");
+    }
+    byte[] bundleBytes = Utils.hex2Byte(bundle);
+
+    final byte[] respHMACKey = new byte[32];
+    final byte[] respXORKey = new byte[length];
+    HKDF.deriveMany(requestKey, new byte[0], ctxInfo, respHMACKey, respXORKey);
+    unbundleBytes(bundleBytes, respHMACKey, respXORKey, rest);
+  }
+
+  public void keys(byte[] keyFetchToken, final RequestDelegate<TwoKeys> delegate) {
+    final byte[] tokenId = new byte[32];
+    final byte[] reqHMACKey = new byte[32];
+    final byte[] requestKey = new byte[32];
+    try {
+      HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
+    } catch (Exception e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "account/keys"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey, false) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          byte[] kA = new byte[32];
+          byte[] wrapkB = new byte[32];
+          unbundleBody(body, requestKey, FxAccountUtils.KW("account/keys"), kA, wrapkB);
+          delegate.handleSuccess(new TwoKeys(kA, wrapkB));
+          return;
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+    resource.get();
+  }
+
+  @SuppressWarnings("unchecked")
+  public void sign(final byte[] sessionToken, final ExtendedJSONObject publicKey, long durationInSeconds, final RequestDelegate<String> delegate) {
+    final JSONObject body = new JSONObject();
+    body.put("publicKey", publicKey);
+    body.put("duration", durationInSeconds);
+
+    final byte[] tokenId = new byte[32];
+    final byte[] reqHMACKey = new byte[32];
+    try {
+      HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
+    } catch (Exception e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    BaseResource resource;
+    try {
+      resource = new BaseResource(new URI(serverURI + "certificate/sign"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<String>(resource, delegate, tokenId, reqHMACKey, true) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        String cert = body.getString("cert");
+        if (cert == null) {
+          delegate.handleError(new FxAccountClientException("cert must be a non-null string"));
+          return;
+        }
+        delegate.handleSuccess(cert);
+      }
+    };
+    post(resource, body, delegate);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/FxAccountClientException.java
@@ -0,0 +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/. */
+
+package org.mozilla.gecko.background.fxa;
+
+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
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/background/fxa/FxAccountUtils.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.background.fxa;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.crypto.HKDF;
+import org.mozilla.gecko.sync.crypto.KeyBundle;
+
+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 String KW_VERSION_STRING = "identity.mozilla.com/picl/v1/";
+
+  public static String bytes(String string) throws UnsupportedEncodingException {
+    return Utils.byte2Hex(string.getBytes("UTF-8"));
+  }
+
+  public static byte[] KW(String name) throws UnsupportedEncodingException {
+    return Utils.concatAll(
+        KW_VERSION_STRING.getBytes("UTF-8"),
+        name.getBytes("UTF-8"));
+  }
+
+  public static byte[] KWE(String name, byte[] emailUTF8) throws UnsupportedEncodingException {
+    return Utils.concatAll(
+        KW_VERSION_STRING.getBytes("UTF-8"),
+        name.getBytes("UTF-8"),
+        ":".getBytes("UTF-8"),
+        emailUTF8);
+  }
+
+  /**
+   * Calculate the SRP verifier <tt>x</tt> value.
+   */
+  public static BigInteger srpVerifierLowercaseX(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes)
+      throws NoSuchAlgorithmException, UnsupportedEncodingException {
+    byte[] inner = Utils.sha256(Utils.concatAll(emailUTF8, ":".getBytes("UTF-8"), srpPWBytes));
+    byte[] outer = Utils.sha256(Utils.concatAll(srpSaltBytes, inner));
+    return new BigInteger(1, outer);
+  }
+
+  /**
+   * Calculate the SRP verifier <tt>v</tt> value.
+   */
+  public static BigInteger srpVerifierLowercaseV(byte[] emailUTF8, byte[] srpPWBytes, byte[] srpSaltBytes, BigInteger g, BigInteger N)
+      throws NoSuchAlgorithmException, UnsupportedEncodingException {
+    BigInteger x = srpVerifierLowercaseX(emailUTF8, srpPWBytes, srpSaltBytes);
+    BigInteger v = g.modPow(x, N);
+    return v;
+  }
+
+  /**
+   * Format x modulo N in hexadecimal, using as many characters as N takes (in hexadecimal).
+   * @param x to format.
+   * @param N modulus.
+   * @return x modulo N in hexadecimal.
+   */
+  public static String hexModN(BigInteger x, BigInteger N) {
+    int byteLength = (N.bitLength() + 7) / 8;
+    int hexLength = 2 * byteLength;
+    return Utils.byte2Hex(Utils.hex2Byte((x.mod(N)).toString(16), byteLength), hexLength);
+  }
+
+  public static KeyBundle generateSyncKeyBundle(final byte[] kB) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
+    byte[] encryptionKey = new byte[32];
+    byte[] hmacKey = new byte[32];
+    byte[] derived = HKDF.derive(kB, new byte[0], FxAccountUtils.KW("oldsync"), 2*32);
+    System.arraycopy(derived, 0*32, encryptionKey, 0, 1*32);
+    System.arraycopy(derived, 1*32, hmacKey, 0, 1*32);
+    return new KeyBundle(encryptionKey, hmacKey);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/ASNUtils.java
@@ -0,0 +1,82 @@
+/* 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.browserid;
+
+
+/**
+ * Java produces signature in ASN.1 format. Here's some hard-coded encoding and decoding
+ * code, courtesy of a comment in
+ * <a href="http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array">http://stackoverflow.com/questions/10921733/how-sign-method-of-the-digital-signature-combines-the-r-s-values-in-to-array</a>.
+ */
+public class ASNUtils {
+  /**
+   * Decode two short arrays from ASN.1 bytes.
+   * @param input to extract.
+   * @return length 2 array of byte arrays.
+   */
+  public static byte[][] decodeTwoArraysFromASN1(byte[] input) throws IllegalArgumentException {
+    if (input == null) {
+      throw new IllegalArgumentException("input must not be null");
+    }
+    if (input.length <= 3)
+      throw new IllegalArgumentException("bad length");
+    if (input[0] != 0x30)
+      throw new IllegalArgumentException("bad encoding");
+    if ((input[1] & ((byte) 0x80)) != 0)
+      throw new IllegalArgumentException("bad length encoding");
+    if (input[2] != 0x02)
+      throw new IllegalArgumentException("bad encoding");
+    if ((input[3] & ((byte) 0x80)) != 0)
+      throw new IllegalArgumentException("bad length encoding");
+    byte rLength = input[3];
+    if (input.length <= 5 + rLength)
+      throw new IllegalArgumentException("bad length");
+    if (input[4 + rLength] != 0x02)
+      throw new IllegalArgumentException("bad encoding");
+    if ((input[5 + rLength] & (byte) 0x80) !=0)
+      throw new IllegalArgumentException("bad length encoding");
+    byte sLength = input[5 + rLength];
+    if (input.length != 6 + sLength + rLength)
+      throw new IllegalArgumentException("bad length");
+    byte[] rArr = new byte[rLength];
+    byte[] sArr = new byte[sLength];
+    System.arraycopy(input, 4, rArr, 0, rLength);
+    System.arraycopy(input, 6 + rLength, sArr, 0, sLength);
+    return new byte[][] { rArr, sArr };
+  }
+
+  /**
+   * Encode two short arrays into ASN.1 bytes.
+   * @param first array to encode.
+   * @param second array to encode.
+   * @return array.
+   */
+  public static byte[] encodeTwoArraysToASN1(byte[] first, byte[] second) throws IllegalArgumentException {
+    if (first == null) {
+      throw new IllegalArgumentException("first must not be null");
+    }
+    if (second == null) {
+      throw new IllegalArgumentException("second must not be null");
+    }
+    byte[] output = new byte[6 + first.length + second.length];
+    output[0] = 0x30;
+    if (4 + first.length + second.length > 255)
+      throw new IllegalArgumentException("bad length");
+    output[1] = (byte) (4 + first.length + second.length);
+    if ((output[1] & ((byte) 0x80)) != 0)
+      throw new IllegalArgumentException("bad length encoding");
+    output[2] = 0x02;
+    output[3] = (byte) first.length;
+    if ((output[3] & ((byte) 0x80)) != 0)
+      throw new IllegalArgumentException("bad length encoding");
+    System.arraycopy(first, 0, output, 4, first.length);
+    output[4 + first.length] = 0x02;
+    output[5 + first.length] = (byte) second.length;
+    if ((output[5 + first.length] & ((byte) 0x80)) != 0)
+      throw new IllegalArgumentException("bad length encoding");
+    System.arraycopy(second, 0, output, 6 + first.length, second.length);
+    return output;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/BrowserIDKeyPair.java
@@ -0,0 +1,23 @@
+/* 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.browserid;
+
+public class BrowserIDKeyPair {
+  protected final SigningPrivateKey privateKey;
+  protected final VerifyingPublicKey publicKey;
+
+  public BrowserIDKeyPair(SigningPrivateKey privateKey, VerifyingPublicKey publicKey) {
+    this.privateKey = privateKey;
+    this.publicKey = publicKey;
+  }
+
+  public SigningPrivateKey getPrivate() {
+    return this.privateKey;
+  }
+
+  public VerifyingPublicKey getPublic() {
+    return this.publicKey;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/DSACryptoImplementation.java
@@ -0,0 +1,171 @@
+/* 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.browserid;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.interfaces.DSAParams;
+import java.security.interfaces.DSAPrivateKey;
+import java.security.interfaces.DSAPublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+
+public class DSACryptoImplementation {
+  public static final String SIGNATURE_ALGORITHM = "SHA1withDSA";
+  public static final int SIGNATURE_LENGTH_BYTES = 40; // DSA signatures are always 40 bytes long.
+
+  protected static class DSAVerifyingPublicKey implements VerifyingPublicKey {
+    protected final DSAPublicKey publicKey;
+
+    public DSAVerifyingPublicKey(DSAPublicKey publicKey) {
+      this.publicKey = publicKey;
+    }
+
+    @Override
+    public String serialize() {
+      DSAParams params = publicKey.getParams();
+      ExtendedJSONObject o = new ExtendedJSONObject();
+      o.put("algorithm", "DS");
+      o.put("y", publicKey.getY().toString(16));
+      o.put("g", params.getG().toString(16));
+      o.put("p", params.getP().toString(16));
+      o.put("q", params.getQ().toString(16));
+      return o.toJSONString();
+    }
+
+    @Override
+    public boolean verifyMessage(byte[] bytes, byte[] signature)
+        throws GeneralSecurityException {
+      if (bytes == null) {
+        throw new IllegalArgumentException("bytes must not be null");
+      }
+      if (signature == null) {
+        throw new IllegalArgumentException("signature must not be null");
+      }
+      if (signature.length != SIGNATURE_LENGTH_BYTES) {
+        return false;
+      }
+      byte[] first = new byte[signature.length / 2];
+      byte[] second = new byte[signature.length / 2];
+      System.arraycopy(signature, 0, first, 0, first.length);
+      System.arraycopy(signature, first.length, second, 0, second.length);
+      BigInteger r = new BigInteger(Utils.byte2Hex(first), 16);
+      BigInteger s = new BigInteger(Utils.byte2Hex(second), 16);
+      // This is awful, but encoding an extra 0 byte works better on devices.
+      byte[] encoded = ASNUtils.encodeTwoArraysToASN1(
+          Utils.hex2Byte(r.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2),
+          Utils.hex2Byte(s.toString(16), 1 + SIGNATURE_LENGTH_BYTES / 2));
+
+      final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+      signer.initVerify(publicKey);
+      signer.update(bytes);
+      return signer.verify(encoded);
+    }
+  }
+
+  protected static class DSASigningPrivateKey implements SigningPrivateKey {
+    protected final DSAPrivateKey privateKey;
+
+    public DSASigningPrivateKey(DSAPrivateKey privateKey) {
+      this.privateKey = privateKey;
+    }
+
+    @Override
+    public String getAlgorithm() {
+      return "DS" + (privateKey.getParams().getP().bitLength() + 7)/8;
+    }
+
+    @Override
+    public String serialize() {
+      DSAParams params = privateKey.getParams();
+      ExtendedJSONObject o = new ExtendedJSONObject();
+      o.put("algorithm", "DS");
+      o.put("x", privateKey.getX().toString(16));
+      o.put("g", params.getG().toString(16));
+      o.put("p", params.getP().toString(16));
+      o.put("q", params.getQ().toString(16));
+      return o.toJSONString();
+    }
+
+    @Override
+    public byte[] signMessage(byte[] bytes)
+        throws GeneralSecurityException {
+      if (bytes == null) {
+        throw new IllegalArgumentException("bytes must not be null");
+      }
+      final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+      signer.initSign(privateKey);
+      signer.update(bytes);
+      final byte[] signature = signer.sign();
+
+      final byte[][] arrays = ASNUtils.decodeTwoArraysFromASN1(signature);
+      BigInteger r = new BigInteger(arrays[0]);
+      BigInteger s = new BigInteger(arrays[1]);
+      // This is awful, but signatures are always 40 bytes long.
+      byte[] decoded = Utils.concatAll(
+          Utils.hex2Byte(r.toString(16), SIGNATURE_LENGTH_BYTES / 2),
+          Utils.hex2Byte(s.toString(16), SIGNATURE_LENGTH_BYTES / 2));
+      return decoded;
+    }
+  }
+
+  public static BrowserIDKeyPair generateKeypair(int keysize)
+      throws NoSuchAlgorithmException {
+    final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
+    keyPairGenerator.initialize(keysize);
+    final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+    DSAPrivateKey privateKey = (DSAPrivateKey) keyPair.getPrivate();
+    DSAPublicKey publicKey = (DSAPublicKey) keyPair.getPublic();
+    return new BrowserIDKeyPair(new DSASigningPrivateKey(privateKey), new DSAVerifyingPublicKey(publicKey));
+  }
+
+  public static SigningPrivateKey createPrivateKey(BigInteger x, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
+    if (x == null) {
+      throw new IllegalArgumentException("x must not be null");
+    }
+    if (p == null) {
+      throw new IllegalArgumentException("p must not be null");
+    }
+    if (q == null) {
+      throw new IllegalArgumentException("q must not be null");
+    }
+    if (g == null) {
+      throw new IllegalArgumentException("g must not be null");
+    }
+    KeySpec keySpec = new DSAPrivateKeySpec(x, p, q, g);
+    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+    DSAPrivateKey privateKey = (DSAPrivateKey) keyFactory.generatePrivate(keySpec);
+    return new DSASigningPrivateKey(privateKey);
+  }
+
+  public static VerifyingPublicKey createPublicKey(BigInteger y, BigInteger p, BigInteger q, BigInteger g) throws NoSuchAlgorithmException, InvalidKeySpecException {
+    if (y == null) {
+      throw new IllegalArgumentException("n must not be null");
+    }
+    if (p == null) {
+      throw new IllegalArgumentException("p must not be null");
+    }
+    if (q == null) {
+      throw new IllegalArgumentException("q must not be null");
+    }
+    if (g == null) {
+      throw new IllegalArgumentException("g must not be null");
+    }
+    KeySpec keySpec = new DSAPublicKeySpec(y, p, q, g);
+    KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+    DSAPublicKey publicKey = (DSAPublicKey) keyFactory.generatePublic(keySpec);
+    return new DSAVerifyingPublicKey(publicKey);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/JSONWebTokenUtils.java
@@ -0,0 +1,190 @@
+/* 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.browserid;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Map;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.apache.commons.codec.binary.Base64;
+import org.mozilla.apache.commons.codec.binary.StringUtils;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+import org.mozilla.gecko.sync.Utils;
+
+/**
+ * Encode and decode JSON Web Tokens.
+ * <p>
+ * Reverse-engineered from the Node.js jwcrypto library at
+ * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
+ * and informed by the informal draft standard "JSON Web Token (JWT)" at
+ * <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>.
+ */
+public class JSONWebTokenUtils {
+  public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
+  public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
+  public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
+  public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
+
+  public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException  {
+    return encode(payload, privateKey, null);
+  }
+
+  protected static String encode(String payload, SigningPrivateKey privateKey, Map<String, Object> headerFields) throws UnsupportedEncodingException, GeneralSecurityException  {
+    ExtendedJSONObject header = new ExtendedJSONObject();
+    if (headerFields != null) {
+      header.putAll(headerFields);
+    }
+    header.put("alg", privateKey.getAlgorithm());
+    String encodedHeader  = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8"));
+    String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8"));
+    ArrayList<String> segments = new ArrayList<String>();
+    segments.add(encodedHeader);
+    segments.add(encodedPayload);
+    byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8");
+    byte[] signature = privateKey.signMessage(message);
+    segments.add(Base64.encodeBase64URLSafeString(signature));
+    return Utils.toDelimitedString(".", segments);
+  }
+
+  public static String decode(String token, VerifyingPublicKey publicKey) throws GeneralSecurityException, UnsupportedEncodingException  {
+    if (token == null) {
+      throw new IllegalArgumentException("token must not be null");
+    }
+    String[] segments = token.split("\\.");
+    if (segments == null || segments.length != 3) {
+      throw new GeneralSecurityException("malformed token");
+    }
+    byte[] message = (segments[0] + "." + segments[1]).getBytes("UTF-8");
+    byte[] signature = Base64.decodeBase64(segments[2]);
+    boolean verifies = publicKey.verifyMessage(message, signature);
+    if (!verifies) {
+      throw new GeneralSecurityException("bad signature");
+    }
+    String payload = StringUtils.newStringUtf8(Base64.decodeBase64(segments[1]));
+    return payload;
+  }
+
+  protected static String getPayloadString(String payloadString, String issuer,
+      long issuedAt, String audience, long expiresAt) throws NonObjectJSONException,
+      IOException, ParseException {
+    ExtendedJSONObject payload;
+    if (payloadString != null) {
+      payload = new ExtendedJSONObject(payloadString);
+    } else {
+      payload = new ExtendedJSONObject();
+    }
+    payload.put("iss", issuer);
+    payload.put("iat", issuedAt);
+    if (audience != null) {
+      payload.put("aud", audience);
+    }
+    payload.put("exp", expiresAt);
+    return payload.toJSONString();
+  }
+
+  protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException  {
+    ExtendedJSONObject payload = new ExtendedJSONObject();
+    ExtendedJSONObject principal = new ExtendedJSONObject();
+    principal.put("email", email);
+    payload.put("principal", principal);
+    payload.put("public-key", new ExtendedJSONObject(publicKeyToSign.serialize()));
+    return payload.toJSONString();
+  }
+
+  public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
+      String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
+    String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
+    String payloadString = getPayloadString(certificatePayloadString, issuer, issuedAt, null, expiresAt);
+    return JSONWebTokenUtils.encode(payloadString, privateKey);
+  }
+
+  public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
+    String issuer = DEFAULT_CERTIFICATE_ISSUER;
+    long issuedAt = System.currentTimeMillis();
+    long durationInMilliseconds = DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS;
+    return createCertificate(publicKeyToSign, email, issuer, issuedAt, issuedAt + durationInMilliseconds, privateKey);
+  }
+
+  public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
+      String issuer, long issuedAt, long durationInMilliseconds) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
+    long expiresAt = issuedAt + durationInMilliseconds;
+    String emptyAssertionPayloadString = "{}";
+    String payloadString = getPayloadString(emptyAssertionPayloadString, issuer, issuedAt, audience, expiresAt);
+    String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
+    return certificate + "~" + signature;
+  }
+
+  public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
+    String issuer = DEFAULT_ASSERTION_ISSUER;
+    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 true if the certificate is well-formed.
+   */
+  public static boolean dumpCertificate(String input) {
+    try {
+      String[] parts = input.split("\\.");
+      if (parts.length != 3) {
+        throw new IllegalArgumentException("certificate must have three parts");
+      }
+      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);
+      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) {
+    try {
+      String[] parts = input.split("~");
+      if (parts.length != 2) {
+        throw new IllegalArgumentException("input must have two parts");
+      }
+      String certificate = parts[0];
+      String assertion = parts[1];
+      parts = assertion.split("\\.");
+      if (parts.length != 3) {
+        throw new IllegalArgumentException("assertion must have three parts");
+      }
+      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);
+      return true;
+    } catch (Exception e) {
+      System.out.println("Malformed assertion -- got exception trying to dump contents.");
+      e.printStackTrace();
+      return false;
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/MockMyIDTokenFactory.java
@@ -0,0 +1,125 @@
+/* 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.browserid;
+
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+
+/**
+ * Generate certificates and assertions backed by mockmyid.com's private key.
+ * <p>
+ * These artifacts are for testing only.
+ */
+public class MockMyIDTokenFactory {
+  public static final BigInteger MOCKMYID_x = new BigInteger("385cb3509f086e110c5e24bdd395a84b335a09ae", 16);
+  public static final BigInteger MOCKMYID_y = new BigInteger("738ec929b559b604a232a9b55a5295afc368063bb9c20fac4e53a74970a4db7956d48e4c7ed523405f629b4cc83062f13029c4d615bbacb8b97f5e56f0c7ac9bc1d4e23809889fa061425c984061fca1826040c399715ce7ed385c4dd0d402256912451e03452d3c961614eb458f188e3e8d2782916c43dbe2e571251ce38262", 16);
+  public static final BigInteger MOCKMYID_p = new BigInteger("ff600483db6abfc5b45eab78594b3533d550d9f1bf2a992a7a8daa6dc34f8045ad4e6e0c429d334eeeaaefd7e23d4810be00e4cc1492cba325ba81ff2d5a5b305a8d17eb3bf4a06a349d392e00d329744a5179380344e82a18c47933438f891e22aeef812d69c8f75e326cb70ea000c3f776dfdbd604638c2ef717fc26d02e17", 16);
+  public static final BigInteger MOCKMYID_q = new BigInteger("e21e04f911d1ed7991008ecaab3bf775984309c3", 16);
+  public static final BigInteger MOCKMYID_g = new BigInteger("c52a4a0ff3b7e61fdf1867ce84138369a6154f4afa92966e3c827e25cfa6cf508b90e5de419e1337e07a2e9e2a3cd5dea704d175f8ebf6af397d69e110b96afb17c7a03259329e4829b0d03bbc7896b15b4ade53e130858cc34d96269aa89041f409136c7242a38895c9d5bccad4f389af1d7a4bd1398bd072dffa896233397a", 16);
+
+  // Computed lazily by static <code>getMockMyIDPrivateKey</code>.
+  protected static SigningPrivateKey cachedMockMyIDPrivateKey = null;
+
+  public static SigningPrivateKey getMockMyIDPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
+    if (cachedMockMyIDPrivateKey == null) {
+      cachedMockMyIDPrivateKey = DSACryptoImplementation.createPrivateKey(MOCKMYID_x, MOCKMYID_p, MOCKMYID_q, MOCKMYID_g);
+    }
+    return cachedMockMyIDPrivateKey;
+  }
+
+  /**
+   * Sign a public key asserting ownership of username@mockmyid.com with
+   * mockmyid.com's private key.
+   *
+   * @param publicKeyToSign
+   *          public key to sign.
+   * @param username
+   *          sign username@mockmyid.com
+   * @param issuedAt
+   *          timestamp for certificate, in milliseconds since the epoch.
+   * @param durationInMilliseconds
+   *          lifespan of certificate, in milliseconds.
+   * @return encoded certificate string.
+   * @throws Exception
+   */
+  public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, String username,
+      final long issuedAt, final long durationInMilliseconds)
+          throws Exception {
+    if (!username.endsWith("@mockmyid.com")) {
+      username = username + "@mockmyid.com";
+    }
+    long expiresAt = issuedAt + durationInMilliseconds;
+    SigningPrivateKey mockMyIdPrivateKey = getMockMyIDPrivateKey();
+    return JSONWebTokenUtils.createCertificate(publicKeyToSign, username, "mockmyid.com", issuedAt, expiresAt, mockMyIdPrivateKey);
+  }
+
+  /**
+   * Sign a public key asserting ownership of username@mockmyid.com with
+   * mockmyid.com's private key.
+   *
+   * @param publicKeyToSign
+   *          public key to sign.
+   * @param username
+   *          sign username@mockmyid.com
+   * @return encoded certificate string.
+   * @throws Exception
+   */
+  public String createMockMyIDCertificate(final VerifyingPublicKey publicKeyToSign, final String username)
+      throws Exception {
+    return createMockMyIDCertificate(publicKeyToSign, username,
+        System.currentTimeMillis(), JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS );
+  }
+
+  /**
+   * Generate an assertion asserting ownership of username@mockmyid.com to a
+   * relying party. The underlying certificate is signed by mockymid.com's
+   * private key.
+   *
+   * @param keyPair
+   *          to sign with.
+   * @param username
+   *          sign username@mockmyid.com.
+   * @param certificateIssuedAt
+   *          timestamp for certificate, in milliseconds since the epoch.
+   * @param certificateDurationInMilliseconds
+   *          lifespan of certificate, in milliseconds.
+   * @param assertionIssuedAt
+   *          timestamp for assertion, in milliseconds since the epoch.
+   * @param assertionDurationInMilliseconds
+   *          lifespan of assertion, in milliseconds.
+   * @return encoded assertion string.
+   * @throws Exception
+   */
+  public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience,
+      long certificateIssuedAt, long certificateDurationInMilliseconds,
+      long assertionIssuedAt, long assertionDurationInMilliseconds)
+          throws Exception {
+    String certificate = createMockMyIDCertificate(keyPair.getPublic(), username,
+        certificateIssuedAt, certificateDurationInMilliseconds);
+    return JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience,
+        JSONWebTokenUtils.DEFAULT_ASSERTION_ISSUER, assertionIssuedAt, assertionDurationInMilliseconds);
+  }
+
+  /**
+   * Generate an assertion asserting ownership of username@mockmyid.com to a
+   * relying party. The underlying certificate is signed by mockymid.com's
+   * private key.
+   *
+   * @param keyPair
+   *          to sign with.
+   * @param username
+   *          sign username@mockmyid.com.
+   * @return encoded assertion string.
+   * @throws Exception
+   */
+  public String createMockMyIDAssertion(BrowserIDKeyPair keyPair, String username, String audience)
+      throws Exception {
+    long now = System.currentTimeMillis();
+    return createMockMyIDAssertion(keyPair, username, audience,
+        now, JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS,
+        now + 1, JSONWebTokenUtils.DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/RSACryptoImplementation.java
@@ -0,0 +1,117 @@
+/* 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.browserid;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.RSAPrivateKeySpec;
+import java.security.spec.RSAPublicKeySpec;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class RSACryptoImplementation {
+  public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
+
+  protected static class RSAVerifyingPublicKey implements VerifyingPublicKey {
+    protected final RSAPublicKey publicKey;
+
+    public RSAVerifyingPublicKey(RSAPublicKey publicKey) {
+      this.publicKey = publicKey;
+    }
+
+    @Override
+    public String serialize() {
+      ExtendedJSONObject o = new ExtendedJSONObject();
+      o.put("algorithm", "RS");
+      o.put("n", publicKey.getModulus().toString(10));
+      o.put("e", publicKey.getPublicExponent().toString(10));
+      return o.toJSONString();
+    }
+
+    @Override
+    public boolean verifyMessage(byte[] bytes, byte[] signature)
+        throws GeneralSecurityException {
+      final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+      signer.initVerify(publicKey);
+      signer.update(bytes);
+      return signer.verify(signature);
+    }
+  }
+
+  protected static class RSASigningPrivateKey implements SigningPrivateKey {
+    protected final RSAPrivateKey privateKey;
+
+    public RSASigningPrivateKey(RSAPrivateKey privateKey) {
+      this.privateKey = privateKey;
+    }
+
+    @Override
+    public String getAlgorithm() {
+      return "RS" + (privateKey.getModulus().bitLength() + 7)/8;
+    }
+
+    @Override
+    public String serialize() {
+      ExtendedJSONObject o = new ExtendedJSONObject();
+      o.put("algorithm", "RS");
+      o.put("n", privateKey.getModulus().toString(10));
+      o.put("e", privateKey.getPrivateExponent().toString(10));
+      return o.toJSONString();
+    }
+
+    @Override
+    public byte[] signMessage(byte[] bytes)
+        throws GeneralSecurityException {
+      final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
+      signer.initSign(privateKey);
+      signer.update(bytes);
+      return signer.sign();
+    }
+  }
+
+  public static BrowserIDKeyPair generateKeypair(final int keysize) throws NoSuchAlgorithmException {
+    final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+    keyPairGenerator.initialize(keysize);
+    final KeyPair keyPair = keyPairGenerator.generateKeyPair();
+    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+    return new BrowserIDKeyPair(new RSASigningPrivateKey(privateKey), new RSAVerifyingPublicKey(publicKey));
+  }
+
+  public static SigningPrivateKey createPrivateKey(BigInteger n, BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {
+    if (n == null) {
+      throw new IllegalArgumentException("n must not be null");
+    }
+    if (d == null) {
+      throw new IllegalArgumentException("d must not be null");
+    }
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+    KeySpec keySpec = new RSAPrivateKeySpec(n, d);
+    RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
+    return new RSASigningPrivateKey(privateKey);
+  }
+
+  public static VerifyingPublicKey createPublicKey(BigInteger n, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException {
+    if (n == null) {
+      throw new IllegalArgumentException("n must not be null");
+    }
+    if (e == null) {
+      throw new IllegalArgumentException("e must not be null");
+    }
+    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+    KeySpec keySpec = new RSAPublicKeySpec(n, e);
+    RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
+    return new RSAVerifyingPublicKey(publicKey);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/SigningPrivateKey.java
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid;
+
+import java.security.GeneralSecurityException;
+
+public interface SigningPrivateKey {
+  /**
+   * Return the JSON Web Token "alg" header corresponding to this private key.
+   * <p>
+   * The header is used when formatting web tokens, and generally denotes the
+   * algorithm and an ad-hoc encoding of the key size.
+   *
+   * @return header.
+   */
+  public String getAlgorithm();
+
+  /**
+   * Generate a printable representation of a private key.
+   * <p>
+   * <b>This should only be used for debugging. No private keys should go over
+   * the wire at any time.</b>
+   *
+   * @param privateKey
+   *          to represent.
+   * @return printable representation.
+   */
+  public String serialize();
+
+  /**
+   * Sign a message.
+   * @param message to sign.
+   * @return signature.
+   * @throws GeneralSecurityException
+   */
+  public byte[] signMessage(byte[] message) throws GeneralSecurityException;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/VerifyingPublicKey.java
@@ -0,0 +1,32 @@
+/* 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.browserid;
+
+import java.security.GeneralSecurityException;
+
+
+public interface VerifyingPublicKey {
+  /**
+   * Generate a printable representation of a public key.
+   *
+   * @param publicKey
+   *          to represent.
+   * @return printable representation.
+   */
+  public String serialize();
+
+  /**
+   * Verify a signature.
+   *
+   * @param message
+   *          to verify signature of.
+   * @param signature
+   *          to verify.
+   * @return true if signature is a signature of message produced by the private
+   *         key corresponding to this public key.
+   * @throws GeneralSecurityException
+   */
+  public boolean verifyMessage(byte[] message, byte[] signature) throws GeneralSecurityException;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/BrowserIDRemoteVerifierClient.java
@@ -0,0 +1,131 @@
+/* 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.browserid.verifier;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierErrorResponseException;
+import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierException.BrowserIDVerifierMalformedResponseException;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.entity.UrlEncodedFormEntity;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+
+public class BrowserIDRemoteVerifierClient implements BrowserIDVerifierClient {
+  protected static class RemoteVerifierResourceDelegate extends BaseResourceDelegate {
+    private final BrowserIDVerifierDelegate delegate;
+
+    protected RemoteVerifierResourceDelegate(Resource resource, BrowserIDVerifierDelegate delegate) {
+      super(resource);
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void handleHttpResponse(HttpResponse response) {
+      SyncResponse res = new SyncResponse(response);
+      int statusCode = res.getStatusCode();
+      Logger.debug(LOG_TAG, "Got response with status code " + statusCode + ".");
+
+      if (statusCode != 200) {
+        delegate.handleError(new BrowserIDVerifierErrorResponseException("Expected status code 200."));
+        return;
+      }
+
+      ExtendedJSONObject o = null;
+      try {
+        o = res.jsonObjectBody();
+      } catch (Exception e) {
+        delegate.handleError(new BrowserIDVerifierMalformedResponseException(e));
+        return;
+      }
+
+      String status = o.getString("status");
+      if ("failure".equals(status)) {
+        delegate.handleFailure(o);
+        return;
+      }
+
+      if (!("okay".equals(status))) {
+        delegate.handleError(new BrowserIDVerifierMalformedResponseException("Expected status okay, got '" + status + "'."));
+        return;
+      }
+
+      delegate.handleSuccess(o);
+    }
+
+    @Override
+    public void handleTransportException(GeneralSecurityException e) {
+      Logger.warn(LOG_TAG, "Got transport exception.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleHttpProtocolException(ClientProtocolException e) {
+      Logger.warn(LOG_TAG, "Got protocol exception.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleHttpIOException(IOException e) {
+      Logger.warn(LOG_TAG, "Got IO exception.", e);
+      delegate.handleError(e);
+    }
+  }
+
+  public static final String LOG_TAG = "BrowserIDRemoteVerifierClient";
+
+  public static final String DEFAULT_VERIFIER_URL = "https://verifier.login.persona.org/verify";
+
+  protected final URI verifierUri;
+
+  public BrowserIDRemoteVerifierClient(URI verifierUri) {
+    this.verifierUri = verifierUri;
+  }
+
+  public BrowserIDRemoteVerifierClient() throws URISyntaxException {
+    this.verifierUri = new URI(DEFAULT_VERIFIER_URL);
+  }
+
+  @Override
+  public void verify(String audience, String assertion, final BrowserIDVerifierDelegate delegate) {
+    if (audience == null) {
+      throw new IllegalArgumentException("audience cannot be null.");
+    }
+    if (assertion == null) {
+      throw new IllegalArgumentException("assertion cannot be null.");
+    }
+    if (delegate == null) {
+      throw new IllegalArgumentException("delegate cannot be null.");
+    }
+
+    BaseResource r = new BaseResource(verifierUri);
+
+    r.delegate = new RemoteVerifierResourceDelegate(r, delegate);
+
+    List<NameValuePair> nvps = new ArrayList<NameValuePair>();
+    nvps.add(new BasicNameValuePair("audience", audience));
+    nvps.add(new BasicNameValuePair("assertion", assertion));
+
+    try {
+      r.post(new UrlEncodedFormEntity(nvps, "UTF-8"));
+    } catch (UnsupportedEncodingException e) {
+      delegate.handleError(e);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/BrowserIDVerifierClient.java
@@ -0,0 +1,9 @@
+/* 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.browserid.verifier;
+
+public interface BrowserIDVerifierClient {
+  public abstract void verify(String audience, String assertion, BrowserIDVerifierDelegate delegate);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/BrowserIDVerifierDelegate.java
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.browserid.verifier;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public interface BrowserIDVerifierDelegate {
+  void handleSuccess(ExtendedJSONObject response);
+  void handleFailure(ExtendedJSONObject response);
+  void handleError(Exception e);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/browserid/verifier/BrowserIDVerifierException.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.browserid.verifier;
+
+public class BrowserIDVerifierException extends Exception {
+  private static final long serialVersionUID = 2228946910754889975L;
+
+  public BrowserIDVerifierException(String detailMessage) {
+    super(detailMessage);
+  }
+
+  public BrowserIDVerifierException(Throwable throwable) {
+    super(throwable);
+  }
+
+  public static class BrowserIDVerifierMalformedResponseException extends BrowserIDVerifierException {
+    private static final long serialVersionUID = 115377527009652839L;
+
+    public BrowserIDVerifierMalformedResponseException(String detailMessage) {
+      super(detailMessage);
+    }
+
+    public BrowserIDVerifierMalformedResponseException(Throwable throwable) {
+      super(throwable);
+    }
+  }
+
+  public static class BrowserIDVerifierErrorResponseException extends BrowserIDVerifierException {
+    private static final long serialVersionUID = 115377527009652840L;
+
+    public BrowserIDVerifierErrorResponseException(String detailMessage) {
+      super(detailMessage);
+    }
+
+    public BrowserIDVerifierErrorResponseException(Throwable throwable) {
+      super(throwable);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/FxAccountConstants.java.in
@@ -0,0 +1,16 @@
+#filter substitution
+/* 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;
+
+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.dev.lcip.org";
+  public static final String DEFAULT_AUTH_ENDPOINT = "http://auth.oldsync.dev.lcip.org";
+
+  public static final String PREFS_PATH = "fxa.v1";
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/FxAccountSetupActivity.java
@@ -0,0 +1,48 @@
+/* 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.AppConstants;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * Activity which displays login screen to the user.
+ */
+public class FxAccountSetupActivity extends Activity {
+  protected static final String LOG_TAG = FxAccountSetupActivity.class.getSimpleName();
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void onCreate(Bundle icicle) {
+    Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
+
+    super.onCreate(icicle);
+    setContentView(R.layout.fxaccount_setup);
+  }
+
+  @Override
+  public void onResume() {
+    Logger.debug(LOG_TAG, "onResume()");
+
+    super.onResume();
+
+    // Start Fennec at about:accounts page.
+    Intent intent = new Intent(Intent.ACTION_VIEW);
+    intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
+        AppConstants.ANDROID_PACKAGE_NAME + ".App");
+    intent.setData(Uri.parse("about:accounts"));
+
+    startActivity(intent);
+    finish();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/authenticator/FxAccountAuthenticator.java
@@ -0,0 +1,163 @@
+/* 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 org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.activities.FxAccountSetupActivity;
+import org.mozilla.gecko.fxa.sync.FxAccount;
+import org.mozilla.gecko.sync.Utils;
+
+import android.accounts.AbstractAccountAuthenticator;
+import android.accounts.Account;
+import android.accounts.AccountAuthenticatorResponse;
+import android.accounts.AccountManager;
+import android.accounts.NetworkErrorException;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+public class FxAccountAuthenticator extends AbstractAccountAuthenticator {
+  public static final String LOG_TAG = FxAccountAuthenticator.class.getSimpleName();
+
+  public static final String JSON_KEY_UID = "uid";
+  public static final String JSON_KEY_SESSION_TOKEN = "session_token";
+  public static final String JSON_KEY_KA = "kA";
+  public static final String JSON_KEY_KB = "kB";
+  public static final String JSON_KEY_IDP_ENDPOINT = "idp_endpoint";
+  public static final String JSON_KEY_AUTH_ENDPOINT = "auth_endpoint";
+
+  protected final Context context;
+  protected final AccountManager accountManager;
+
+  public FxAccountAuthenticator(Context context) {
+    super(context);
+    this.context = context;
+    this.accountManager = AccountManager.get(context);
+  }
+
+  protected static void enableSyncing(Context context, Account account) {
+    for (String authority : new String[] {
+        AppConstants.ANDROID_PACKAGE_NAME + ".db.browser",
+        AppConstants.ANDROID_PACKAGE_NAME + ".db.formhistory",
+        AppConstants.ANDROID_PACKAGE_NAME + ".db.tabs",
+        AppConstants.ANDROID_PACKAGE_NAME + ".db.passwords",
+    }) {
+      ContentResolver.setSyncAutomatically(account, authority, true);
+      ContentResolver.setIsSyncable(account, authority, 1);
+    }
+  }
+
+  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);
+    userData.putString(JSON_KEY_IDP_ENDPOINT, FxAccountConstants.DEFAULT_IDP_ENDPOINT);
+    userData.putString(JSON_KEY_AUTH_ENDPOINT, FxAccountConstants.DEFAULT_AUTH_ENDPOINT);
+    if (!accountManager.addAccountExplicitly(account, sessionToken, userData)) {
+      Logger.warn(LOG_TAG, "Error adding account named " + account.name + " of type " + account.type);
+      return null;
+    }
+    // Enable syncing by default.
+    enableSyncing(context, account);
+    return account;
+  }
+
+  @Override
+  public Bundle addAccount(AccountAuthenticatorResponse response,
+      String accountType, String authTokenType, String[] requiredFeatures,
+      Bundle options)
+          throws NetworkErrorException {
+    Logger.debug(LOG_TAG, "addAccount");
+
+    final Bundle res = new Bundle();
+
+    if (!FxAccountConstants.ACCOUNT_TYPE.equals(accountType)) {
+      res.putInt(AccountManager.KEY_ERROR_CODE, -1);
+      res.putString(AccountManager.KEY_ERROR_MESSAGE, "Not adding unknown account type.");
+      return res;
+    }
+
+    Intent intent = new Intent(context, FxAccountSetupActivity.class);
+    res.putParcelable(AccountManager.KEY_INTENT, intent);
+    return res;
+  }
+
+  @Override
+  public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)
+      throws NetworkErrorException {
+    Logger.debug(LOG_TAG, "confirmCredentials");
+
+    return null;
+  }
+
+  @Override
+  public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
+    Logger.debug(LOG_TAG, "editProperties");
+
+    return null;
+  }
+
+  @Override
+  public Bundle getAuthToken(final AccountAuthenticatorResponse response,
+      final Account account, final String authTokenType, final Bundle options)
+          throws NetworkErrorException {
+    Logger.debug(LOG_TAG, "getAuthToken");
+
+    Logger.warn(LOG_TAG, "Returning null bundle for getAuthToken.");
+
+    return null;
+  }
+
+  @Override
+  public String getAuthTokenLabel(String authTokenType) {
+    Logger.debug(LOG_TAG, "getAuthTokenLabel");
+
+    return null;
+  }
+
+  @Override
+  public Bundle hasFeatures(AccountAuthenticatorResponse response,
+      Account account, String[] features) throws NetworkErrorException {
+    Logger.debug(LOG_TAG, "hasFeatures");
+
+    return null;
+  }
+
+  @Override
+  public Bundle updateCredentials(AccountAuthenticatorResponse response,
+      Account account, String authTokenType, Bundle options)
+          throws NetworkErrorException {
+    Logger.debug(LOG_TAG, "updateCredentials");
+
+    return null;
+  }
+
+  /**
+   * Extract an FxAccount from an Android Account object.
+   *
+   * @param context to use for AccountManager.
+   * @param account to extract FxAccount from.
+   * @return FxAccount instance.
+   */
+  public static FxAccount fromAndroidAccount(Context context, Account account) {
+    AccountManager accountManager = AccountManager.get(context);
+
+    final byte[] sessionTokenBytes = Utils.hex2Byte(accountManager.getUserData(account, JSON_KEY_SESSION_TOKEN));
+    final byte[] kA = Utils.hex2Byte(accountManager.getUserData(account, JSON_KEY_KA), 16);
+    final byte[] kB = Utils.hex2Byte(accountManager.getUserData(account, JSON_KEY_KB), 16);
+
+    final String idpEndpoint = accountManager.getUserData(account, JSON_KEY_IDP_ENDPOINT);
+    final String authEndpoint = accountManager.getUserData(account, JSON_KEY_AUTH_ENDPOINT);
+
+    return new FxAccount(account.name, sessionTokenBytes, kA, kB, idpEndpoint, authEndpoint);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/authenticator/FxAccountAuthenticatorService.java
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.authenticator;
+
+import org.mozilla.gecko.background.common.log.Logger;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public class FxAccountAuthenticatorService extends Service {
+  public static final String LOG_TAG = FxAccountAuthenticatorService.class.getSimpleName();
+
+  // Lazily initialized by <code>getAuthenticator</code>.
+  protected FxAccountAuthenticator accountAuthenticator = null;
+
+  protected FxAccountAuthenticator getAuthenticator() {
+    if (accountAuthenticator == null) {
+      accountAuthenticator = new FxAccountAuthenticator(this);
+    }
+
+    return accountAuthenticator;
+  }
+
+  @Override
+  public void onCreate() {
+    Logger.debug(LOG_TAG, "onCreate");
+
+    accountAuthenticator = getAuthenticator();
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    Logger.debug(LOG_TAG, "onBind");
+
+    if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT)) {
+      return getAuthenticator().getIBinder();
+    }
+
+    return null;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccount.java
@@ -0,0 +1,175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.JSONWebTokenUtils;
+import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.HawkAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
+import org.mozilla.gecko.tokenserver.TokenServerClient;
+import org.mozilla.gecko.tokenserver.TokenServerClientDelegate;
+import org.mozilla.gecko.tokenserver.TokenServerException;
+import org.mozilla.gecko.tokenserver.TokenServerToken;
+
+import android.content.Context;
+import ch.boye.httpclientandroidlib.HttpResponse;
+
+/**
+ * Represent a Firefox Account.
+ *
+ * This is the FxAccounts equivalent of {@link SyncAccountParameters}.
+ */
+public class FxAccount {
+  protected static final String LOG_TAG = FxAccount.class.getSimpleName();
+
+  public interface Delegate {
+    public void handleSuccess(String uid, String endpoint, AuthHeaderProvider authHeaderProvider);
+    public void handleError(Exception e);
+  }
+
+  protected final String email;
+  protected final byte[] sessionTokenBytes;
+  protected final byte[] kA;
+  protected final byte[] kB;
+  protected final String idpEndpoint;
+  protected final String authEndpoint;
+  protected final Executor executor;
+
+  public FxAccount(String email, byte[] sessionTokenBytes, byte[] kA, byte[] kB, String idpEndpoint, String authEndpoint) {
+    this.email = email;
+    this.sessionTokenBytes = sessionTokenBytes;
+    this.kA = kA;
+    this.kB = kB;
+    this.idpEndpoint = idpEndpoint;
+    this.authEndpoint = authEndpoint;
+    this.executor = Executors.newSingleThreadExecutor();
+  }
+
+  protected static class InnerFxAccountClientRequestDelegate implements FxAccountClient.RequestDelegate<String> {
+    protected final Executor executor;
+    protected final String audience;
+    protected final String tokenServerEndpoint;
+    protected final BrowserIDKeyPair keyPair;
+    protected final Delegate delegate;
+
+    protected InnerFxAccountClientRequestDelegate(Executor executor, String audience, String tokenServerEndpoint, BrowserIDKeyPair keyPair, Delegate delegate) {
+      this.executor = executor;
+      this.audience = audience;
+      this.tokenServerEndpoint = tokenServerEndpoint;
+      this.keyPair = keyPair;
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void handleError(Exception e) {
+      Logger.error(LOG_TAG, "Failed to sign.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleFailure(int status, HttpResponse response) {
+      HTTPFailureException e = new HTTPFailureException(new SyncStorageResponse(response));
+      Logger.error(LOG_TAG, "Failed to sign.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleSuccess(String certificate) {
+      Logger.pii(LOG_TAG, "Got certificate " + certificate);
+
+      try {
+        String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience);
+        if (Logger.LOG_PERSONAL_INFORMATION) {
+          Logger.pii(LOG_TAG, "Generated assertion " + assertion);
+          JSONWebTokenUtils.dumpAssertion(assertion);
+        }
+
+        TokenServerClient tokenServerclient = new TokenServerClient(new URI(tokenServerEndpoint), executor);
+
+        tokenServerclient.getTokenFromBrowserIDAssertion(assertion, true, new InnerTokenServerClientDelegate(delegate));
+      } catch (Exception e) {
+        Logger.error(LOG_TAG, "Got error doing stuff.", e);
+        delegate.handleError(e);
+      }
+    }
+  }
+
+  protected static class InnerTokenServerClientDelegate implements TokenServerClientDelegate {
+    protected final Delegate delegate;
+
+    public InnerTokenServerClientDelegate(Delegate delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public void handleSuccess(TokenServerToken token) {
+      AuthHeaderProvider authHeaderProvider;
+      try {
+        authHeaderProvider = new HawkAuthHeaderProvider(token.id, token.key.getBytes("UTF-8"), false);
+      } catch (UnsupportedEncodingException e) {
+        Logger.error(LOG_TAG, "Failed to sync.", e);
+        delegate.handleError(e);
+        return;
+      }
+
+      delegate.handleSuccess(token.uid, token.endpoint, authHeaderProvider);
+    }
+
+    @Override
+    public void handleFailure(TokenServerException e) {
+      Logger.error(LOG_TAG, "Failed fetching server token.", e);
+      delegate.handleError(e);
+    }
+
+    @Override
+    public void handleError(Exception e) {
+      Logger.error(LOG_TAG, "Got error fetching token server token.", e);
+      delegate.handleError(e);
+    }
+  }
+
+  /**
+   * Request a signed certificate and exchange it for a token.
+   *
+   * This is temporary code that does not do production-ready caching. This
+   * should be made obsolete by a fully featured {@link FxAccountAuthenticator}.
+   *
+   * @param context to use.
+   * @param tokenServerEndpoint to get token from.
+   * @param keyPair to sign certificate for.
+   * @param delegate to callback to.
+   */
+  public void login(final Context context, final String tokenServerEndpoint,
+      final BrowserIDKeyPair keyPair, final Delegate delegate) {
+    ExtendedJSONObject keyPairObject;
+    try {
+      keyPairObject = new ExtendedJSONObject(keyPair.getPublic().serialize());
+    } catch (Exception e) {
+      delegate.handleError(e);
+      return;
+    }
+
+    // We have nested executors in play here. Since we control the executor and
+    // the delegates, we can guarantee that there is no dead-lock between the
+    // inner FxAccountClient delegate, the outer TokenServerClient delegate, and
+    // the user supplied delegate.
+    FxAccountClient fxAccountClient = new FxAccountClient(idpEndpoint, executor);
+    fxAccountClient.sign(sessionTokenBytes, keyPairObject,
+        JSONWebTokenUtils.DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS,
+        new InnerFxAccountClientRequestDelegate(executor, authEndpoint, tokenServerEndpoint, keyPair, delegate));
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountBookmarksSyncService.java
@@ -0,0 +1,9 @@
+/* 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;
+
+public class FxAccountBookmarksSyncService extends FxAccountSyncService {
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountGlobalSession.java
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.sync;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.HashMap;
+
+import org.json.simple.parser.ParseException;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+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,
+      Context context, Bundle extras, ClientsDataDelegate clientsDelegate)
+      throws SyncConfigurationException, IllegalArgumentException, IOException,
+      ParseException, NonObjectJSONException, URISyntaxException {
+    super(username, authHeaderProvider, prefsPath, syncKeyBundle,
+        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);
+    Logger.warn(LOG_TAG, "storageEndpoint is " + uri + " and clusterURL is " + config.clusterURL);
+  }
+
+  @Override
+  public void prepareStages() {
+    super.prepareStages();
+    HashMap<Stage, GlobalSyncStage> stages = new HashMap<Stage, GlobalSyncStage>();
+    stages.putAll(this.stages);
+    stages.put(Stage.ensureClusterURL, new CheckPreconditionsStage());
+    this.stages = Collections.unmodifiableMap(stages);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountHistorySyncService.java
@@ -0,0 +1,9 @@
+/* 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;
+
+public class FxAccountHistorySyncService extends FxAccountSyncService {
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountPasswordsSyncService.java
@@ -0,0 +1,9 @@
+/* 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;
+
+public class FxAccountPasswordsSyncService extends FxAccountHistorySyncService {
+
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java
@@ -0,0 +1,159 @@
+/* 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.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.browserid.BrowserIDKeyPair;
+import org.mozilla.gecko.browserid.RSACryptoImplementation;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.fxa.authenticator.FxAccountAuthenticator;
+import org.mozilla.gecko.sync.GlobalSession;
+import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
+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.GlobalSyncStage.Stage;
+
+import android.accounts.Account;
+import android.content.AbstractThreadedSyncAdapter;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SyncResult;
+import android.os.Bundle;
+
+public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
+  private static final String LOG_TAG = FxAccountSyncAdapter.class.getSimpleName();
+
+  protected final ExecutorService executor;
+
+  public FxAccountSyncAdapter(Context context, boolean autoInitialize) {
+    super(context, autoInitialize);
+    this.executor = Executors.newSingleThreadExecutor();
+  }
+
+  /**
+   * 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;
+
+    public SessionCallback(CountDownLatch latch) {
+      if (latch == null) {
+        throw new IllegalArgumentException("latch must not be null");
+      }
+      this.latch = latch;
+    }
+
+    @Override
+    public boolean shouldBackOff() {
+      return false;
+    }
+
+    @Override
+    public void requestBackoff(long backoff) {
+    }
+
+    @Override
+    public void informUpgradeRequiredResponse(GlobalSession session) {
+    }
+
+    @Override
+    public void informUnauthorizedResponse(GlobalSession globalSession, URI oldClusterURL) {
+    }
+
+    @Override
+    public void handleStageCompleted(Stage currentState, GlobalSession globalSession) {
+    }
+
+    @Override
+    public void handleSuccess(GlobalSession globalSession) {
+      Logger.info(LOG_TAG, "Successfully synced!");
+      latch.countDown();
+    }
+
+    @Override
+    public void handleError(GlobalSession globalSession, Exception ex) {
+      Logger.warn(LOG_TAG, "Sync failed.", ex);
+      latch.countDown();
+    }
+
+    @Override
+    public void handleAborted(GlobalSession globalSession, String reason) {
+      Logger.warn(LOG_TAG, "Sync aborted: " + reason);
+      latch.countDown();
+    }
+  };
+
+  /**
+   * 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.info(LOG_TAG, "Syncing FxAccount" +
+        " account named " + account.name +
+        " for authority " + authority +
+        " with instance " + this + ".");
+
+    final CountDownLatch latch = new CountDownLatch(1);
+    try {
+      final BrowserIDKeyPair keyPair = RSACryptoImplementation.generateKeypair(1024);
+      Logger.info(LOG_TAG, "Generated keypair. ");
+
+      final FxAccount fxAccount = FxAccountAuthenticator.fromAndroidAccount(getContext(), account);
+      final String tokenServerEndpoint = fxAccount.authEndpoint + (fxAccount.authEndpoint.endsWith("/") ? "" : "/") + "1.0/sync/1.1";
+
+      fxAccount.login(getContext(), tokenServerEndpoint, keyPair, new FxAccount.Delegate() {
+        @Override
+        public void handleSuccess(final String uid, final String endpoint, final AuthHeaderProvider authHeaderProvider) {
+          Logger.pii(LOG_TAG, "Got token! uid is " + uid + " and endpoint is " + endpoint + ".");
+
+          final BaseGlobalSessionCallback callback = new SessionCallback(latch);
+
+          Executors.newSingleThreadExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+              FxAccountGlobalSession globalSession = null;
+              try {
+                SharedPreferences sharedPrefs = getContext().getSharedPreferences(FxAccountConstants.PREFS_PATH, Context.MODE_PRIVATE);
+                ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs);
+                final KeyBundle syncKeyBundle = FxAccountUtils.generateSyncKeyBundle(fxAccount.kB);
+                globalSession = new FxAccountGlobalSession(endpoint, uid, authHeaderProvider, FxAccountConstants.PREFS_PATH, syncKeyBundle, callback, getContext(), extras, clientsDataDelegate);
+                globalSession.start();
+              } catch (Exception e) {
+                callback.handleError(globalSession, e);
+                return;
+              }
+            }
+          });
+        }
+
+        @Override
+        public void handleError(Exception e) {
+          Logger.info(LOG_TAG, "Failed to get token.", e);
+          latch.countDown();
+        }
+      });
+
+      latch.await();
+    } catch (Exception e) {
+      Logger.error(LOG_TAG, "Got error logging in.", e);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountSyncService.java
@@ -0,0 +1,28 @@
+/* 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 android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+public abstract class FxAccountSyncService extends Service {
+  private static final Object syncAdapterLock = new Object();
+  private static FxAccountSyncAdapter syncAdapter = null;
+
+  @Override
+  public void onCreate() {
+    synchronized (syncAdapterLock) {
+      if (syncAdapter == null) {
+        syncAdapter = new FxAccountSyncAdapter(getApplicationContext(), true);
+      }
+    }
+  }
+
+  @Override
+  public IBinder onBind(Intent intent) {
+    return syncAdapter.getSyncAdapterBinder();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/sync/FxAccountTabsSyncService.java
@@ -0,0 +1,9 @@
+/* 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;
+
+public class FxAccountTabsSyncService extends FxAccountSyncService {
+
+}
--- a/mobile/android/base/locales/Makefile.in
+++ b/mobile/android/base/locales/Makefile.in
@@ -55,21 +55,23 @@ strings-xml-preqs =\
   $(BRANDPATH) \
   $(STRINGSPATH) \
   $(SYNCSTRINGSPATH) \
   $(BOOKMARKSPATH) \
   $(if $(IS_LANGUAGE_REPACK),FORCE) \
   $(NULL)
 
 $(if $(MOZ_ANDROID_SHARED_ACCOUNT_TYPE),,$(error Missing MOZ_ANDROID_SHARED_ACCOUNT_TYPE))
+$(if $(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE),,$(error Missing MOZ_ANDROID_SHARED_FXACCOUNT_TYPE))
 
 $(dir-strings-xml)/strings.xml: $(strings-xml-preqs)
 	$(call py_action,preprocessor, \
       $(DEFINES) \
 	  -DANDROID_PACKAGE_NAME=$(ANDROID_PACKAGE_NAME) \
 	  -DBOOKMARKSPATH='$(BOOKMARKSPATH)' \
 	  -DBRANDPATH='$(BRANDPATH)' \
 	  -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE=$(MOZ_ANDROID_SHARED_ACCOUNT_TYPE) \
+	  -DMOZ_ANDROID_SHARED_FXACCOUNT_TYPE=$(MOZ_ANDROID_SHARED_FXACCOUNT_TYPE) \
 	  -DMOZ_APP_DISPLAYNAME='@MOZ_APP_DISPLAYNAME@' \
 	  -DSTRINGSPATH='$(STRINGSPATH)' \
 	  -DSYNCSTRINGSPATH='$(SYNCSTRINGSPATH)' \
       $< \
 	  -o $@)
--- a/mobile/android/base/locales/en-US/sync_strings.dtd
+++ b/mobile/android/base/locales/en-US/sync_strings.dtd
@@ -2,16 +2,18 @@
    - 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/. -->
 
 <!-- Don't localize these. They're here until they have
      a better place to live. -->
 <!ENTITY syncBrand.fullName.label "Firefox Sync">
 <!ENTITY syncBrand.shortName.label "Sync">
 
+<!ENTITY fxaccountBrand.fullName.label "Firefox Account">
+
 <!-- Main titles. -->
 <!ENTITY sync.app.name.label '&syncBrand.fullName.label;'>
 <!ENTITY sync.title.connect.label 'Connect to &syncBrand.shortName.label;'>
 <!ENTITY sync.title.adddevice.label 'Add a &syncBrand.fullName.label; Account'>
 <!ENTITY sync.title.pair.label 'Pair a Device'>
 
 <!-- J-PAKE Key Screen -->
 <!ENTITY sync.subtitle.connect.label 'To activate your new device, select “Set up &syncBrand.shortName.label;” on the device.'>
@@ -99,8 +101,11 @@
 <!-- Send tab to device. -->
 <!ENTITY sync.title.send.tab.label 'Send Tab To Devices'>
 <!ENTITY sync.button.send.label 'Send'>
 <!ENTITY sync.button.set.up.sync.label 'Set up &syncBrand.shortName.label;'>
 <!ENTITY sync.title.redirect.to.set.up.sync.label 'Set up &syncBrand.shortName.label; to send tabs'>
 <!ENTITY sync.text.redirect.to.set.up.sync.label 'Set up &syncBrand.fullName.label; on your device to send tabs to other devices.'>
 <!ENTITY sync.text.tab.sent.label 'Your tab was sent!'>
 <!ENTITY sync.text.tab.not.sent.label 'There was a problem sending your tab.'>
+
+<!-- Firefox Account strings -->
+<!ENTITY fxaccount.label '&fxaccountBrand.fullName.label;'>
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -126,16 +126,17 @@ gbjar.sources += [
     'favicons/cache/FaviconCacheElement.java',
     'favicons/cache/FaviconsForURL.java',
     'favicons/Favicons.java',
     'favicons/LoadFaviconTask.java',
     'favicons/OnFaviconLoadedListener.java',
     'FilePickerResultHandler.java',
     'FilePickerResultHandlerSync.java',
     'FindInPageBar.java',
+    'FirefoxAccountsHelper.java',
     'FormAssistPopup.java',
     'GeckoAccessibility.java',
     'GeckoActivity.java',
     'GeckoActivityStatus.java',
     'GeckoApp.java',
     'GeckoApplication.java',
     'GeckoAppShell.java',
     'GeckoBatteryManager.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/fxaccount_setup.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/fxaccount_authenticator.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:icon="@drawable/icon"
+    android:smallIcon="@drawable/icon"
+    android:label="@string/fxaccount_label"
+/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/fxaccount_bookmarks_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:contentAuthority="@string/content_authority_db_browser"
+    android:isAlwaysSyncable="true"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/fxaccount_history_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:contentAuthority="@string/content_authority_db_formhistory"
+    android:isAlwaysSyncable="true"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/fxaccount_passwords_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:contentAuthority="@string/content_authority_db_passwords"
+    android:isAlwaysSyncable="true"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/xml/fxaccount_tabs_syncadapter.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
+    android:accountType="@string/moz_android_shared_fxaccount_type"
+    android:contentAuthority="@string/content_authority_db_tabs"
+    android:isAlwaysSyncable="true"
+    android:supportsUploading="true"
+    android:userVisible="true"
+/>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -16,17 +16,21 @@
 <!ENTITY formatD "&#037;d">
 ]>
 
 #includesubst @BOOKMARKSPATH@
 <resources>
   <string name="moz_app_displayname">@MOZ_APP_DISPLAYNAME@</string>
   <string name="android_package_name">@ANDROID_PACKAGE_NAME@</string>
   <string name="content_authority_db_browser">@ANDROID_PACKAGE_NAME@.db.browser</string>
+  <string name="content_authority_db_formhistory">@ANDROID_PACKAGE_NAME@.db.formhistory</string>
+  <string name="content_authority_db_passwords">@ANDROID_PACKAGE_NAME@.db.passwords</string>
+  <string name="content_authority_db_tabs">@ANDROID_PACKAGE_NAME@.db.tabs</string>
   <string name="moz_android_shared_account_type">@MOZ_ANDROID_SHARED_ACCOUNT_TYPE@</string>
+  <string name="moz_android_shared_fxaccount_type">@MOZ_ANDROID_SHARED_FXACCOUNT_TYPE@</string>
 #include ../services/strings.xml.in
   <string name="no_space_to_start_error">&no_space_to_start_error;</string>
   <string name="error_loading_file">&error_loading_file;</string>
 
   <string name="bookmarks_title">&bookmarks_title;</string>
   <string name="history_title">&history_title;</string>
   <string name="reading_list_title">&reading_list_title;</string>
 
--- a/mobile/android/base/sync/ExtendedJSONObject.java
+++ b/mobile/android/base/sync/ExtendedJSONObject.java
@@ -253,16 +253,21 @@ public class ExtendedJSONObject {
   }
 
   public void put(String key, Object value) {
     @SuppressWarnings("unchecked")
     Map<Object, Object> map = this.object;
     map.put(key, value);
   }
 
+  @SuppressWarnings({ "unchecked", "rawtypes" })
+  public void putAll(Map map) {
+    this.object.putAll(map);
+  }
+
   /**
    * Remove key-value pair from JSONObject.
    *
    * @param key
    *          to be removed.
    * @return true if key exists and was removed, false otherwise.
    */
   public boolean remove(String key) {
--- a/mobile/android/base/sync/Utils.java
+++ b/mobile/android/base/sync/Utils.java
@@ -78,19 +78,27 @@ public class Utils {
   public static void reseedSharedRandom() {
     sharedSecureRandom.setSeed(sharedSecureRandom.generateSeed(8));
   }
 
   /**
    * Helper to convert a byte array to a hex-encoded string
    */
   public static String byte2Hex(final byte[] b) {
-    final StringBuilder hs = new StringBuilder(b.length * 2);
+    return byte2Hex(b, 2 * b.length);
+  }
+
+  public static String byte2Hex(final byte[] b, int hexLength) {
+    final StringBuilder hs = new StringBuilder(Math.max(2*b.length, hexLength));
     String stmp;
 
+    for (int n = 0; n < hexLength - 2*b.length; n++) {
+      hs.append("0");
+    }
+
     for (int n = 0; n < b.length; n++) {
       stmp = Integer.toHexString(b[n] & 0XFF);
 
       if (stmp.length() == 1) {
         hs.append("0");
       }
       hs.append(stmp);
     }
@@ -132,16 +140,27 @@ public class Utils {
   }
 
   public static byte[] decodeFriendlyBase32(String base32) {
     Base32 converter = new Base32();
     final String translated = base32.replace('8', 'l').replace('9', 'o');
     return converter.decode(translated.toUpperCase());
   }
 
+  public static byte[] hex2Byte(String str, int byteLength) {
+    byte[] second = hex2Byte(str);
+    if (second.length >= byteLength) {
+      return second;
+    }
+    // New Java arrays are zeroed:
+    // http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.12.5
+    byte[] first = new byte[byteLength - second.length];
+    return Utils.concatAll(first, second);
+  }
+
   public static byte[] hex2Byte(String str) {
     if (str.length() % 2 == 1) {
       str = "0" + str;
     }
 
     byte[] bytes = new byte[str.length() / 2];
     for (int i = 0; i < bytes.length; i++) {
       bytes[i] = (byte) Integer.parseInt(str.substring(2 * i, 2 * i + 2), 16);
--- a/mobile/android/base/sync/crypto/HKDF.java
+++ b/mobile/android/base/sync/crypto/HKDF.java
@@ -103,9 +103,26 @@ public class HKDF {
    * Output: hashed byte[].
    */
   public static byte[] digestBytes(byte[] message, Mac hasher) {
     hasher.update(message);
     byte[] ret = hasher.doFinal();
     hasher.reset();
     return ret;
   }
+
+  public static byte[] derive(byte[] skm, byte[] xts, byte[] ctxInfo, int dkLen) throws InvalidKeyException, NoSuchAlgorithmException {
+    return hkdfExpand(hkdfExtract(xts, skm), ctxInfo, dkLen);
+  }
+
+  public static void deriveMany(byte[] skm, byte[] xts, byte[] ctxInfo, byte[]... keys) throws InvalidKeyException, NoSuchAlgorithmException {
+    int length = 0;
+    for (byte[] key : keys) {
+      length += key.length;
+    }
+    byte[] derived = hkdfExpand(hkdfExtract(xts, skm), ctxInfo, length);
+    int offset = 0;
+    for (byte[] key : keys) {
+      System.arraycopy(derived, offset, key, 0, key.length);
+      offset += key.length;
+    }
+  }
 }
--- a/mobile/android/base/sync/net/BaseResource.java
+++ b/mobile/android/base/sync/net/BaseResource.java
@@ -442,9 +442,13 @@ public class BaseResource implements Res
 
   public void put(JSONObject jsonObject) throws UnsupportedEncodingException {
     put(jsonEntity(jsonObject));
   }
 
   public void post(ExtendedJSONObject o) throws UnsupportedEncodingException {
     post(jsonEntity(o));
   }
+
+  public void post(JSONObject jsonObject) throws UnsupportedEncodingException {
+    post(jsonEntity(jsonObject));
+  }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/net/SRPConstants.java
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.net;
+
+import java.math.BigInteger;
+
+/**
+ * SRP Group Parameters from
+ * <a href="http://tools.ietf.org/html/rfc5054#appendix-A">Appendix A of RFC 5054</a>.
+ *
+ * The 1024-, 1536-, and 2048-bit groups are taken from software
+ * developed by Tom Wu and Eugene Jhong for the Stanford SRP
+ * distribution, and subsequently proven to be prime.  The larger primes
+ * are taken from [MODP], but generators have been calculated that are
+ * primitive roots of N, unlike the generators in [MODP].
+ *
+ * The 1024-bit and 1536-bit groups <b>MUST</b> be supported.
+ */
+public class SRPConstants {
+  public static class Parameters {
+    public final BigInteger N;
+    public final BigInteger g;
+    public final int bitLength;
+    public final int byteLength;
+    public final int hexLength;
+
+    protected Parameters(String N, long g) {
+      if (N == null) {
+        throw new IllegalArgumentException("N must not be null");
+      }
+      this.N = new BigInteger(N.replaceAll(" ", ""), 16); // Hex.
+      this.g = BigInteger.valueOf(g);
+      this.hexLength = this.N.toString(16).length();
+      this.byteLength = hexLength / 2;
+      this.bitLength = this.byteLength * 8;
+    }
+  }
+
+  public static final Parameters _1024 = new Parameters("" +
+      "EEAF0AB9 ADB38DD6 9C33F80A FA8FC5E8 60726187 75FF3C0B 9EA2314C" +
+      "9C256576 D674DF74 96EA81D3 383B4813 D692C6E0 E0D5D8E2 50B98BE4" +
+      "8E495C1D 6089DAD1 5DC7D7B4 6154D6B6 CE8EF4AD 69B15D49 82559B29" +
+      "7BCF1885 C529F566 660E57EC 68EDBC3C 05726CC0 2FD4CBF4 976EAA9A" +
+      "FD5138FE 8376435B 9FC61D2F C0EB06E3", 2L);
+
+  public static final Parameters _1536 = new Parameters("" +
+      "9DEF3CAF B939277A B1F12A86 17A47BBB DBA51DF4 99AC4C80 BEEEA961" +
+      "4B19CC4D 5F4F5F55 6E27CBDE 51C6A94B E4607A29 1558903B A0D0F843" +
+      "80B655BB 9A22E8DC DF028A7C EC67F0D0 8134B1C8 B9798914 9B609E0B" +
+      "E3BAB63D 47548381 DBC5B1FC 764E3F4B 53DD9DA1 158BFD3E 2B9C8CF5" +
+      "6EDF0195 39349627 DB2FD53D 24B7C486 65772E43 7D6C7F8C E442734A" +
+      "F7CCB7AE 837C264A E3A9BEB8 7F8A2FE9 B8B5292E 5A021FFF 5E91479E" +
+      "8CE7A28C 2442C6F3 15180F93 499A234D CF76E3FE D135F9BB", 2L);
+
+  public static final Parameters _2048 = new Parameters("" +
+      "AC6BDB41 324A9A9B F166DE5E 1389582F AF72B665 1987EE07 FC319294" +
+      "3DB56050 A37329CB B4A099ED 8193E075 7767A13D D52312AB 4B03310D" +
+      "CD7F48A9 DA04FD50 E8083969 EDB767B0 CF609517 9A163AB3 661A05FB" +
+      "D5FAAAE8 2918A996 2F0B93B8 55F97993 EC975EEA A80D740A DBF4FF74" +
+      "7359D041 D5C33EA7 1D281E44 6B14773B CA97B43A 23FB8016 76BD207A" +
+      "436C6481 F1D2B907 8717461A 5B9D32E6 88F87748 544523B5 24B0D57D" +
+      "5EA77A27 75D2ECFA 032CFBDB F52FB378 61602790 04E57AE6 AF874E73" +
+      "03CE5329 9CCC041C 7BC308D8 2A5698F3 A8D0C382 71AE35F8 E9DBFBB6" +
+      "94B5C803 D89F7AE4 35DE236D 525F5475 9B65E372 FCD68EF2 0FA7111F" +
+      "9E4AFF73", 2L);
+
+  public static final Parameters _3072 = new Parameters("" +
+      "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+      "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+      "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+      "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+      "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+      "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+      "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+      "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+      "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+      "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+      "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+      "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+      "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+      "E0FD108E 4B82D120 A93AD2CA FFFFFFFF FFFFFFFF", 5L);
+
+  public static final Parameters _4096 = new Parameters("" +
+      "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+      "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+      "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+      "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+      "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+      "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+      "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+      "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+      "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+      "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+      "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+      "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+      "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+      "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" +
+      "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" +
+      "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" +
+      "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" +
+      "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34063199" +
+      "FFFFFFFF FFFFFFFF", 5L);
+
+  public static final Parameters _6144 = new Parameters("" +
+      "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+      "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+      "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+      "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+      "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+      "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+      "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+      "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+      "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+      "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+      "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+      "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+      "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+      "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" +
+      "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" +
+      "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" +
+      "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" +
+      "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+      "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" +
+      "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" +
+      "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" +
+      "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" +
+      "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" +
+      "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+      "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" +
+      "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" +
+      "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" +
+      "6DCC4024 FFFFFFFF FFFFFFFF", 5L);
+
+  public static final Parameters _8192 = new Parameters("" +
+      "FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 29024E08" +
+      "8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD EF9519B3 CD3A431B" +
+      "302B0A6D F25F1437 4FE1356D 6D51C245 E485B576 625E7EC6 F44C42E9" +
+      "A637ED6B 0BFF5CB6 F406B7ED EE386BFB 5A899FA5 AE9F2411 7C4B1FE6" +
+      "49286651 ECE45B3D C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8" +
+      "FD24CF5F 83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D" +
+      "670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B E39E772C" +
+      "180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 DE2BCBF6 95581718" +
+      "3995497C EA956AE5 15D22618 98FA0510 15728E5A 8AAAC42D AD33170D" +
+      "04507A33 A85521AB DF1CBA64 ECFB8504 58DBEF0A 8AEA7157 5D060C7D" +
+      "B3970F85 A6E1E4C7 ABF5AE8C DB0933D7 1E8C94E0 4A25619D CEE3D226" +
+      "1AD2EE6B F12FFA06 D98A0864 D8760273 3EC86A64 521F2B18 177B200C" +
+      "BBE11757 7A615D6C 770988C0 BAD946E2 08E24FA0 74E5AB31 43DB5BFC" +
+      "E0FD108E 4B82D120 A9210801 1A723C12 A787E6D7 88719A10 BDBA5B26" +
+      "99C32718 6AF4E23C 1A946834 B6150BDA 2583E9CA 2AD44CE8 DBBBC2DB" +
+      "04DE8EF9 2E8EFC14 1FBECAA6 287C5947 4E6BC05D 99B2964F A090C3A2" +
+      "233BA186 515BE7ED 1F612970 CEE2D7AF B81BDD76 2170481C D0069127" +
+      "D5B05AA9 93B4EA98 8D8FDDC1 86FFB7DC 90A6C08F 4DF435C9 34028492" +
+      "36C3FAB4 D27C7026 C1D4DCB2 602646DE C9751E76 3DBA37BD F8FF9406" +
+      "AD9E530E E5DB382F 413001AE B06A53ED 9027D831 179727B0 865A8918" +
+      "DA3EDBEB CF9B14ED 44CE6CBA CED4BB1B DB7F1447 E6CC254B 33205151" +
+      "2BD7AF42 6FB8F401 378CD2BF 5983CA01 C64B92EC F032EA15 D1721D03" +
+      "F482D7CE 6E74FEF6 D55E702F 46980C82 B5A84031 900B1C9E 59E7C97F" +
+      "BEC7E8F3 23A97A7E 36CC88BE 0F1D45B7 FF585AC5 4BD407B2 2B4154AA" +
+      "CC8F6D7E BF48E1D8 14CC5ED2 0F8037E0 A79715EE F29BE328 06A1D58B" +
+      "B7C5DA76 F550AA3D 8A1FBFF0 EB19CCB1 A313D55C DA56C9EC 2EF29632" +
+      "387FE8D7 6E3C0468 043E8F66 3F4860EE 12BF2D5B 0B7474D6 E694F91E" +
+      "6DBE1159 74A3926F 12FEE5E4 38777CB6 A932DF8C D8BEC4D0 73B931BA" +
+      "3BC832B6 8D9DD300 741FA7BF 8AFC47ED 2576F693 6BA42466 3AAB639C" +
+      "5AE4F568 3423B474 2BF1C978 238F16CB E39D652D E3FDB8BE FC848AD9" +
+      "22222E04 A4037C07 13EB57A8 1A23F0C7 3473FC64 6CEA306B 4BCBC886" +
+      "2F8385DD FA9D4B7F A2C087E8 79683303 ED5BDD3A 062B3CF5 B3A278A6" +
+      "6D2A13F8 3F44F82D DF310EE0 74AB6A36 4597E899 A0255DC1 64F31CC5" +
+      "0846851D F9AB4819 5DED7EA1 B1D510BD 7EE74D73 FAF36BC3 1ECFA268" +
+      "359046F4 EB879F92 4009438B 481C6CD7 889A002E D5EE382B C9190DA6" +
+      "FC026E47 9558E447 5677E9AA 9E3050E2 765694DF C81F56E8 80B96E71" +
+      "60C980DD 98EDD3DF FFFFFFFF FFFFFFFF", 19L);
+}
--- a/mobile/android/base/sync/net/TLSSocketFactory.java
+++ b/mobile/android/base/sync/net/TLSSocketFactory.java
@@ -13,17 +13,19 @@ import javax.net.ssl.SSLSocket;
 import org.mozilla.gecko.background.common.log.Logger;
 
 import ch.boye.httpclientandroidlib.conn.ssl.SSLSocketFactory;
 import ch.boye.httpclientandroidlib.params.HttpParams;
 
 public class TLSSocketFactory extends SSLSocketFactory {
   private static final String LOG_TAG = "TLSSocketFactory";
   private static final String[] DEFAULT_CIPHER_SUITES = new String[] {
-    "SSL_RSA_WITH_RC4_128_SHA",        // "RC4_SHA"
+    "TLS_DHE_RSA_WITH_AES_256_CBC_SHA",
+    "TLS_DHE_RSA_WITH_AES_128_CBC_SHA",
+    "SSL_RSA_WITH_RC4_128_SHA", // "RC4_SHA"
   };
   private static final String[] DEFAULT_PROTOCOLS = new String[] {
     "SSLv3",
     "TLSv1"
   };
 
   // Guarded by `this`.
   private static String[] cipherSuites = DEFAULT_CIPHER_SUITES;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tokenserver/TokenServerClient.java
@@ -0,0 +1,266 @@
+/* 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.tokenserver;
+
+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.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.NonArrayJSONException;
+import org.mozilla.gecko.sync.NonObjectJSONException;
+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;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
+import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import ch.boye.httpclientandroidlib.message.BasicHeader;
+
+/**
+ * HTTP client for interacting with the Mozilla Services Token Server API v1.0,
+ * as documented at
+ * <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>.
+ * <p>
+ * A token server accepts some authorization credential and returns a different
+ * authorization credential. Usually, it used to exchange a public-key
+ * authorization token that is expensive to validate for a symmetric-key
+ * authorization that is cheap to validate. For example, we might exchange a
+ * BrowserID assertion for a HAWK id and key pair.
+ */
+public class TokenServerClient {
+  protected static final String LOG_TAG = "TokenServerClient";
+
+  public static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
+  public static final String JSON_KEY_CONDITION_URLS = "condition_urls";
+  public static final String JSON_KEY_DURATION = "duration";
+  public static final String JSON_KEY_ERRORS = "errors";
+  public static final String JSON_KEY_ID = "id";
+  public static final String JSON_KEY_KEY = "key";
+  public static final String JSON_KEY_UID = "uid";
+
+  protected final Executor executor;
+  protected final URI uri;
+
+  public TokenServerClient(URI uri, Executor executor) {
+    if (uri == null) {
+      throw new IllegalArgumentException("uri must not be null");
+    }
+    if (executor == null) {
+      throw new IllegalArgumentException("executor must not be null");
+    }
+    this.uri = uri;
+    this.executor = executor;
+  }
+
+  protected void invokeHandleSuccess(final TokenServerClientDelegate delegate, final TokenServerToken token) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        delegate.handleSuccess(token);
+      }
+    });
+  }
+
+  protected void invokeHandleFailure(final TokenServerClientDelegate delegate, final TokenServerException e) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        delegate.handleFailure(e);
+      }
+    });
+  }
+
+  protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) {
+    executor.execute(new Runnable() {
+      @Override
+      public void run() {
+        delegate.handleError(e);
+      }
+    });
+  }
+
+  public TokenServerToken processResponse(HttpResponse response)
+      throws TokenServerException {
+    SyncResponse res = new SyncResponse(response);
+    int statusCode = res.getStatusCode();
+
+    Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + ".");
+
+    // Responses should *always* be JSON, even in the case of 4xx and 5xx
+    // errors. If we don't see JSON, the server is likely very unhappy.
+    String contentType = response.getEntity().getContentType().getValue();
+    if (contentType != "application/json" && !contentType.startsWith("application/json;")) {
+      Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " +
+          contentType + ". Misconfigured server?");
+      throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
+    }
+
+    // Responses should *always* be a valid JSON object.
+    ExtendedJSONObject result;
+    try {
+      result = res.jsonObjectBody();
+    } catch (Exception e) {
+      Logger.debug(LOG_TAG, "Malformed token response.", e);
+      throw new TokenServerMalformedResponseException(null, e);
+    }
+
+    // The service shouldn't have any 3xx, so we don't need to handle those.
+    if (res.getStatusCode() != 200) {
+      // We should have a (Cornice) error report in the JSON. We log that to
+      // help with debugging.
+      List<ExtendedJSONObject> errorList = new ArrayList<ExtendedJSONObject>();
+
+      if (result.containsKey(JSON_KEY_ERRORS)) {
+        try {
+          for (Object error : result.getArray(JSON_KEY_ERRORS)) {
+            Logger.warn(LOG_TAG, "" + error);
+
+            if (error instanceof JSONObject) {
+              errorList.add(new ExtendedJSONObject((JSONObject) error));
+            }
+          }
+        } catch (NonArrayJSONException e) {
+          Logger.warn(LOG_TAG, "Got non-JSON array '" + result.getString(JSON_KEY_ERRORS) + "'.", e);
+        }
+      }
+
+      if (statusCode == 400) {
+        throw new TokenServerMalformedRequestException(errorList, result.toJSONString());
+      }
+
+      if (statusCode == 401) {
+        throw new TokenServerInvalidCredentialsException(errorList, result.toJSONString());
+      }
+
+      // 403 should represent a "condition acceptance needed" response.
+      //
+      // The extra validation of "urls" is important. We don't want to signal
+      // conditions required unless we are absolutely sure that is what the
+      // server is asking for.
+      if (statusCode == 403) {
+        // Bug 792674 and Bug 783598: make this testing simpler. For now, we
+        // check that errors is an array, and take any condition_urls from the
+        // first element.
+
+        try {
+          if (errorList == null || errorList.isEmpty()) {
+            throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields.");
+          }
+
+          ExtendedJSONObject error = errorList.get(0);
+
+          ExtendedJSONObject condition_urls = error.getObject(JSON_KEY_CONDITION_URLS);
+          if (condition_urls != null) {
+            throw new TokenServerConditionsRequiredException(condition_urls);
+          }
+        } catch (NonObjectJSONException e) {
+          Logger.warn(LOG_TAG, "Got non-JSON error object.");
+        }
+
+        throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields.");
+      }
+
+      if (statusCode == 404) {
+        throw new TokenServerUnknownServiceException(errorList);
+      }
+
+      // We shouldn't ever get here...
+      throw new TokenServerException(errorList);
+    }
+
+    // Defensive as possible: verify object has expected keys with non-null string values.
+    for (String k : new String[] { JSON_KEY_ID, JSON_KEY_KEY, JSON_KEY_API_ENDPOINT }) {
+      Object value = result.get(k);
+      if (value == null) {
+        throw new TokenServerMalformedResponseException(null, "Expected key not present in result: " + k);
+      }
+      if (!(value instanceof String)) {
+        throw new TokenServerMalformedResponseException(null, "Value for key not a string in result: " + k);
+      }
+    }
+
+    // Defensive as possible: verify object has expected key(s) with non-null value.
+    for (String k : new String[] { JSON_KEY_UID }) {
+      Object value = result.get(k);
+      if (value == null) {
+        throw new TokenServerMalformedResponseException(null, "Expected key not present in result: " + k);
+      }
+      if (!(value instanceof Long)) {
+        throw new TokenServerMalformedResponseException(null, "Value for key not a string in result: " + k);
+      }
+    }
+
+    Logger.debug(LOG_TAG, "Successful token response: " + result.getString(JSON_KEY_ID));
+
+    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);
+
+    r.delegate = new BaseResourceDelegate(r) {
+      @Override
+      public void handleHttpResponse(HttpResponse response) {
+        try {
+          TokenServerToken token = processResponse(response);
+          invokeHandleSuccess(delegate, token);
+        } catch (TokenServerException e) {
+          invokeHandleFailure(delegate, e);
+        }
+      }
+
+      @Override
+      public void handleTransportException(GeneralSecurityException e) {
+        invokeHandleError(delegate, e);
+      }
+
+      @Override
+      public void handleHttpProtocolException(ClientProtocolException e) {
+        invokeHandleError(delegate, e);
+      }
+
+      @Override
+      public void handleHttpIOException(IOException e) {
+        invokeHandleError(delegate, e);
+      }
+
+      @Override
+      public AuthHeaderProvider getAuthHeaderProvider() {
+        return new BrowserIDAuthHeaderProvider(assertion);
+      }
+
+      @Override
+      public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+        String host = request.getURI().getHost();
+        request.setHeader(new BasicHeader("Host", host));
+
+        if (conditionsAccepted) {
+          request.addHeader("X-Conditions-Accepted", "1");
+        }
+      }
+    };
+
+    r.get();
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tokenserver/TokenServerClientDelegate.java
@@ -0,0 +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/. */
+
+package org.mozilla.gecko.tokenserver;
+
+
+public interface TokenServerClientDelegate {
+  void handleSuccess(TokenServerToken token);
+  void handleFailure(TokenServerException e);
+  void handleError(Exception e);
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tokenserver/TokenServerException.java
@@ -0,0 +1,89 @@
+/* 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.tokenserver;
+
+import java.util.List;
+
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+public class TokenServerException extends Exception {
+  private static final long serialVersionUID = 7185692034925819696L;
+
+  public final List<ExtendedJSONObject> errors;
+
+  public TokenServerException(List<ExtendedJSONObject> errors) {
+    super();
+    this.errors = errors;
+  }
+
+  public TokenServerException(List<ExtendedJSONObject> errors, String string) {
+    super(string);
+    this.errors = errors;
+  }
+
+  public TokenServerException(List<ExtendedJSONObject> errors, Throwable e) {
+    super(e);
+    this.errors = errors;
+  }
+
+  public static class TokenServerConditionsRequiredException extends TokenServerException {
+    private static final long serialVersionUID = 7578072663150608399L;
+
+    public final ExtendedJSONObject conditionUrls;
+
+    public TokenServerConditionsRequiredException(ExtendedJSONObject urls) {
+      super(null);
+      this.conditionUrls = urls;
+    }
+  }
+
+  public static class TokenServerInvalidCredentialsException extends TokenServerException {
+    private static final long serialVersionUID = 7578072663150608398L;
+
+    public TokenServerInvalidCredentialsException(List<ExtendedJSONObject> errors) {
+      super(errors);
+    }
+
+    public TokenServerInvalidCredentialsException(List<ExtendedJSONObject> errors, String message) {
+      super(errors, message);
+    }
+  }
+
+  public static class TokenServerUnknownServiceException extends TokenServerException {
+    private static final long serialVersionUID = 7578072663150608397L;
+
+    public TokenServerUnknownServiceException(List<ExtendedJSONObject> errors) {
+      super(errors);
+    }
+
+    public TokenServerUnknownServiceException(List<ExtendedJSONObject> errors, String message) {
+      super(errors, message);
+    }
+  }
+
+  public static class TokenServerMalformedRequestException extends TokenServerException {
+    private static final long serialVersionUID = 7578072663150608396L;
+
+    public TokenServerMalformedRequestException(List<ExtendedJSONObject> errors) {
+      super(errors);
+    }
+
+    public TokenServerMalformedRequestException(List<ExtendedJSONObject> errors, String message) {
+      super(errors, message);
+    }
+  }
+
+  public static class TokenServerMalformedResponseException extends TokenServerException {
+    private static final long serialVersionUID = 7578072663150608395L;
+
+    public TokenServerMalformedResponseException(List<ExtendedJSONObject> errors, String message) {
+      super(errors, message);
+    }
+
+    public TokenServerMalformedResponseException(List<ExtendedJSONObject> errors, Throwable e) {
+      super(errors, e);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tokenserver/TokenServerToken.java
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko