Merge f-t to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 29 Nov 2014 09:02:17 -0800
changeset 243925 1e024ddabbb3f7e4685c82347891923216875480
parent 243917 8bc6a1522933ac2c849b9d975f3b7667278dd9c7 (current diff)
parent 243924 adc66033b9188de0c5e35697332cc048a77a11cf (diff)
child 243931 df3fc7cb7e8087edcd25a3afcd9540dbd8cc76ff
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone37.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 f-t to m-c, a=merge
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -833,20 +833,23 @@ sync_java_files = [
     'browserid/verifier/BrowserIDRemoteVerifierClient.java',
     'browserid/verifier/BrowserIDVerifierClient.java',
     'browserid/verifier/BrowserIDVerifierDelegate.java',
     'browserid/verifier/BrowserIDVerifierException.java',
     'browserid/VerifyingPublicKey.java',
     'fxa/AccountLoader.java',
     'fxa/activities/FxAccountAbstractActivity.java',
     'fxa/activities/FxAccountAbstractSetupActivity.java',
+    'fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java',
     'fxa/activities/FxAccountConfirmAccountActivity.java',
     'fxa/activities/FxAccountCreateAccountActivity.java',
     'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
+    'fxa/activities/FxAccountFinishMigratingActivity.java',
     'fxa/activities/FxAccountGetStartedActivity.java',
+    'fxa/activities/FxAccountMigrationFinishedActivity.java',
     'fxa/activities/FxAccountSignInActivity.java',
     'fxa/activities/FxAccountStatusActivity.java',
     'fxa/activities/FxAccountStatusFragment.java',
     'fxa/activities/FxAccountUpdateCredentialsActivity.java',
     'fxa/activities/FxAccountVerifiedAccountActivity.java',
     'fxa/authenticator/AccountPickler.java',
     'fxa/authenticator/AndroidFxAccount.java',
     'fxa/authenticator/FxAccountAuthenticator.java',
@@ -856,16 +859,17 @@ sync_java_files = [
     'fxa/FirefoxAccounts.java',
     'fxa/login/BaseRequestDelegate.java',
     'fxa/login/Cohabiting.java',
     'fxa/login/Doghouse.java',
     'fxa/login/Engaged.java',
     'fxa/login/FxAccountLoginStateMachine.java',
     'fxa/login/FxAccountLoginTransition.java',
     'fxa/login/Married.java',
+    'fxa/login/MigratedFromSync11.java',
     'fxa/login/Separated.java',
     'fxa/login/State.java',
     'fxa/login/StateFactory.java',
     'fxa/login/TokensAndKeysState.java',
     'fxa/receivers/FxAccountDeletedReceiver.java',
     'fxa/receivers/FxAccountDeletedService.java',
     'fxa/receivers/FxAccountUpgradeReceiver.java',
     'fxa/sync/FxAccountGlobalSession.java',
@@ -874,16 +878,17 @@ sync_java_files = [
     'fxa/sync/FxAccountSyncAdapter.java',
     'fxa/sync/FxAccountSyncService.java',
     'fxa/sync/FxAccountSyncStatusHelper.java',
     'fxa/sync/SchedulePolicy.java',
     'fxa/tasks/FxAccountCodeResender.java',
     'fxa/tasks/FxAccountCreateAccountTask.java',
     'fxa/tasks/FxAccountSetupTask.java',
     'fxa/tasks/FxAccountSignInTask.java',
+    'fxa/tasks/FxAccountUnlockCodeResender.java',
     'sync/AlreadySyncingException.java',
     'sync/BackoffHandler.java',
     'sync/BadRequiredFieldJSONException.java',
     'sync/CollectionKeys.java',
     'sync/CommandProcessor.java',
     'sync/CommandRunner.java',
     'sync/config/AccountPickler.java',
     'sync/config/activities/SelectEnginesActivity.java',
--- a/mobile/android/base/background/fxa/FxAccountClient.java
+++ b/mobile/android/base/background/fxa/FxAccountClient.java
@@ -12,9 +12,10 @@ import org.mozilla.gecko.sync.ExtendedJS
 
 public interface FxAccountClient {
   public void createAccountAndGetKeys(final byte[] emailUTF8, final PasswordStretcher passwordStretcher, final RequestDelegate<LoginResponse> delegate);
   public void loginAndGetKeys(final byte[] emailUTF8, final PasswordStretcher passwordStretcher, final RequestDelegate<LoginResponse> requestDelegate);
   public void status(byte[] sessionToken, RequestDelegate<StatusResponse> requestDelegate);
   public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate);
   public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate);
   public void resendCode(byte[] sessionToken, RequestDelegate<Void> delegate);
+  public void resendUnlockCode(byte[] emailUTF8, RequestDelegate<Void> delegate);
 }
--- a/mobile/android/base/background/fxa/FxAccountClient10.java
+++ b/mobile/android/base/background/fxa/FxAccountClient10.java
@@ -765,9 +765,51 @@ public class FxAccountClient10 {
         } catch (Exception e) {
           delegate.handleError(e);
           return;
         }
       }
     };
     post(resource, new JSONObject(), delegate);
   }
+
+  /**
+   * Request a fresh unlock code be sent to the account email.
+   * <p>
+   * Since the account can be locked before the device can connect to it, the
+   * only reasonable identifier is the account email. Since the account is
+   * locked out, this request is un-authenticated.
+   *
+   * @param emailUTF8
+   *          identifying account.
+   * @param delegate
+   *          to invoke callbacks.
+   */
+  @SuppressWarnings("unchecked")
+  public void resendUnlockCode(final byte[] emailUTF8, final RequestDelegate<Void> delegate) {
+    final BaseResource resource;
+    final JSONObject body = new JSONObject();
+    try {
+      resource = new BaseResource(new URI(serverURI + "account/unlock/resend_code"));
+      body.put("email", new String(emailUTF8, "UTF-8"));
+    } catch (URISyntaxException e) {
+      invokeHandleError(delegate, e);
+      return;
+    } catch (UnsupportedEncodingException e) {
+      invokeHandleError(delegate, e);
+      return;
+    }
+
+    resource.delegate = new ResourceDelegate<Void>(resource, delegate) {
+      @Override
+      public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+        try {
+          delegate.handleSuccess(null);
+          return;
+        } catch (Exception e) {
+          delegate.handleError(e);
+          return;
+        }
+      }
+    };
+    post(resource, body, delegate);
+  }
 }
--- a/mobile/android/base/background/fxa/FxAccountClientException.java
+++ b/mobile/android/base/background/fxa/FxAccountClientException.java
@@ -91,31 +91,37 @@ public class FxAccountClientException ex
     public boolean isServerUnavailable() {
       return apiErrorNumber == FxAccountRemoteError.SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD;
     }
 
     public boolean isBadEmailCase() {
       return apiErrorNumber == FxAccountRemoteError.INCORRECT_EMAIL_CASE;
     }
 
+    public boolean isAccountLocked() {
+      return apiErrorNumber == FxAccountRemoteError.ACCOUNT_LOCKED;
+    }
+
     public int getErrorMessageStringResource() {
       if (isUpgradeRequired()) {
         return R.string.fxaccount_remote_error_UPGRADE_REQUIRED;
       } else if (isAccountAlreadyExists()) {
         return R.string.fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS;
       } else if (isAccountDoesNotExist()) {
         return R.string.fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;
       } else if (isBadPassword()) {
         return R.string.fxaccount_remote_error_INCORRECT_PASSWORD;
       } else if (isUnverified()) {
         return R.string.fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;
       } else if (isTooManyRequests()) {
         return R.string.fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;
       } else if (isServerUnavailable()) {
         return R.string.fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;
+      } else if (isAccountLocked()) {
+        return R.string.fxaccount_remote_error_ACCOUNT_LOCKED;
       } else {
         return R.string.fxaccount_remote_error_UNKNOWN_ERROR;
       }
     }
   }
 
   public static class FxAccountClientMalformedResponseException extends FxAccountClientRemoteException {
     private static final long serialVersionUID = 2209313149952001098L;
--- a/mobile/android/base/background/fxa/FxAccountRemoteError.java
+++ b/mobile/android/base/background/fxa/FxAccountRemoteError.java
@@ -11,21 +11,21 @@ public interface FxAccountRemoteError {
   public static final int ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT = 104;
   public static final int INVALID_VERIFICATION_CODE = 105;
   public static final int REQUEST_BODY_WAS_NOT_VALID_JSON = 106;
   public static final int REQUEST_BODY_CONTAINS_INVALID_PARAMETERS = 107;
   public static final int REQUEST_BODY_MISSING_REQUIRED_PARAMETERS = 108;
   public static final int INVALID_REQUEST_SIGNATURE = 109;
   public static final int INVALID_AUTHENTICATION_TOKEN = 110;
   public static final int INVALID_AUTHENTICATION_TIMESTAMP = 111;
-  public static final int INVALID_AUTHENTICATION_NONCE = 115;
   public static final int CONTENT_LENGTH_HEADER_WAS_NOT_PROVIDED = 112;
   public static final int REQUEST_BODY_TOO_LARGE = 113;
   public static final int CLIENT_HAS_SENT_TOO_MANY_REQUESTS = 114;
   public static final int INVALID_NONCE_IN_REQUEST_SIGNATURE = 115;
   public static final int ENDPOINT_IS_NO_LONGER_SUPPORTED = 116;
   public static final int INCORRECT_LOGIN_METHOD_FOR_THIS_ACCOUNT = 117;
   public static final int INCORRECT_KEY_RETRIEVAL_METHOD_FOR_THIS_ACCOUNT = 118;
   public static final int INCORRECT_API_VERSION_FOR_THIS_ACCOUNT = 119;
   public static final int INCORRECT_EMAIL_CASE = 120;
+  public static final int ACCOUNT_LOCKED = 121;
   public static final int SERVICE_TEMPORARILY_UNAVAILABLE_DUE_TO_HIGH_LOAD = 201;
   public static final int UNKNOWN_ERROR = 999;
 }
--- a/mobile/android/base/distribution/Distribution.java
+++ b/mobile/android/base/distribution/Distribution.java
@@ -99,16 +99,18 @@ public class Distribution {
     private static final int CODE_CATEGORY_FETCH_SOCKET_ERROR = 11;
     private static final int CODE_CATEGORY_FETCH_SSL_ERROR = 12;
     private static final int CODE_CATEGORY_FETCH_NON_SUCCESS_RESPONSE = 13;
     private static final int CODE_CATEGORY_FETCH_INVALID_CONTENT_TYPE = 14;
 
     // Corresponds to the high value in Histograms.json.
     private static final long MAX_DOWNLOAD_TIME_MSEC = 40000;    // 40 seconds.
 
+    // Wait just a little while for the system to send a referrer intent after install.
+    private static final long DELAY_WAIT_FOR_REFERRER_MSEC = 400;
 
 
     /**
      * Used as a drop-off point for ReferrerReceiver. Checked when we process
      * first-run distribution.
      *
      * This is `protected` so that test code can clear it between runs.
      */
@@ -380,17 +382,26 @@ public class Distribution {
     /**
      * If applicable, download and select the distribution specified in
      * the referrer intent.
      *
      * @return true if a referrer-supplied distribution was selected.
      */
     private boolean checkIntentDistribution() {
         if (referrer == null) {
-            return false;
+            // Wait a predetermined time and try again.
+            // Just block the thread, because it's the simplest solution.
+            try {
+                Thread.sleep(DELAY_WAIT_FOR_REFERRER_MSEC);
+            } catch (InterruptedException e) {
+                // Good enough.
+            }
+            if (referrer == null) {
+                return false;
+            }
         }
 
         URI uri = getReferredDistribution(referrer);
         if (uri == null) {
             return false;
         }
 
         long start = SystemClock.uptimeMillis();
--- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
@@ -1,15 +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/. */
 
 package org.mozilla.gecko.fxa.activities;
 
 import java.io.IOException;
+import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
@@ -18,31 +19,36 @@ import org.mozilla.gecko.background.fxa.
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
 import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.ProgressDisplay;
+import org.mozilla.gecko.fxa.tasks.FxAccountUnlockCodeResender;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.SyncConfiguration;
+import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.setup.Constants;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.content.Context;
 import android.content.Intent;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.text.Editable;
+import android.text.Spannable;
 import android.text.TextWatcher;
+import android.text.method.LinkMovementMethod;
 import android.text.method.PasswordTransformationMethod;
 import android.text.method.SingleLineTransformationMethod;
+import android.text.style.ClickableSpan;
 import android.util.Patterns;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnFocusChangeListener;
 import android.widget.ArrayAdapter;
 import android.widget.AutoCompleteTextView;
 import android.widget.Button;
@@ -149,17 +155,45 @@ abstract public class FxAccountAbstractS
     } else {
       remoteErrorTextView.setText(defaultResourceId);
     }
     Logger.warn(LOG_TAG, "Got exception; showing error message: " + remoteErrorTextView.getText().toString(), e);
     remoteErrorTextView.setVisibility(View.VISIBLE);
   }
 
   protected void showClientRemoteException(final FxAccountClientRemoteException e) {
-    remoteErrorTextView.setText(e.getErrorMessageStringResource());
+    if (!e.isAccountLocked()) {
+      remoteErrorTextView.setText(e.getErrorMessageStringResource());
+      return;
+    }
+
+    // This horrible bit of special-casing is because we want this error message
+    // to contain a clickable, extra chunk of text, but we don't want to pollute
+    // the exception class with Android specifics.
+    final int messageId = e.getErrorMessageStringResource();
+    final int clickableId = R.string.fxaccount_resend_unlock_code_button_label;
+    final Spannable span = Utils.interpolateClickableSpan(this, messageId, clickableId, new ClickableSpan() {
+      @Override
+      public void onClick(View widget) {
+        // It would be best to capture the email address sent to the server
+        // and use it here, but this will do for now. If the user modifies
+        // the email address entered, the error text is hidden, so sending a
+        // changed email address would be the result of an unusual race.
+        final String email = emailEdit.getText().toString();
+        byte[] emailUTF8 = null;
+        try {
+          emailUTF8 = email.getBytes("UTF-8");
+        } catch (UnsupportedEncodingException e) {
+          // It's okay, we'll fail in the code resender.
+        }
+        FxAccountUnlockCodeResender.resendUnlockCode(FxAccountAbstractSetupActivity.this, getAuthServerEndpoint(), emailUTF8);
+      }
+    });
+    remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
+    remoteErrorTextView.setText(span);
   }
 
   protected void addListeners() {
     TextChangedListener textChangedListener = new TextChangedListener();
     EditorActionListener editorActionListener = new EditorActionListener();
     FocusChangeListener focusChangeListener = new FocusChangeListener();
 
     emailEdit.addTextChangedListener(textChangedListener);
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.fxa.PasswordStretcher;
+import org.mozilla.gecko.fxa.FirefoxAccounts;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.Engaged;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+/**
+ * Abstract activity which displays a screen for updating the local password.
+ */
+public abstract class FxAccountAbstractUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
+  protected static final String LOG_TAG = FxAccountAbstractUpdateCredentialsActivity.class.getSimpleName();
+
+  protected AndroidFxAccount fxAccount;
+
+  protected final int layoutResourceId;
+
+  public FxAccountAbstractUpdateCredentialsActivity(int layoutResourceId) {
+    // We want to share code with the other setup activities, but this activity
+    // doesn't create a new Android Account, it modifies an existing one. If you
+    // manage to get an account, and somehow be locked out too, we'll let you
+    // update it.
+    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
+    this.layoutResourceId = layoutResourceId;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void onCreate(Bundle icicle) {
+    Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
+
+    super.onCreate(icicle);
+    setContentView(layoutResourceId);
+
+    emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
+    passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
+    showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
+    remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
+    button = (Button) ensureFindViewById(null, R.id.button, "update credentials");
+    progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
+
+    minimumPasswordLength = 1; // Minimal restriction on passwords entered to sign in.
+    createButton();
+    addListeners();
+    updateButtonState();
+    createShowPasswordButton();
+
+    emailEdit.setEnabled(false);
+
+    TextView view = (TextView) findViewById(R.id.forgot_password_link);
+    ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password);
+
+    updateFromIntentExtras();
+  }
+
+  protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
+    public final String email;
+    public final String serverURI;
+    public final PasswordStretcher passwordStretcher;
+
+    public UpdateCredentialsDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
+      this.email = email;
+      this.serverURI = serverURI;
+      this.passwordStretcher = passwordStretcher;
+    }
+
+    @Override
+    public void handleError(Exception e) {
+      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
+    }
+
+    @Override
+    public void handleFailure(FxAccountClientRemoteException e) {
+      if (e.isUpgradeRequired()) {
+        Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
+        final State state = fxAccount.getState();
+        fxAccount.setState(state.makeDoghouseState());
+        // The status activity will say that the user needs to upgrade.
+        redirectToActivity(FxAccountStatusActivity.class);
+        return;
+      }
+      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
+    }
+
+    @Override
+    public void handleSuccess(LoginResponse result) {
+      Logger.info(LOG_TAG, "Got success signing in.");
+
+      if (fxAccount == null) {
+        this.handleError(new IllegalStateException("fxAccount must not be null"));
+        return;
+      }
+
+      byte[] unwrapkB;
+      try {
+        // It is crucial that we use the email address provided by the server
+        // (rather than whatever the user entered), because the user's keys are
+        // wrapped and salted with the initial email they provided to
+        // /create/account. Of course, we want to pass through what the user
+        // entered locally as much as possible.
+        byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
+        unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
+      } catch (Exception e) {
+        this.handleError(e);
+        return;
+      }
+      fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
+      fxAccount.requestSync(FirefoxAccounts.FORCE);
+
+      // For great debugging.
+      if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
+        fxAccount.dump();
+      }
+
+      setResult(RESULT_OK);
+
+      // Maybe show success activity.
+      final Intent successIntent = makeSuccessIntent(email, result);
+      if (successIntent != null) {
+        startActivity(successIntent);
+      }
+      finish();
+    }
+  }
+
+  public void updateCredentials(String email, String password) {
+    String serverURI = fxAccount.getAccountServerURI();
+    Executor executor = Executors.newSingleThreadExecutor();
+    FxAccountClient client = new FxAccountClient20(serverURI, executor);
+    PasswordStretcher passwordStretcher = makePasswordStretcher(password);
+    try {
+      hideRemoteError();
+      RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, passwordStretcher, serverURI);
+      new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
+    } catch (Exception e) {
+      Logger.warn(LOG_TAG, "Got exception updating credentials for account.", e);
+      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
+    }
+  }
+
+  protected void createButton() {
+    button.setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        final String email = emailEdit.getText().toString();
+        final String password = passwordEdit.getText().toString();
+        updateCredentials(email, password);
+      }
+    });
+  }
+}
--- a/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountConfirmAccountActivity.java
@@ -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/. */
 
 package org.mozilla.gecko.fxa.activities;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.Action;
 import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
 import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
 import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
@@ -136,19 +135,16 @@ public class FxAccountConfirmAccountActi
 
   protected void refresh() {
     final State state = fxAccount.getState();
     final Action neededAction = state.getNeededAction();
     switch (neededAction) {
     case NeedsVerification:
       // This is what we're here to handle.
       break;
-    case NeedsPassword:
-    case NeedsUpgrade:
-    case None:
     default:
       // We're not in the right place!  Redirect to status.
       Logger.warn(LOG_TAG, "No need to verify Firefox Account that needs action " + neededAction.toString() +
           " (in state " + state.getStateLabel() + ").");
       setResult(RESULT_CANCELED);
       this.redirectToActivity(FxAccountStatusActivity.class);
       return;
     }
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
@@ -17,25 +17,25 @@ import org.mozilla.gecko.background.fxa.
 import org.mozilla.gecko.background.fxa.FxAccountClient;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
 import org.mozilla.gecko.fxa.tasks.FxAccountCreateAccountTask;
+import org.mozilla.gecko.sync.Utils;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.text.Spannable;
-import android.text.Spanned;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ClickableSpan;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.widget.AutoCompleteTextView;
 import android.widget.Button;
 import android.widget.CheckBox;
 import android.widget.EditText;
@@ -119,36 +119,33 @@ public class FxAccountCreateAccountActiv
     if (!e.isAccountAlreadyExists()) {
       super.showClientRemoteException(e);
       return;
     }
 
     // This horrible bit of special-casing is because we want this error message to
     // contain a clickable, extra chunk of text, but we don't want to pollute
     // the exception class with Android specifics.
-    final String clickablePart = getString(R.string.fxaccount_sign_in_button_label);
-    final String message = getString(e.getErrorMessageStringResource(), clickablePart);
-    final int clickableStart = message.lastIndexOf(clickablePart);
-    final int clickableEnd = clickableStart + clickablePart.length();
+    final int messageId = e.getErrorMessageStringResource();
+    final int clickableId = R.string.fxaccount_sign_in_button_label;
 
-    final Spannable span = Spannable.Factory.getInstance().newSpannable(message);
-    span.setSpan(new ClickableSpan() {
+    final Spannable span = Utils.interpolateClickableSpan(this, messageId, clickableId, new ClickableSpan() {
       @Override
       public void onClick(View widget) {
         // Pass through the email address that already existed.
         String email = e.body.getString("email");
         if (email == null) {
             email = emailEdit.getText().toString();
         }
         final String password = passwordEdit.getText().toString();
 
         final Bundle extras = makeExtrasBundle(email, password);
         startActivityInstead(FxAccountSignInActivity.class, CHILD_REQUEST_CODE, extras);
       }
-    }, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+    });
     remoteErrorTextView.setMovementMethod(LinkMovementMethod.getInstance());
     remoteErrorTextView.setText(span);
   }
 
   /**
    * We might have switched to the SignIn activity; if that activity
    * succeeds, feed its result back to the authenticator.
    */
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/FxAccountFinishMigratingActivity.java
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.StateLabel;
+
+import android.content.Intent;
+
+/**
+ * Activity which displays a screen for inputting the password and finishing
+ * migrating to Firefox Accounts / Sync 1.5.
+ */
+public class FxAccountFinishMigratingActivity extends FxAccountAbstractUpdateCredentialsActivity {
+  protected static final String LOG_TAG = FxAccountFinishMigratingActivity.class.getSimpleName();
+
+  public FxAccountFinishMigratingActivity() {
+    super(R.layout.fxaccount_finish_migrating);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    this.fxAccount = getAndroidFxAccount();
+    if (fxAccount == null) {
+      Logger.warn(LOG_TAG, "Could not get Firefox Account.");
+      setResult(RESULT_CANCELED);
+      finish();
+      return;
+    }
+    final State state = fxAccount.getState();
+    if (state.getStateLabel() != StateLabel.MigratedFromSync11) {
+      Logger.warn(LOG_TAG, "Cannot finish migrating from Firefox Account in state: " + state.getStateLabel());
+      setResult(RESULT_CANCELED);
+      finish();
+      return;
+    }
+    emailEdit.setText(fxAccount.getEmail());
+  }
+
+  @Override
+  public Intent makeSuccessIntent(String email, LoginResponse result) {
+    final Intent successIntent = new Intent(this, FxAccountMigrationFinishedActivity.class);
+    // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+    // the soft keyboard not being shown for the started activity. Why, Android, why?
+    successIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+    return successIntent;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/activities/FxAccountMigrationFinishedActivity.java
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.fxa.activities;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
+import org.mozilla.gecko.fxa.login.State;
+import org.mozilla.gecko.fxa.login.State.Action;
+import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
+
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+/**
+ * Activity which displays "Upgrade finished" success screen.
+ */
+public class FxAccountMigrationFinishedActivity extends FxAccountAbstractActivity {
+  private static final String LOG_TAG = FxAccountMigrationFinishedActivity.class.getSimpleName();
+
+  protected AndroidFxAccount fxAccount;
+
+  public FxAccountMigrationFinishedActivity() {
+    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void onCreate(Bundle icicle) {
+    Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
+
+    super.onCreate(icicle);
+    setContentView(R.layout.fxaccount_migration_finished);
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    this.fxAccount = getAndroidFxAccount();
+    if (fxAccount == null) {
+      Logger.warn(LOG_TAG, "Could not get Firefox Account.");
+      setResult(RESULT_CANCELED);
+      finish();
+      return;
+    }
+    final State state = fxAccount.getState();
+    if (state.getNeededAction() == Action.NeedsFinishMigrating) {
+      Logger.warn(LOG_TAG, "Firefox Account needs to finish migrating; not displaying migration finished activity.");
+      setResult(RESULT_CANCELED);
+      finish();
+      return;
+    }
+
+    final View backToBrowsingButton = ensureFindViewById(null, R.id.button, "back to browsing button");
+    backToBrowsingButton.setOnClickListener(new OnClickListener() {
+      @Override
+      public void onClick(View v) {
+        ActivityUtils.openURLInFennec(v.getContext(), null);
+      }
+    });
+  }
+}
--- a/mobile/android/base/fxa/activities/FxAccountStatusActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusActivity.java
@@ -1,22 +1,21 @@
 /* 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.LocaleAware.LocaleAwareFragmentActivity;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.sync.Utils;
-import org.mozilla.gecko.LocaleAware.LocaleAwareActivity;
-import org.mozilla.gecko.LocaleAware.LocaleAwareFragmentActivity;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
 import android.accounts.AccountManagerCallback;
 import android.accounts.AccountManagerFuture;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
 import android.app.ActionBar;
--- a/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
+++ b/mobile/android/base/fxa/activities/FxAccountStatusFragment.java
@@ -72,16 +72,17 @@ public class FxAccountStatusFragment
   protected Preference emailPreference;
   protected Preference authServerPreference;
 
   protected Preference needsPasswordPreference;
   protected Preference needsUpgradePreference;
   protected Preference needsVerificationPreference;
   protected Preference needsMasterSyncAutomaticallyEnabledPreference;
   protected Preference needsAccountEnabledPreference;
+  protected Preference needsFinishMigratingPreference;
 
   protected PreferenceCategory syncCategory;
 
   protected CheckBoxPreference bookmarksPreference;
   protected CheckBoxPreference historyPreference;
   protected CheckBoxPreference tabsPreference;
   protected CheckBoxPreference passwordsPreference;
 
@@ -133,16 +134,17 @@ public class FxAccountStatusFragment
     emailPreference = ensureFindPreference("email");
     authServerPreference = ensureFindPreference("auth_server");
 
     needsPasswordPreference = ensureFindPreference("needs_credentials");
     needsUpgradePreference = ensureFindPreference("needs_upgrade");
     needsVerificationPreference = ensureFindPreference("needs_verification");
     needsMasterSyncAutomaticallyEnabledPreference = ensureFindPreference("needs_master_sync_automatically_enabled");
     needsAccountEnabledPreference = ensureFindPreference("needs_account_enabled");
+    needsFinishMigratingPreference = ensureFindPreference("needs_finish_migrating");
 
     syncCategory = (PreferenceCategory) ensureFindPreference("sync_category");
 
     bookmarksPreference = (CheckBoxPreference) ensureFindPreference("bookmarks");
     historyPreference = (CheckBoxPreference) ensureFindPreference("history");
     tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
     passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
 
@@ -152,16 +154,17 @@ public class FxAccountStatusFragment
       connectDebugButtons();
       ALWAYS_SHOW_AUTH_SERVER = true;
       ALWAYS_SHOW_SYNC_SERVER = true;
     }
 
     needsPasswordPreference.setOnPreferenceClickListener(this);
     needsVerificationPreference.setOnPreferenceClickListener(this);
     needsAccountEnabledPreference.setOnPreferenceClickListener(this);
+    needsFinishMigratingPreference.setOnPreferenceClickListener(this);
 
     bookmarksPreference.setOnPreferenceClickListener(this);
     historyPreference.setOnPreferenceClickListener(this);
     tabsPreference.setOnPreferenceClickListener(this);
     passwordsPreference.setOnPreferenceClickListener(this);
 
     deviceNamePreference = (EditTextPreference) ensureFindPreference("device_name");
     deviceNamePreference.setOnPreferenceChangeListener(this);
@@ -199,16 +202,30 @@ public class FxAccountStatusFragment
       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
       // the soft keyboard not being shown for the started activity. Why, Android, why?
       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
       startActivity(intent);
 
       return true;
     }
 
+    if (preference == needsFinishMigratingPreference) {
+      final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class);
+      final Bundle extras = getExtrasForAccount();
+      if (extras != null) {
+        intent.putExtras(extras);
+      }
+      // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
+      // the soft keyboard not being shown for the started activity. Why, Android, why?
+      intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+      startActivity(intent);
+
+      return true;
+    }
+
     if (preference == needsVerificationPreference) {
       FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
 
       Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
       // Per http://stackoverflow.com/a/8992365, this triggers a known bug with
       // the soft keyboard not being shown for the started activity. Why, Android, why?
       intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
       startActivity(intent);
@@ -275,16 +292,17 @@ public class FxAccountStatusFragment
    */
   protected void showOnlyOneErrorPreference(Preference errorPreferenceToShow) {
     final Preference[] errorPreferences = new Preference[] {
         this.needsPasswordPreference,
         this.needsUpgradePreference,
         this.needsVerificationPreference,
         this.needsMasterSyncAutomaticallyEnabledPreference,
         this.needsAccountEnabledPreference,
+        this.needsFinishMigratingPreference,
     };
     for (Preference errorPreference : errorPreferences) {
       final boolean currentlyShown = null != findPreference(errorPreference.getKey());
       final boolean shouldBeShown = errorPreference == errorPreferenceToShow;
       if (currentlyShown == shouldBeShown) {
         continue;
       }
       if (shouldBeShown) {
@@ -320,16 +338,22 @@ public class FxAccountStatusFragment
   }
 
   protected void showNeedsAccountEnabled() {
     syncCategory.setTitle(R.string.fxaccount_status_sync);
     showOnlyOneErrorPreference(needsAccountEnabledPreference);
     setCheckboxesEnabled(false);
   }
 
+  protected void showNeedsFinishMigrating() {
+    syncCategory.setTitle(R.string.fxaccount_status_sync);
+    showOnlyOneErrorPreference(needsFinishMigratingPreference);
+    setCheckboxesEnabled(false);
+  }
+
   protected void showConnected() {
     syncCategory.setTitle(R.string.fxaccount_status_sync_enabled);
     showOnlyOneErrorPreference(null);
     setCheckboxesEnabled(true);
   }
 
   protected class InnerSyncStatusDelegate implements FirefoxAccounts.SyncStatusListener {
     protected final Runnable refreshRunnable = new Runnable() {
@@ -459,18 +483,22 @@ public class FxAccountStatusFragment
         showNeedsUpgrade();
         break;
       case NeedsPassword:
         showNeedsPassword();
         break;
       case NeedsVerification:
         showNeedsVerification();
         break;
-      default:
+      case NeedsFinishMigrating:
+        showNeedsFinishMigrating();
+        break;
+      case None:
         showConnected();
+        break;
       }
 
       // We check for the master setting last, since it is not strictly
       // necessary for the user to address this error state: it's really a
       // warning state. We surface it for the user's convenience, and to prevent
       // confused folks wondering why Sync is not working at all.
       final boolean masterSyncAutomatically = ContentResolver.getMasterSyncAutomatically();
       if (!masterSyncAutomatically) {
@@ -698,16 +726,21 @@ public class FxAccountStatusFragment
         State state = fxAccount.getState();
         fxAccount.setState(state.makeSeparatedState());
         refresh();
       } else if ("debug_require_upgrade".equals(key)) {
         Logger.info(LOG_TAG, "Moving to Doghouse state: Requiring upgrade.");
         State state = fxAccount.getState();
         fxAccount.setState(state.makeDoghouseState());
         refresh();
+      } else if ("debug_migrated_from_sync11".equals(key)) {
+        Logger.info(LOG_TAG, "Moving to MigratedFromSync11 state: Requiring password.");
+        State state = fxAccount.getState();
+        fxAccount.setState(state.makeMigratedFromSync11State(null));
+        refresh();
       } else {
         return false;
       }
       return true;
     }
   }
 
   /**
@@ -724,17 +757,18 @@ public class FxAccountStatusFragment
     debugCategory.setTitle(debugCategory.getKey());
 
     String[] debugKeys = new String[] {
         "debug_refresh",
         "debug_dump",
         "debug_force_sync",
         "debug_forget_certificate",
         "debug_require_password",
-        "debug_require_upgrade" };
+        "debug_require_upgrade",
+        "debug_migrated_from_sync11" };
     for (String debugKey : debugKeys) {
       final Preference button = ensureFindPreference(debugKey);
       button.setTitle(debugKey); // Not very friendly, but this is for debugging only!
       button.setOnPreferenceClickListener(listener);
     }
   }
 
   @Override
--- a/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountUpdateCredentialsActivity.java
@@ -1,192 +1,52 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
-import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
-import org.mozilla.gecko.background.fxa.FxAccountClient;
-import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
-import org.mozilla.gecko.background.fxa.FxAccountClient20;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
-import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
-import org.mozilla.gecko.background.fxa.FxAccountUtils;
-import org.mozilla.gecko.background.fxa.PasswordStretcher;
-import org.mozilla.gecko.fxa.FirefoxAccounts;
-import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
-import org.mozilla.gecko.fxa.login.Engaged;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.StateLabel;
-import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
-import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
 
-import android.os.Bundle;
-import android.view.View;
-import android.view.View.OnClickListener;
-import android.widget.AutoCompleteTextView;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ProgressBar;
-import android.widget.TextView;
+import android.content.Intent;
 
 /**
  * Activity which displays a screen for updating the local password.
  */
-public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupActivity {
+public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractUpdateCredentialsActivity {
   protected static final String LOG_TAG = FxAccountUpdateCredentialsActivity.class.getSimpleName();
 
-  protected AndroidFxAccount fxAccount;
-
   public FxAccountUpdateCredentialsActivity() {
-    // We want to share code with the other setup activities, but this activity
-    // doesn't create a new Android Account, it modifies an existing one. If you
-    // manage to get an account, and somehow be locked out too, we'll let you
-    // update it.
-    super(CANNOT_RESUME_WHEN_NO_ACCOUNTS_EXIST);
-  }
-
-  /**
-   * {@inheritDoc}
-   */
-  @Override
-  public void onCreate(Bundle icicle) {
-    Logger.debug(LOG_TAG, "onCreate(" + icicle + ")");
-
-    super.onCreate(icicle);
-    setContentView(R.layout.fxaccount_update_credentials);
-
-    emailEdit = (AutoCompleteTextView) ensureFindViewById(null, R.id.email, "email edit");
-    passwordEdit = (EditText) ensureFindViewById(null, R.id.password, "password edit");
-    showPasswordButton = (Button) ensureFindViewById(null, R.id.show_password, "show password button");
-    remoteErrorTextView = (TextView) ensureFindViewById(null, R.id.remote_error, "remote error text view");
-    button = (Button) ensureFindViewById(null, R.id.button, "update credentials");
-    progressBar = (ProgressBar) ensureFindViewById(null, R.id.progress, "progress bar");
-
-    minimumPasswordLength = 1; // Minimal restriction on passwords entered to sign in.
-    createButton();
-    addListeners();
-    updateButtonState();
-    createShowPasswordButton();
-
-    emailEdit.setEnabled(false);
-
-    TextView view = (TextView) findViewById(R.id.forgot_password_link);
-    ActivityUtils.linkTextView(view, R.string.fxaccount_sign_in_forgot_password, R.string.fxaccount_link_forgot_password);
-
-    updateFromIntentExtras();
+    super(R.layout.fxaccount_update_credentials);
   }
 
   @Override
   public void onResume() {
     super.onResume();
     this.fxAccount = getAndroidFxAccount();
     if (fxAccount == null) {
       Logger.warn(LOG_TAG, "Could not get Firefox Account.");
       setResult(RESULT_CANCELED);
       finish();
       return;
     }
-    State state = fxAccount.getState();
+    final State state = fxAccount.getState();
     if (state.getStateLabel() != StateLabel.Separated) {
       Logger.warn(LOG_TAG, "Cannot update credentials from Firefox Account in state: " + state.getStateLabel());
       setResult(RESULT_CANCELED);
       finish();
       return;
     }
     emailEdit.setText(fxAccount.getEmail());
   }
 
-  protected class UpdateCredentialsDelegate implements RequestDelegate<LoginResponse> {
-    public final String email;
-    public final String serverURI;
-    public final PasswordStretcher passwordStretcher;
-
-    public UpdateCredentialsDelegate(String email, PasswordStretcher passwordStretcher, String serverURI) {
-      this.email = email;
-      this.serverURI = serverURI;
-      this.passwordStretcher = passwordStretcher;
-    }
-
-    @Override
-    public void handleError(Exception e) {
-      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
-    }
-
-    @Override
-    public void handleFailure(FxAccountClientRemoteException e) {
-      if (e.isUpgradeRequired()) {
-        Logger.error(LOG_TAG, "Got upgrade required from remote server; transitioning Firefox Account to Doghouse state.");
-        final State state = fxAccount.getState();
-        fxAccount.setState(state.makeDoghouseState());
-        // The status activity will say that the user needs to upgrade.
-        redirectToActivity(FxAccountStatusActivity.class);
-        return;
-      }
-      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
-    }
-
-    @Override
-    public void handleSuccess(LoginResponse result) {
-      Logger.info(LOG_TAG, "Got success signing in.");
-
-      if (fxAccount == null) {
-        this.handleError(new IllegalStateException("fxAccount must not be null"));
-        return;
-      }
-
-      byte[] unwrapkB;
-      try {
-        // It is crucial that we use the email address provided by the server
-        // (rather than whatever the user entered), because the user's keys are
-        // wrapped and salted with the initial email they provided to
-        // /create/account. Of course, we want to pass through what the user
-        // entered locally as much as possible.
-        byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(result.remoteEmail.getBytes("UTF-8"));
-        unwrapkB = FxAccountUtils.generateUnwrapBKey(quickStretchedPW);
-      } catch (Exception e) {
-        this.handleError(e);
-        return;
-      }
-      fxAccount.setState(new Engaged(email, result.uid, result.verified, unwrapkB, result.sessionToken, result.keyFetchToken));
-      fxAccount.requestSync(FirefoxAccounts.FORCE);
-
-      // For great debugging.
-      if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
-        fxAccount.dump();
-      }
-
-      setResult(RESULT_OK);
-      finish();
-    }
-  }
-
-  public void updateCredentials(String email, String password) {
-    String serverURI = fxAccount.getAccountServerURI();
-    Executor executor = Executors.newSingleThreadExecutor();
-    FxAccountClient client = new FxAccountClient20(serverURI, executor);
-    PasswordStretcher passwordStretcher = makePasswordStretcher(password);
-    try {
-      hideRemoteError();
-      RequestDelegate<LoginResponse> delegate = new UpdateCredentialsDelegate(email, passwordStretcher, serverURI);
-      new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
-    } catch (Exception e) {
-      Logger.warn(LOG_TAG, "Got exception updating credentials for account.", e);
-      showRemoteError(e, R.string.fxaccount_update_credentials_unknown_error);
-    }
-  }
-
-  protected void createButton() {
-    button.setOnClickListener(new OnClickListener() {
-      @Override
-      public void onClick(View v) {
-        final String email = emailEdit.getText().toString();
-        final String password = passwordEdit.getText().toString();
-        updateCredentials(email, password);
-      }
-    });
+  @Override
+  public Intent makeSuccessIntent(String email, LoginResponse result) {
+    // We don't show anything after updating credentials. The updating Activity
+    // sets its result to OK and the user is returned to the previous task,
+    // which is often the Status Activity.
+    return null;
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/login/MigratedFromSync11.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.login;
+
+import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
+import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.PasswordRequired;
+
+public class MigratedFromSync11 extends State {
+  public final String password;
+
+  public MigratedFromSync11(String email, String uid, boolean verified, String password) {
+    super(StateLabel.MigratedFromSync11, email, uid, verified);
+    // Null password is allowed.
+    this.password = password;
+  }
+
+  @Override
+  public void execute(final ExecuteDelegate delegate) {
+    delegate.handleTransition(new PasswordRequired(), this);
+  }
+
+  @Override
+  public Action getNeededAction() {
+    return Action.NeedsFinishMigrating;
+  }
+}
--- a/mobile/android/base/fxa/login/State.java
+++ b/mobile/android/base/fxa/login/State.java
@@ -4,30 +4,32 @@
 
 package org.mozilla.gecko.fxa.login;
 
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 
 public abstract class State {
-  public static final long CURRENT_VERSION = 2L;
+  public static final long CURRENT_VERSION = 3L;
 
   public enum StateLabel {
     Engaged,
     Cohabiting,
     Married,
     Separated,
     Doghouse,
+    MigratedFromSync11,
   }
 
   public enum Action {
     NeedsUpgrade,
     NeedsPassword,
     NeedsVerification,
+    NeedsFinishMigrating,
     None,
   }
 
   protected final StateLabel stateLabel;
   public final String email;
   public final String uid;
   public final boolean verified;
 
@@ -55,12 +57,16 @@ public abstract class State {
   public State makeSeparatedState() {
     return new Separated(email, uid, verified);
   }
 
   public State makeDoghouseState() {
     return new Doghouse(email, uid, verified);
   }
 
+  public State makeMigratedFromSync11State(String password) {
+    return new MigratedFromSync11(email, uid, verified, password);
+  }
+
   public abstract void execute(ExecuteDelegate delegate);
 
   public abstract Action getNeededAction();
 }
--- a/mobile/android/base/fxa/login/StateFactory.java
+++ b/mobile/android/base/fxa/login/StateFactory.java
@@ -49,18 +49,21 @@ public class StateFactory {
 
   public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
     Long version = o.getLong("version");
     if (version == null) {
       throw new IllegalStateException("version must not be null");
     }
 
     final int v = version.intValue();
+    if (v == 3) {
+      // The most common case is the most recent version.
+      return fromJSONObjectV3(stateLabel, o);
+    }
     if (v == 2) {
-      // The most common case is the most recent version.
       return fromJSONObjectV2(stateLabel, o);
     }
     if (v == 1) {
       final State state = fromJSONObjectV1(stateLabel, o);
       return migrateV1toV2(stateLabel, state);
     }
     throw new IllegalStateException("version must be in {1, 2}");
   }
@@ -129,16 +132,33 @@ public class StateFactory {
           Utils.hex2Byte(o.getString("kB")),
           keyPairFromJSONObjectV2(o.getObject("keyPair")),
           o.getString("certificate"));
     default:
       return fromJSONObjectV1(stateLabel, o);
     }
   }
 
+  /**
+   * Exactly the same as {@link fromJSONObjectV2}, except that there's a new
+   * MigratedFromSyncV11 state.
+   */
+  protected static State fromJSONObjectV3(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
+    switch (stateLabel) {
+    case MigratedFromSync11:
+      return new MigratedFromSync11(
+          o.getString("email"),
+          o.getString("uid"),
+          o.getBoolean("verified"),
+          o.getString("password"));
+    default:
+      return fromJSONObjectV2(stateLabel, o);
+    }
+  }
+
   protected static void logMigration(State from, State to) {
     if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
       return;
     }
     try {
       FxAccountUtils.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e);
--- a/mobile/android/base/fxa/sync/FxAccountNotificationManager.java
+++ b/mobile/android/base/fxa/sync/FxAccountNotificationManager.java
@@ -3,16 +3,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.sync;
 
 import org.mozilla.gecko.BrowserLocaleManager;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity;
 import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.fxa.login.State.Action;
 
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
@@ -62,22 +63,31 @@ public class FxAccountNotificationManage
       return;
     }
 
     if (!localeUpdated) {
       localeUpdated = true;
       BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(context);
     }
 
-    final String title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
-    final String text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
+    final String title;
+    final String text;
+    final Intent notificationIntent;
+    if (action == Action.NeedsFinishMigrating) {
+      title = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_title);
+      text = context.getResources().getString(R.string.fxaccount_sync_finish_migrating_notification_text, state.email);
+      notificationIntent = new Intent(context, FxAccountFinishMigratingActivity.class);
+    } else {
+      title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
+      text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
+      notificationIntent = new Intent(context, FxAccountStatusActivity.class);
+    }
     Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
     FxAccountUtils.pii(LOG_TAG, "And text: " + text);
 
-    final Intent notificationIntent = new Intent(context, FxAccountStatusActivity.class);
     final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
 
     final Builder builder = new NotificationCompat.Builder(context);
     builder
     .setContentTitle(title)
     .setContentText(text)
     .setSmallIcon(R.drawable.ic_status_logo)
     .setAutoCancel(true)
--- a/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java
+++ b/mobile/android/base/fxa/sync/FxAccountSchedulePolicy.java
@@ -113,16 +113,17 @@ public class FxAccountSchedulePolicy imp
     requestPeriodicSync(interval);
   }
 
   @Override
   public void onHandleFinal(Action needed) {
     switch (needed) {
     case NeedsPassword:
     case NeedsUpgrade:
+    case NeedsFinishMigrating:
       requestPeriodicSync(POLL_INTERVAL_ERROR_STATE_SEC);
       break;
     case NeedsVerification:
       requestPeriodicSync(POLL_INTERVAL_PENDING_VERIFICATION);
       break;
     case None:
       // No action needed: we'll set the periodic sync interval
       // when the sync finishes, via the SessionCallback.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fxa/tasks/FxAccountUnlockCodeResender.java
@@ -0,0 +1,105 @@
+/* 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.tasks;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.background.common.log.Logger;
+import org.mozilla.gecko.background.fxa.FxAccountClient;
+import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
+import org.mozilla.gecko.background.fxa.FxAccountClient20;
+import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.widget.Toast;
+
+/**
+ * A helper class that provides a simple interface for requesting a Firefox
+ * Account unlock account email be (re)-sent.
+ */
+public class FxAccountUnlockCodeResender {
+  private static final String LOG_TAG = FxAccountUnlockCodeResender.class.getSimpleName();
+
+  private static class FxAccountUnlockCodeTask extends FxAccountSetupTask<Void> {
+    protected static final String LOG_TAG = FxAccountUnlockCodeTask.class.getSimpleName();
+
+    protected final byte[] emailUTF8;
+
+    public FxAccountUnlockCodeTask(Context context, byte[] emailUTF8, FxAccountClient client, RequestDelegate<Void> delegate) {
+      super(context, null, client, delegate);
+      this.emailUTF8 = emailUTF8;
+    }
+
+    @Override
+    protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
+      try {
+        client.resendUnlockCode(emailUTF8, innerDelegate);
+        latch.await();
+        return innerDelegate;
+      } catch (Exception e) {
+        Logger.error(LOG_TAG, "Got exception signing in.", e);
+        delegate.handleError(e);
+      }
+      return null;
+    }
+  }
+
+  private static class ResendUnlockCodeDelegate implements RequestDelegate<Void> {
+    public final Context context;
+
+    public ResendUnlockCodeDelegate(Context context) {
+      this.context = context;
+    }
+
+    @Override
+    public void handleError(Exception e) {
+      Logger.warn(LOG_TAG, "Got exception requesting fresh unlock code; ignoring.", e);
+      Toast.makeText(context, R.string.fxaccount_unlock_code_not_sent, Toast.LENGTH_LONG).show();
+    }
+
+    @Override
+    public void handleFailure(FxAccountClientRemoteException e) {
+      handleError(e);
+    }
+
+    @Override
+    public void handleSuccess(Void result) {
+      Toast.makeText(context, R.string.fxaccount_unlock_code_sent, Toast.LENGTH_SHORT).show();
+    }
+  }
+
+  /**
+   * Resends the account unlock email, and displays an appropriate toast on both
+   * send success and failure. Note that because the underlying implementation
+   * uses {@link AsyncTask}, the provided context must be UI-capable and this
+   * method called from the UI thread.
+   *
+   * Note that it may actually be possible to run this (and the
+   * {@link AsyncTask}) method from a background thread - but this hasn't been
+   * tested.
+   *
+   * @param context
+   *          A UI-capable Android context.
+   * @param authServerURI
+   *          to send request to.
+   * @param emailUTF8
+   *          bytes of email address identifying account; null indicates a local failure.
+   */
+  public static void resendUnlockCode(Context context, String authServerURI, byte[] emailUTF8) {
+    RequestDelegate<Void> delegate = new ResendUnlockCodeDelegate(context);
+
+    if (emailUTF8 == null) {
+      delegate.handleError(new IllegalArgumentException("emailUTF8 must not be null"));
+      return;
+    }
+
+    final Executor executor = Executors.newSingleThreadExecutor();
+    final FxAccountClient client = new FxAccountClient20(authServerURI, executor);
+    new FxAccountUnlockCodeTask(context, emailUTF8, client, delegate).execute();
+  }
+}
--- a/mobile/android/base/home/RemoteTabsPanel.java
+++ b/mobile/android/base/home/RemoteTabsPanel.java
@@ -152,16 +152,18 @@ public class RemoteTabsPanel extends Hom
         case None:
             return new RemoteTabsExpandableListFragment();
         case NeedsVerification:
             return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_verification);
         case NeedsPassword:
             return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
         case NeedsUpgrade:
             return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_upgrade);
+        case NeedsFinishMigrating:
+            return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_finish_migrating);
         default:
             // This should never happen, but we're confident we have a Firefox
             // Account at this point, so let's show the needs password screen.
             // That's our best hope of righting the ship.
             Log.wtf(LOGTAG, "Got unexpected action needed; offering needs password.");
             return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
         }
     }
--- a/mobile/android/base/home/RemoteTabsStaticFragment.java
+++ b/mobile/android/base/home/RemoteTabsStaticFragment.java
@@ -5,16 +5,17 @@
 package org.mozilla.gecko.home;
 
 import java.util.EnumSet;
 import java.util.Locale;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity;
+import org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity;
 import org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -85,17 +86,18 @@ public class RemoteTabsStaticFragment ex
 
     @Override
     public void onViewCreated(View view, Bundle savedInstanceState) {
         for (int resourceId : new int[] {
                 R.id.remote_tabs_setup_get_started,
                 R.id.remote_tabs_setup_old_sync_link,
                 R.id.remote_tabs_needs_verification_resend_email,
                 R.id.remote_tabs_needs_verification_help,
-                R.id.remote_tabs_needs_password_sign_in, }) {
+                R.id.remote_tabs_needs_password_sign_in,
+                R.id.remote_tabs_needs_finish_migrating_sign_in, }) {
             maybeSetOnClickListener(view, resourceId);
         }
     }
 
     @Override
     public void onClick(final View v) {
         final int id = v.getId();
         if (id == R.id.remote_tabs_setup_get_started) {
@@ -111,20 +113,23 @@ public class RemoteTabsStaticFragment ex
         } else if (id == R.id.remote_tabs_needs_verification_resend_email) {
             // Send a fresh email; this displays a toast, so the user gets feedback.
             FirefoxAccounts.resendVerificationEmail(getActivity());
         } else if (id == R.id.remote_tabs_needs_verification_help) {
             // Don't allow switch-to-tab.
             final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
             mUrlOpenListener.onUrlOpen(CONFIRM_ACCOUNT_SUPPORT_URL, flags);
         } else if (id == R.id.remote_tabs_needs_password_sign_in) {
-            // This Activity will redirect to the correct Activity as needed.
             final Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             startActivity(intent);
+        } else if (id == R.id.remote_tabs_needs_finish_migrating_sign_in) {
+            final Intent intent = new Intent(getActivity(), FxAccountFinishMigratingActivity.class);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            startActivity(intent);
         }
     }
 
     @Override
     protected void load() {
         // We're static, so nothing to do here!
     }
 }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -424,16 +424,18 @@ size. -->
 <!-- Localization note (home_move_up_to_filter): The variable is replaced by the name of the
      previous location in the navigation, such as the previous folder -->
 <!ENTITY home_move_up_to_filter "Up to &formatS;">
 
 <!ENTITY home_remote_tabs_title "Synced Tabs">
 <!ENTITY home_remote_tabs_empty "Your tabs from other devices show up here.">
 <!ENTITY home_remote_tabs_unable_to_connect "Unable to connect">
 <!ENTITY home_remote_tabs_need_to_sign_in "Please sign in to reconnect your Firefox Account and continue syncing.">
+<!ENTITY home_remote_tabs_need_to_finish_migrating "Your new Firefox Account is ready!">
+
 <!ENTITY home_remote_tabs_trouble_verifying "Trouble verifying your account?">
 <!ENTITY home_remote_tabs_need_to_verify "Please verify your Firefox Account to start syncing.">
 
 <!ENTITY home_remote_tabs_one_hidden_device "1 device hidden">
 <!-- Localization note (home_remote_tabs_many_hidden_devices) : The
      formatD is replaced with the number of hidden devices.  The
      number of hidden devices is always more than one.  We can't use
      Android plural forms, sadly. See Bug #753859. -->
--- a/mobile/android/base/locales/en-US/sync_strings.dtd
+++ b/mobile/android/base/locales/en-US/sync_strings.dtd
@@ -165,44 +165,54 @@
 
 <!ENTITY fxaccount_confirm_account_header 'Confirm your account'>
 <!-- Localization note: &formatS; is the Firefox Account's email address. -->
 <!ENTITY fxaccount_confirm_account_verification_link 'A verification link has been sent to &formatS;'>
 <!ENTITY fxaccount_confirm_account_resend_email 'Resend email'>
 <!ENTITY fxaccount_confirm_account_change_email 'Forget this email address?'>
 <!ENTITY fxaccount_confirm_account_verification_link_sent2 'Verification email sent'>
 <!ENTITY fxaccount_confirm_account_verification_link_not_sent2 'Couldn\&apos;t send verification email'>
+<!ENTITY fxaccount_resend_unlock_code_button_label 'Resend unlock email'>
+<!ENTITY fxaccount_unlock_code_sent 'Account unlock email sent'>
+<!ENTITY fxaccount_unlock_code_not_sent 'Couldn\&apos;t send account unlock email'>
 
 <!ENTITY fxaccount_sign_in_sub_header 'Sign in'>
 <!ENTITY fxaccount_sign_in_button_label 'Sign in'>
 <!ENTITY fxaccount_sign_in_forgot_password 'Forgot password?'>
 <!ENTITY fxaccount_sign_in_create_account_instead 'Create an account'>
 <!ENTITY fxaccount_sign_in_unknown_error 'Could not sign in'>
 
 <!ENTITY fxaccount_account_verified_sub_header 'Account verified'>
 <!ENTITY fxaccount_account_verified_description2 'Your data will begin syncing momentarily.'>
 
+<!ENTITY fxaccount_migration_finished_header 'Upgrade finished'>
+
 <!ENTITY fxaccount_update_credentials_header 'Sign in'>
 <!ENTITY fxaccount_update_credentials_button_label 'Sign in'>
 <!ENTITY fxaccount_update_credentials_unknown_error 'Could not sign in'>
 
+<!ENTITY fxaccount_finish_migrating_header 'Sign in to finish upgrading'>
+<!ENTITY fxaccount_finish_migrating_button_label 'Finish upgrading'>
+<!ENTITY fxaccount_finish_migrating_description 'Upgrading can transfer a lot of data. It\&apos;s best to be on a WiFi network.'>
+
 <!ENTITY fxaccount_status_header2 'Firefox Account'>
 <!ENTITY fxaccount_status_signed_in_as 'Signed in as'>
 <!ENTITY fxaccount_status_auth_server 'Account server'>
 <!ENTITY fxaccount_status_sync_now 'Sync now'>
 <!ENTITY fxaccount_status_syncing2 'Syncing…'>
 <!ENTITY fxaccount_status_device_name 'Device name'>
 <!ENTITY fxaccount_status_sync_server 'Sync server'>
 <!ENTITY fxaccount_status_sync '&syncBrand.shortName.label;'>
 <!ENTITY fxaccount_status_sync_enabled '&syncBrand.shortName.label;: enabled'>
 <!ENTITY fxaccount_status_needs_verification2 'Your account needs to be verified. Tap to resend verification email.'>
 <!ENTITY fxaccount_status_needs_credentials 'Cannot connect. Tap to sign in.'>
 <!ENTITY fxaccount_status_needs_upgrade 'You need to upgrade &brandShortName; to sign in.'>
 <!ENTITY fxaccount_status_needs_master_sync_automatically_enabled '&syncBrand.shortName.label; is set up, but not syncing automatically. Toggle “Auto-sync data” in Android Settings &gt; Data Usage.'>
 <!ENTITY fxaccount_status_needs_account_enabled '&syncBrand.shortName.label; is set up, but not syncing automatically. Tap to start syncing.'>
+<!ENTITY fxaccount_status_needs_finish_migrating 'Tap to sign in to your new Firefox Account.'>
 <!ENTITY fxaccount_status_bookmarks 'Bookmarks'>
 <!ENTITY fxaccount_status_history 'History'>
 <!ENTITY fxaccount_status_passwords 'Passwords'>
 <!ENTITY fxaccount_status_tabs 'Open tabs'>
 <!ENTITY fxaccount_status_legal 'Legal' >
 <!-- Localization note: when tapped, the following two strings link to
      external web pages.  Compare fxaccount_policy_{linktos,linkprivacy}:
      these strings are separated to accommodate languages that decline
@@ -240,13 +250,19 @@
 <!ENTITY fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS_2 'Account already exists. &formatS1;'>
 <!ENTITY fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST 'Invalid email or password'>
 <!ENTITY fxaccount_remote_error_INCORRECT_PASSWORD 'Invalid email or password'>
 <!ENTITY fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT 'Account is not verified'>
 <!ENTITY fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS 'Server busy, try again soon'>
 <!ENTITY fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD 'Server busy, try again soon'>
 <!ENTITY fxaccount_remote_error_UNKNOWN_ERROR 'There was a problem'>
 <!ENTITY fxaccount_remote_error_COULD_NOT_CONNECT 'Cannot connect to network'>
+<!ENTITY fxaccount_remote_error_ACCOUNT_LOCKED 'Account is locked. &formatS1;'>
 
 <!ENTITY fxaccount_sync_sign_in_error_notification_title2 '&syncBrand.shortName.label; is not connected'>
 <!-- Localization note: the format string below will be replaced
      with the Firefox Account's email address. -->
 <!ENTITY fxaccount_sync_sign_in_error_notification_text2 'Tap to sign in as &formatS;'>
+
+<!ENTITY fxaccount_sync_finish_migrating_notification_title 'Finish upgrading &syncBrand.shortName.label;?'>
+<!-- Localization note: the format string below will be replaced
+     with the Firefox Account's email address. -->
+<!ENTITY fxaccount_sync_finish_migrating_notification_text 'Tap to sign in as &formatS;'>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/fxaccount_finish_migrating.xml
@@ -0,0 +1,61 @@
+<?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/.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:fillViewport="true" >
+
+    <LinearLayout
+        android:id="@+id/update_credentials_view"
+        style="@style/FxAccountMiddle" >
+
+        <LinearLayout style="@style/FxAccountSpacer" />
+
+        <TextView
+            style="@style/FxAccountHeaderItem"
+            android:text="@string/fxaccount_finish_migrating_header" />
+
+        <include layout="@layout/fxaccount_custom_server_view" />
+
+        <include layout="@layout/fxaccount_email_password_view" />
+
+        <TextView
+            style="@style/FxAccountTextItem"
+            android:layout_marginTop="10dp"
+            android:text="@string/fxaccount_finish_migrating_description" />
+
+        <TextView
+            android:id="@+id/remote_error"
+            style="@style/FxAccountErrorItem" />
+
+        <RelativeLayout style="@style/FxAccountButtonLayout" >
+
+            <ProgressBar
+                android:id="@+id/progress"
+                style="@style/FxAccountProgress" />
+
+            <Button
+                android:id="@+id/button"
+                style="@style/FxAccountButton"
+                android:text="@string/fxaccount_finish_migrating_button_label" />
+        </RelativeLayout>
+
+        <TextView
+            android:id="@+id/forgot_password_link"
+            style="@style/FxAccountLinkifiedItem"
+            android:layout_marginTop="10dp"
+            android:text="@string/fxaccount_sign_in_forgot_password" />
+
+        <LinearLayout style="@style/FxAccountSpacer" />
+
+        <ImageView
+            style="@style/FxAccountIcon"
+            android:contentDescription="@string/fxaccount_empty_contentDescription" />
+    </LinearLayout>
+
+</ScrollView>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/fxaccount_migration_finished.xml
@@ -0,0 +1,46 @@
+<?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/.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:fillViewport="true" >
+
+    <LinearLayout style="@style/FxAccountMiddle" >
+
+        <TextView
+            style="@style/FxAccountHeaderItem"
+            android:text="@string/fxaccount_migration_finished_header" >
+        </TextView>
+
+        <ImageView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_marginBottom="45dp"
+            android:contentDescription="@string/fxaccount_empty_contentDescription"
+            android:src="@drawable/fxaccount_checkbox" >
+        </ImageView>
+
+        <TextView
+            style="@style/FxAccountTextItem"
+            android:layout_marginBottom="40dp"
+            android:text="@string/fxaccount_migration_finished_description"
+            android:textSize="18sp" />
+
+        <Button
+            android:id="@+id/button"
+            style="@style/FxAccountButton"
+            android:text="@string/fxaccount_back_to_browsing" />
+
+        <LinearLayout style="@style/FxAccountSpacer" />
+
+        <ImageView
+            style="@style/FxAccountIcon"
+            android:contentDescription="@string/fxaccount_empty_contentDescription" />
+    </LinearLayout>
+
+</ScrollView>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/remote_tabs_needs_finish_migrating.xml
@@ -0,0 +1,34 @@
+<?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/.
+-->
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" >
+
+    <LinearLayout style="@style/RemoteTabsPanelFrame" >
+
+        <TextView
+            style="@style/RemoteTabsPanelItem.TextAppearance.Header"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/fxaccount_finish_migrating_header" />
+
+        <TextView
+            style="@style/RemoteTabsPanelItem.TextAppearance"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/home_remote_tabs_need_to_finish_migrating" />
+
+        <Button
+            android:id="@+id/remote_tabs_needs_finish_migrating_sign_in"
+            style="@style/RemoteTabsPanelItem.Button"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/fxaccount_finish_migrating_button_label" />
+    </LinearLayout>
+
+</ScrollView>
--- a/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml
+++ b/mobile/android/base/resources/xml/fxaccount_status_prefscreen.xml
@@ -49,16 +49,23 @@
             android:title="@string/fxaccount_status_needs_master_sync_automatically_enabled" />
         <Preference
             android:editable="false"
             android:icon="@drawable/fxaccount_sync_error"
             android:key="needs_account_enabled"
             android:layout="@layout/fxaccount_status_error_preference"
             android:persistent="false"
             android:title="@string/fxaccount_status_needs_account_enabled" />
+        <Preference
+            android:editable="false"
+            android:icon="@drawable/fxaccount_sync_error"
+            android:key="needs_finish_migrating"
+            android:layout="@layout/fxaccount_status_error_preference"
+            android:persistent="false"
+            android:title="@string/fxaccount_status_needs_finish_migrating" />
 
         <Preference
             android:editable="false"
             android:key="sync_now"
             android:defaultValue=""
             android:persistent="false"
             android:title="Sync now"
             android:summary="" />
@@ -120,11 +127,12 @@
     <PreferenceCategory
         android:key="debug_category" >
         <Preference android:key="debug_refresh" />
         <Preference android:key="debug_dump" />
         <Preference android:key="debug_force_sync" />
         <Preference android:key="debug_forget_certificate" />
         <Preference android:key="debug_require_password" />
         <Preference android:key="debug_require_upgrade" />
+        <Preference android:key="debug_migrated_from_sync11" />
     </PreferenceCategory>
 
 </PreferenceScreen>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -364,16 +364,17 @@
   <string name="home_reading_list_hint">&home_reading_list_hint2;</string>
   <string name="home_reading_list_hint_accessible">&home_reading_list_hint_accessible;</string>
   <string name="home_default_empty">&home_default_empty;</string>
   <string name="home_move_up_to_filter">&home_move_up_to_filter;</string>
   <string name="home_remote_tabs_title">&home_remote_tabs_title;</string>
   <string name="home_remote_tabs_empty">&home_remote_tabs_empty;</string>
   <string name="home_remote_tabs_unable_to_connect">&home_remote_tabs_unable_to_connect;</string>
   <string name="home_remote_tabs_need_to_sign_in">&home_remote_tabs_need_to_sign_in;</string>
+  <string name="home_remote_tabs_need_to_finish_migrating">&home_remote_tabs_need_to_finish_migrating;</string>
   <string name="home_remote_tabs_trouble_verifying">&home_remote_tabs_trouble_verifying;</string>
   <string name="home_remote_tabs_need_to_verify">&home_remote_tabs_need_to_verify;</string>
   <string name="home_remote_tabs_one_hidden_device">&home_remote_tabs_one_hidden_device;</string>
   <string name="home_remote_tabs_many_hidden_devices">&home_remote_tabs_many_hidden_devices;</string>
   <string name="home_remote_tabs_hidden_devices_title">&home_remote_tabs_hidden_devices_title;</string>
   <string name="home_remote_tabs_unhide_selected_devices">&home_remote_tabs_unhide_selected_devices;</string>
   <string name="private_browsing_title">&private_browsing_title;</string>
   <string name="private_tabs_panel_empty_desc">&private_tabs_panel_empty_desc;</string>
--- a/mobile/android/base/sync/Utils.java
+++ b/mobile/android/base/sync/Utils.java
@@ -30,16 +30,19 @@ import org.mozilla.apache.commons.codec.
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.nativecode.NativeCrypto;
 import org.mozilla.gecko.sync.setup.Constants;
 
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.Bundle;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.style.ClickableSpan;
 
 public class Utils {
 
   private static final String LOG_TAG = "Utils";
 
   private static final SecureRandom sharedSecureRandom = new SecureRandom();
 
   // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29>
@@ -609,9 +612,31 @@ public class Utils {
     }
 
     String country = locale.getCountry();    // Can be an empty string.
     if (country.equals("")) {
       return language;
     }
     return language + "-" + country;
   }
+
+  /**
+   * Make a span with a clickable chunk of text interpolated in.
+   *
+   * @param context Android context.
+   * @param messageId of string containing clickable chunk.
+   * @param clickableId of string to make clickable.
+   * @param clickableSpan to activate on click.
+   * @return Spannable.
+   */
+  public static Spannable interpolateClickableSpan(Context context, int messageId, int clickableId, ClickableSpan clickableSpan) {
+    // This horrible bit of special-casing is because we want this error message to
+    // contain a clickable, extra chunk of text, but we don't want to pollute
+    // the exception class with Android specifics.
+    final String clickablePart = context.getString(clickableId);
+    final String message = context.getString(messageId, clickablePart);
+    final int clickableStart = message.lastIndexOf(clickablePart);
+    final int clickableEnd = clickableStart + clickablePart.length();
+    final Spannable span = Spannable.Factory.getInstance().newSpannable(message);
+    span.setSpan(clickableSpan, clickableStart, clickableEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+    return span;
+  }
 }
--- a/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
+++ b/mobile/android/services/manifests/FxAccountAndroidManifest_activities.xml.in
@@ -64,16 +64,31 @@
             android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity"
             android:configChanges="locale|layoutDirection"
             android:windowSoftInputMode="adjustResize">
         </activity>
 
         <activity
             android:theme="@style/FxAccountTheme"
+            android:name="org.mozilla.gecko.fxa.activities.FxAccountFinishMigratingActivity"
+            android:configChanges="locale|layoutDirection"
+            android:windowSoftInputMode="adjustResize">
+        </activity>
+
+        <activity
+            android:theme="@style/FxAccountTheme"
+            android:name="org.mozilla.gecko.fxa.activities.FxAccountMigrationFinishedActivity"
+            android:configChanges="locale|layoutDirection"
+            android:noHistory="true"
+            android:windowSoftInputMode="adjustResize">
+        </activity>
+
+        <activity
+            android:theme="@style/FxAccountTheme"
             android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountNotAllowedActivity"
             android:configChanges="locale|layoutDirection"
             android:noHistory="true"
             android:windowSoftInputMode="adjustResize">
         </activity>
 
         <receiver
             android:name="org.mozilla.gecko.fxa.receivers.FxAccountDeletedReceiver"
--- a/mobile/android/services/strings.xml.in
+++ b/mobile/android/services/strings.xml.in
@@ -152,46 +152,57 @@
 <string name="fxaccount_account_create_not_allowed_learn_more">&fxaccount_account_create_not_allowed_learn_more;</string>
 
 <string name="fxaccount_confirm_account_header">&fxaccount_confirm_account_header;</string>
 <string name="fxaccount_confirm_account_verification_link">&fxaccount_confirm_account_verification_link;</string>
 <string name="fxaccount_confirm_account_resend_email">&fxaccount_confirm_account_resend_email;</string>
 <string name="fxaccount_confirm_account_change_email">&fxaccount_confirm_account_change_email;</string>
 <string name="fxaccount_confirm_account_verification_link_sent">&fxaccount_confirm_account_verification_link_sent2;</string>
 <string name="fxaccount_confirm_account_verification_link_not_sent">&fxaccount_confirm_account_verification_link_not_sent2;</string>
+<string name="fxaccount_resend_unlock_code_button_label">&fxaccount_resend_unlock_code_button_label;</string>
+<string name="fxaccount_unlock_code_sent">&fxaccount_unlock_code_sent;</string>
+<string name="fxaccount_unlock_code_not_sent">&fxaccount_unlock_code_not_sent;</string>
 
 <string name="fxaccount_sign_in_sub_header">&fxaccount_sign_in_sub_header;</string>
 <string name="fxaccount_sign_in_button_label">&fxaccount_sign_in_button_label;</string>
 <string name="fxaccount_sign_in_forgot_password">&fxaccount_sign_in_forgot_password;</string>
 <string name="fxaccount_sign_in_create_account_instead">&fxaccount_sign_in_create_account_instead;</string>
 <string name="fxaccount_sign_in_unknown_error">&fxaccount_sign_in_unknown_error;</string>
 
 <string name="fxaccount_account_verified_sub_header">&fxaccount_account_verified_sub_header;</string>
 <string name="fxaccount_account_verified_description">&fxaccount_account_verified_description2;</string>
 
+<string name="fxaccount_migration_finished_header">&fxaccount_migration_finished_header;</string>
+<string name="fxaccount_migration_finished_description">&fxaccount_account_verified_description2;</string>
+
 <string name="fxaccount_update_credentials_header">&fxaccount_update_credentials_header;</string>
 <string name="fxaccount_update_credentials_button_label">&fxaccount_update_credentials_button_label;</string>
 <string name="fxaccount_update_credentials_unknown_error">&fxaccount_update_credentials_unknown_error;</string>
 
+<string name="fxaccount_finish_migrating_header">&fxaccount_finish_migrating_header;</string>
+<string name="fxaccount_finish_migrating_button_label">&fxaccount_finish_migrating_button_label;</string>
+<string name="fxaccount_finish_migrating_description">&fxaccount_finish_migrating_description;</string>
+
 <string name="fxaccount_status_activity_label">&syncBrand.shortName.label;</string>
 <string name="fxaccount_status_header">&fxaccount_status_header2;</string>
 <string name="fxaccount_status_signed_in_as">&fxaccount_status_signed_in_as;</string>
 <string name="fxaccount_status_auth_server">&fxaccount_status_auth_server;</string>
 <string name="fxaccount_status_sync_now">&fxaccount_status_sync_now;</string>
 <string name="fxaccount_status_syncing">&fxaccount_status_syncing2;</string>
 <string name="fxaccount_status_last_synced">&remote_tabs_last_synced;</string>
 <string name="fxaccount_status_device_name">&fxaccount_status_device_name;</string>
 <string name="fxaccount_status_sync_server">&fxaccount_status_sync_server;</string>
 <string name="fxaccount_status_sync">&fxaccount_status_sync;</string>
 <string name="fxaccount_status_sync_enabled">&fxaccount_status_sync_enabled;</string>
 <string name="fxaccount_status_needs_verification">&fxaccount_status_needs_verification2;</string>
 <string name="fxaccount_status_needs_credentials">&fxaccount_status_needs_credentials;</string>
 <string name="fxaccount_status_needs_upgrade">&fxaccount_status_needs_upgrade;</string>
 <string name="fxaccount_status_needs_master_sync_automatically_enabled">&fxaccount_status_needs_master_sync_automatically_enabled;</string>
 <string name="fxaccount_status_needs_account_enabled">&fxaccount_status_needs_account_enabled;</string>
+<string name="fxaccount_status_needs_finish_migrating">&fxaccount_status_needs_finish_migrating;</string>
 <string name="fxaccount_status_bookmarks">&fxaccount_status_bookmarks;</string>
 <string name="fxaccount_status_history">&fxaccount_status_history;</string>
 <string name="fxaccount_status_passwords">&fxaccount_status_passwords;</string>
 <string name="fxaccount_status_tabs">&fxaccount_status_tabs;</string>
 <string name="fxaccount_status_legal">&fxaccount_status_legal;</string>
 <string name="fxaccount_status_linktos">&fxaccount_status_linktos;</string>
 <string name="fxaccount_status_linkprivacy">&fxaccount_status_linkprivacy;</string>
 <string name="fxaccount_status_more">&fxaccount_status_more;</string>
@@ -205,17 +216,21 @@
 <string name="fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS">&fxaccount_remote_error_ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS_2;</string>
 <string name="fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST">&fxaccount_remote_error_ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST;</string>
 <string name="fxaccount_remote_error_INCORRECT_PASSWORD">&fxaccount_remote_error_INCORRECT_PASSWORD;</string>
 <string name="fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT">&fxaccount_remote_error_ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT;</string>
 <string name="fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS">&fxaccount_remote_error_CLIENT_HAS_SENT_TOO_MANY_REQUESTS;</string>
 <string name="fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD">&fxaccount_remote_error_SERVICE_TEMPORARILY_UNAVAILABLE_TO_DUE_HIGH_LOAD;</string>
 <string name="fxaccount_remote_error_UNKNOWN_ERROR">&fxaccount_remote_error_UNKNOWN_ERROR;</string>
 <string name="fxaccount_remote_error_COULD_NOT_CONNECT">&fxaccount_remote_error_COULD_NOT_CONNECT;</string>
+<string name="fxaccount_remote_error_ACCOUNT_LOCKED">&fxaccount_remote_error_ACCOUNT_LOCKED;</string>
 
 <string name="fxaccount_sync_sign_in_error_notification_title">&fxaccount_sync_sign_in_error_notification_title2;</string>
 <string name="fxaccount_sync_sign_in_error_notification_text">&fxaccount_sync_sign_in_error_notification_text2;</string>
 
 <!-- Remove Account -->
 <string name="fxaccount_remove_account_dialog_title">&fxaccount_remove_account_dialog_title;</string>
 <string name="fxaccount_remove_account_dialog_message">&fxaccount_remove_account_dialog_message;</string>
 <string name="fxaccount_remove_account_toast">&fxaccount_remove_account_toast;</string>
 <string name="fxaccount_remove_account_menu_item">&fxaccount_remove_account_menu_item;</string>
+
+<string name="fxaccount_sync_finish_migrating_notification_title">&fxaccount_sync_finish_migrating_notification_title;</string>
+<string name="fxaccount_sync_finish_migrating_notification_text">&fxaccount_sync_finish_migrating_notification_text;</string>
new file mode 100644
--- /dev/null
+++ b/testing/xpcshell/dbg-actors.js
@@ -0,0 +1,51 @@
+/* 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 { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
+let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const { RootActor } = devtools.require("devtools/server/actors/root");
+const { BrowserTabList } = devtools.require("devtools/server/actors/webbrowser");
+
+/**
+ * xpcshell-test (XPCST) specific actors.
+ *
+ */
+
+/**
+ * Construct a root actor appropriate for use in a server running xpcshell
+ * tests. <snip boilerplate> :)
+ */
+function createRootActor(connection)
+{
+  let parameters = {
+    tabList: new XPCSTTabList(connection),
+    globalActorFactories: DebuggerServer.globalActorFactories,
+    onShutdown() {
+      // If the user never switches to the "debugger" tab we might get a
+      // shutdown before we've attached.
+      Services.obs.notifyObservers(null, "xpcshell-test-devtools-shutdown", null);
+    }
+  };
+  return new RootActor(connection, parameters);
+}
+
+/**
+ * A "stub" TabList implementation that provides no tabs.
+ */
+
+function XPCSTTabList(connection)
+{
+  BrowserTabList.call(this, connection);
+}
+
+XPCSTTabList.prototype = Object.create(BrowserTabList.prototype);
+
+XPCSTTabList.prototype.constructor = XPCSTTabList;
+
+XPCSTTabList.prototype.getList = function() {
+  return Promise.resolve([]);
+};
--- a/testing/xpcshell/head.js
+++ b/testing/xpcshell/head.js
@@ -331,17 +331,111 @@ function _register_modules_protocol_hand
                     _TESTING_MODULES_DIR);
   }
 
   let modulesURI = ios.newFileURI(modulesFile);
 
   protocolHandler.setSubstitution("testing-common", modulesURI);
 }
 
+function _initDebugging(port) {
+  let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+              .getService(Components.interfaces.nsIPrefBranch);
+
+  // Always allow remote debugging.
+  prefs.setBoolPref("devtools.debugger.remote-enabled", true);
+
+  // for debugging-the-debugging, let an env var cause log spew.
+  let env = Components.classes["@mozilla.org/process/environment;1"]
+                      .getService(Components.interfaces.nsIEnvironment);
+  if (env.get("DEVTOOLS_DEBUGGER_LOG")) {
+    prefs.setBoolPref("devtools.debugger.log", true);
+  }
+  if (env.get("DEVTOOLS_DEBUGGER_LOG_VERBOSE")) {
+    prefs.setBoolPref("devtools.debugger.log.verbose", true);
+  }
+
+  let {DebuggerServer} = Components.utils.import('resource://gre/modules/devtools/dbg-server.jsm', {});
+  DebuggerServer.init(() => true);
+  DebuggerServer.addBrowserActors();
+  DebuggerServer.addActors("resource://testing-common/dbg-actors.js");
+
+  // An observer notification that tells us when we can "resume" script
+  // execution.
+  let obsSvc = Components.classes["@mozilla.org/observer-service;1"].
+               getService(Components.interfaces.nsIObserverService);
+  let initialized = false;
+
+  const TOPICS = ["devtools-thread-resumed", "xpcshell-test-devtools-shutdown"];
+  let observe = function(subject, topic, data) {
+    switch (topic) {
+      case "devtools-thread-resumed":
+        // Exceptions in here aren't reported and block the debugger from
+        // resuming, so...
+        try {
+          // Add a breakpoint for the first line in our test files.
+          let threadActor = subject.wrappedJSObject;
+          let location = { line: 1 };
+          for (let file of _TEST_FILE) {
+            let sourceActor = threadActor.sources.source({originalUrl: file});
+            sourceActor.createAndStoreBreakpoint(location);
+          }
+        } catch (ex) {
+          do_print("Failed to initialize breakpoints: " + ex + "\n" + ex.stack);
+        }
+        break;
+      case "xpcshell-test-devtools-shutdown":
+        // the debugger has shutdown before we got a resume event - nothing
+        // special to do here.
+        break;
+    }
+    initialized = true;
+    for (let topicToRemove of TOPICS) {
+      obsSvc.removeObserver(observe, topicToRemove);
+    }
+  };
+
+  for (let topic of TOPICS) {
+    obsSvc.addObserver(observe, topic, false);
+  }
+
+  do_print("");
+  do_print("*******************************************************************");
+  do_print("Waiting for the debugger to connect on port " + port)
+  do_print("")
+  do_print("To connect the debugger, open a Firefox instance, select 'Connect'");
+  do_print("from the Developer menu and specify the port as " + port);
+  do_print("*******************************************************************");
+  do_print("")
+
+  DebuggerServer.openListener(port);
+
+  // spin an event loop until the debugger connects.
+  let thr = Components.classes["@mozilla.org/thread-manager;1"]
+              .getService().currentThread;
+  while (!initialized) {
+    do_print("Still waiting for debugger to connect...");
+    thr.processNextEvent(true);
+  }
+  // NOTE: if you want to debug the harness itself, you can now add a 'debugger'
+  // statement anywhere and it will stop - but we've already added a breakpoint
+  // for the first line of the test scripts, so we just continue...
+  do_print("Debugger connected, starting test execution");
+}
+
 function _execute_test() {
+  // _JSDEBUGGER_PORT is dynamically defined by <runxpcshelltests.py>.
+  if (_JSDEBUGGER_PORT) {
+    try {
+      _initDebugging(_JSDEBUGGER_PORT);
+    } catch (ex) {
+      do_print("Failed to initialize debugging: " + ex + "\n" + ex.stack);
+    }
+  }
+
   _register_protocol_handlers();
 
   // Override idle service by default.
   // Call do_get_idle() to restore the factory and get the service.
   _fakeIdleService.activate();
 
   _Promise.Debugging.clearUncaughtErrorObservers();
   _Promise.Debugging.addUncaughtErrorObserver(function observer({message, date, fileName, stack, lineNumber}) {
@@ -1067,16 +1161,18 @@ function do_load_child_test_harness()
   _XPCSHELL_PROCESS = "parent";
 
   let command =
         "const _HEAD_JS_PATH=" + uneval(_HEAD_JS_PATH) + "; "
       + "const _HTTPD_JS_PATH=" + uneval(_HTTPD_JS_PATH) + "; "
       + "const _HEAD_FILES=" + uneval(_HEAD_FILES) + "; "
       + "const _TAIL_FILES=" + uneval(_TAIL_FILES) + "; "
       + "const _TEST_NAME=" + uneval(_TEST_NAME) + "; "
+      // We'll need more magic to get the debugger working in the child
+      + "const _JSDEBUGGER_PORT=0; "
       + "const _XPCSHELL_PROCESS='child';";
 
   if (this._TESTING_MODULES_DIR) {
     command += " const _TESTING_MODULES_DIR=" + uneval(_TESTING_MODULES_DIR) + ";";
   }
 
   command += " load(_HEAD_JS_PATH);";
   sendCommand(command);
--- a/testing/xpcshell/mach_commands.py
+++ b/testing/xpcshell/mach_commands.py
@@ -60,16 +60,17 @@ class XPCShellRunner(MozbuildObject):
         manifest = TestManifest(manifests=[os.path.join(self.topobjdir,
             '_tests', 'xpcshell', 'xpcshell.ini')])
 
         return self._run_xpcshell_harness(manifest=manifest, **kwargs)
 
     def run_test(self, test_paths, interactive=False,
                  keep_going=False, sequential=False, shuffle=False,
                  debugger=None, debuggerArgs=None, debuggerInteractive=None,
+                 jsDebugger=False, jsDebuggerPort=None,
                  rerun_failures=False, test_objects=None, verbose=False,
                  log=None,
                  # ignore parameters from other platforms' options
                  **kwargs):
         """Runs an individual xpcshell test."""
         from mozbuild.testing import TestResolver
         from manifestparser import TestManifest
 
@@ -78,16 +79,17 @@ class XPCShellRunner(MozbuildObject):
         if build_path not in sys.path:
             sys.path.append(build_path)
 
         if test_paths == ['all']:
             self.run_suite(interactive=interactive,
                            keep_going=keep_going, shuffle=shuffle, sequential=sequential,
                            debugger=debugger, debuggerArgs=debuggerArgs,
                            debuggerInteractive=debuggerInteractive,
+                           jsDebugger=jsDebugger, jsDebuggerPort=jsDebuggerPort,
                            rerun_failures=rerun_failures,
                            verbose=verbose, log=log)
             return
         elif test_paths:
             test_paths = [self._wrap_path_argument(p).relpath() for p in test_paths]
 
         if test_objects:
             tests = test_objects
@@ -108,28 +110,31 @@ class XPCShellRunner(MozbuildObject):
         args = {
             'interactive': interactive,
             'keep_going': keep_going,
             'shuffle': shuffle,
             'sequential': sequential,
             'debugger': debugger,
             'debuggerArgs': debuggerArgs,
             'debuggerInteractive': debuggerInteractive,
+            'jsDebugger': jsDebugger,
+            'jsDebuggerPort': jsDebuggerPort,
             'rerun_failures': rerun_failures,
             'manifest': manifest,
             'verbose': verbose,
             'log': log,
         }
 
         return self._run_xpcshell_harness(**args)
 
     def _run_xpcshell_harness(self, manifest,
                               test_path=None, shuffle=False, interactive=False,
                               keep_going=False, sequential=False,
                               debugger=None, debuggerArgs=None, debuggerInteractive=None,
+                              jsDebugger=False, jsDebuggerPort=None,
                               rerun_failures=False, verbose=False, log=None):
 
         # Obtain a reference to the xpcshell test runner.
         import runxpcshelltests
 
         xpcshell = runxpcshelltests.XPCShellTests(log=log)
         self.log_manager.enable_unstructured()
 
@@ -156,16 +161,18 @@ class XPCShellRunner(MozbuildObject):
             'profileName': 'firefox',
             'verbose': verbose or single_test,
             'xunitFilename': os.path.join(self.statedir, 'xpchsell.xunit.xml'),
             'xunitName': 'xpcshell',
             'pluginsPath': os.path.join(self.distdir, 'plugins'),
             'debugger': debugger,
             'debuggerArgs': debuggerArgs,
             'debuggerInteractive': debuggerInteractive,
+            'jsDebugger': jsDebugger,
+            'jsDebuggerPort': jsDebuggerPort,
         }
 
         if test_path is not None:
             args['testPath'] = test_path
 
         # A failure manifest is written by default. If --rerun-failures is
         # specified and a prior failure manifest is found, the prior manifest
         # will be run. A new failure manifest is always written over any
@@ -412,16 +419,23 @@ class MachCommands(MachCommandBase):
     @CommandArgument("--debugger-args", default=None, metavar='ARGS', type=str,
                      dest = "debuggerArgs",
                      help = "pass the given args to the debugger _before_ "
                             "the application on the command line")
     @CommandArgument("--debugger-interactive", action = "store_true",
                      dest = "debuggerInteractive",
                      help = "prevents the test harness from redirecting "
                             "stdout and stderr for interactive debuggers")
+    @CommandArgument("--jsdebugger", dest="jsDebugger", action="store_true",
+                     help="Waits for a devtools JS debugger to connect before "
+                          "starting the test.")
+    @CommandArgument("--jsdebugger-port", dest="jsDebuggerPort",
+                     type=int, default=6000,
+                     help="The port to listen on for a debugger connection if "
+                          "--jsdebugger is specified (default=6000).")
     @CommandArgument('--interactive', '-i', action='store_true',
         help='Open an xpcshell prompt before running tests.')
     @CommandArgument('--keep-going', '-k', action='store_true',
         help='Continue running tests after a SIGINT is received.')
     @CommandArgument('--sequential', action='store_true',
         help='Run the tests sequentially.')
     @CommandArgument('--shuffle', '-s', action='store_true',
         help='Randomize the execution order of tests.')
--- a/testing/xpcshell/moz.build
+++ b/testing/xpcshell/moz.build
@@ -4,8 +4,12 @@
 # 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_DIRS += ['example']
 
 PYTHON_UNIT_TESTS += [
     'selftest.py',
 ]
+
+TESTING_JS_MODULES += [
+    'dbg-actors.js',
+]
--- a/testing/xpcshell/runxpcshelltests.py
+++ b/testing/xpcshell/runxpcshelltests.py
@@ -14,17 +14,17 @@ import os.path
 import random
 import re
 import shutil
 import signal
 import sys
 import time
 import traceback
 
-from collections import deque
+from collections import deque, namedtuple
 from distutils import dir_util
 from multiprocessing import cpu_count
 from optparse import OptionParser
 from subprocess import Popen, PIPE, STDOUT
 from tempfile import mkdtemp, gettempdir
 from threading import (
     Timer,
     Thread,
@@ -106,16 +106,17 @@ class XPCShellTestThread(Thread):
         self.test_object = test_object
         self.cleanup_dir_list = cleanup_dir_list
         self.retry = retry
 
         self.appPath = kwargs.get('appPath')
         self.xrePath = kwargs.get('xrePath')
         self.testingModulesDir = kwargs.get('testingModulesDir')
         self.debuggerInfo = kwargs.get('debuggerInfo')
+        self.jsDebuggerInfo = kwargs.get('jsDebuggerInfo')
         self.pluginsPath = kwargs.get('pluginsPath')
         self.httpdManifest = kwargs.get('httpdManifest')
         self.httpdJSPath = kwargs.get('httpdJSPath')
         self.headJSPath = kwargs.get('headJSPath')
         self.testharnessdir = kwargs.get('testharnessdir')
         self.profileName = kwargs.get('profileName')
         self.singleFile = kwargs.get('singleFile')
         self.env = copy.deepcopy(kwargs.get('env'))
@@ -361,20 +362,25 @@ class XPCShellTestThread(Thread):
           along with the address of the webserver which some tests require.
 
           On a remote system, this is overloaded to resolve quoting issues over a secondary command line.
         """
         cmdH = ", ".join(['"' + f.replace('\\', '/') + '"'
                        for f in headfiles])
         cmdT = ", ".join(['"' + f.replace('\\', '/') + '"'
                        for f in tailfiles])
+
+        dbgport = 0 if self.jsDebuggerInfo is None else self.jsDebuggerInfo.port
+
         return xpcscmd + \
                 ['-e', 'const _SERVER_ADDR = "localhost"',
                  '-e', 'const _HEAD_FILES = [%s];' % cmdH,
-                 '-e', 'const _TAIL_FILES = [%s];' % cmdT]
+                 '-e', 'const _TAIL_FILES = [%s];' % cmdT,
+                 '-e', 'const _JSDEBUGGER_PORT = %d;' % dbgport,
+                ]
 
     def getHeadAndTailFiles(self, test_object):
         """Obtain the list of head and tail files.
 
         Returns a 2-tuple. The first element is a list of head files. The second
         is a list of tail files.
         """
         def sanitize_list(s, kind):
@@ -627,17 +633,17 @@ class XPCShellTestThread(Thread):
             self.env['DMD_PRELOAD_VALUE'] = libdmd
 
         testTimeoutInterval = self.harness_timeout
         # Allow a test to request a multiple of the timeout if it is expected to take long
         if 'requesttimeoutfactor' in self.test_object:
             testTimeoutInterval *= int(self.test_object['requesttimeoutfactor'])
 
         testTimer = None
-        if not self.interactive and not self.debuggerInfo:
+        if not self.interactive and not self.debuggerInfo and not self.jsDebuggerInfo:
             testTimer = Timer(testTimeoutInterval, lambda: self.testTimeout(proc))
             testTimer.start()
 
         proc = None
         process_output = None
 
         try:
             self.log.test_start(name)
@@ -999,17 +1005,18 @@ class XPCShellTests(object):
     def runTests(self, xpcshell, xrePath=None, appPath=None, symbolsPath=None,
                  manifest=None, testdirs=None, testPath=None, mobileArgs=None,
                  interactive=False, verbose=False, keepGoing=False, logfiles=True,
                  thisChunk=1, totalChunks=1, debugger=None,
                  debuggerArgs=None, debuggerInteractive=False,
                  profileName=None, mozInfo=None, sequential=False, shuffle=False,
                  testsRootDir=None, testingModulesDir=None, pluginsPath=None,
                  testClass=XPCShellTestThread, failureManifest=None,
-                 log=None, stream=None, **otherOptions):
+                 log=None, stream=None, jsDebugger=False, jsDebuggerPort=0,
+                 **otherOptions):
         """Run xpcshell tests.
 
         |xpcshell|, is the xpcshell executable to use to run the tests.
         |xrePath|, if provided, is the path to the XRE to use.
         |appPath|, if provided, is the path to an application directory.
         |symbolsPath|, if provided is the path to a directory containing
           breakpad symbols for processing crashes in tests.
         |manifest|, if provided, is a file containing a list of
@@ -1070,16 +1077,22 @@ class XPCShellTests(object):
             if not testingModulesDir.endswith(os.path.sep):
                 testingModulesDir += os.path.sep
 
         self.debuggerInfo = None
 
         if debugger:
             self.debuggerInfo = mozdebug.get_debugger_info(debugger, debuggerArgs, debuggerInteractive)
 
+        self.jsDebuggerInfo = None
+        if jsDebugger:
+            # A namedtuple let's us keep .port instead of ['port']
+            JSDebuggerInfo = namedtuple('JSDebuggerInfo', ['port'])
+            self.jsDebuggerInfo = JSDebuggerInfo(port=jsDebuggerPort)
+
         self.xpcshell = xpcshell
         self.xrePath = xrePath
         self.appPath = appPath
         self.symbolsPath = symbolsPath
         self.manifest = manifest
         self.testdirs = testdirs
         self.testPath = testPath
         self.interactive = interactive
@@ -1156,16 +1169,17 @@ class XPCShellTests(object):
         self.cleanup_dir_list = []
         self.try_again_list = []
 
         kwargs = {
             'appPath': self.appPath,
             'xrePath': self.xrePath,
             'testingModulesDir': self.testingModulesDir,
             'debuggerInfo': self.debuggerInfo,
+            'jsDebuggerInfo': self.jsDebuggerInfo,
             'pluginsPath': self.pluginsPath,
             'httpdManifest': self.httpdManifest,
             'httpdJSPath': self.httpdJSPath,
             'headJSPath': self.headJSPath,
             'testharnessdir': self.testharnessdir,
             'profileName': self.profileName,
             'singleFile': self.singleFile,
             'env': self.env, # making a copy of this in the testthreads
@@ -1185,16 +1199,23 @@ class XPCShellTests(object):
         if self.debuggerInfo:
             # Force a sequential run
             self.sequential = True
 
             # If we have an interactive debugger, disable SIGINT entirely.
             if self.debuggerInfo.interactive:
                 signal.signal(signal.SIGINT, lambda signum, frame: None)
 
+        if self.jsDebuggerInfo:
+            # The js debugger magic needs more work to do the right thing
+            # if debugging multiple files.
+            if len(self.alltests) != 1:
+                self.log.error("Error: --jsdebugger can only be used with a single test!")
+                return False
+
         # create a queue of all tests that will run
         tests_queue = deque()
         # also a list for the tests that need to be run sequentially
         sequential_tests = []
         for test_object in self.alltests:
             # Test identifiers are provided for the convenience of logging. These
             # start as path names but are rewritten in case tests from the same path
             # are re-run.
@@ -1429,16 +1450,23 @@ class XPCShellOptions(OptionParser):
         self.add_option("--debugger-args",
                         action = "store", dest = "debuggerArgs",
                         help = "pass the given args to the debugger _before_ "
                            "the application on the command line")
         self.add_option("--debugger-interactive",
                         action = "store_true", dest = "debuggerInteractive",
                         help = "prevents the test harness from redirecting "
                           "stdout and stderr for interactive debuggers")
+        self.add_option("--jsdebugger", dest="jsDebugger", action="store_true",
+                        help="Waits for a devtools JS debugger to connect before "
+                             "starting the test.")
+        self.add_option("--jsdebugger-port", type="int", dest="jsDebuggerPort",
+                        default=6000,
+                        help="The port to listen on for a debugger connection if "
+                             "--jsdebugger is specified.")
 
 def main():
     parser = XPCShellOptions()
     structured.commandline.add_logging_group(parser)
     options, args = parser.parse_args()
 
 
     log = structured.commandline.setup_logging("XPCShell",
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -597,16 +597,19 @@ function ThreadActor(aParent, aGlobal)
   this.global = aGlobal;
 
   this._allEventsListener = this._allEventsListener.bind(this);
   this.onNewGlobal = this.onNewGlobal.bind(this);
   this.onNewSource = this.onNewSource.bind(this);
   this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
   this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
   this.onNewScript = this.onNewScript.bind(this);
+  // Set a wrappedJSObject property so |this| can be sent via the observer svc
+  // for the xpcshell harness.
+  this.wrappedJSObject = this;
 }
 
 ThreadActor.prototype = {
   // Used by the ObjectActor to keep track of the depth of grip() calls.
   _gripDepth: null,
 
   actorPrefix: "context",
 
@@ -1173,16 +1176,21 @@ ThreadActor.prototype = {
         this._options.pauseOnExceptions = aRequest.pauseOnExceptions;
         this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions;
         this.maybePauseOnExceptions();
         this._maybeListenToEvents(aRequest);
       }
 
       let packet = this._resumed();
       this._popThreadPause();
+      // Tell anyone who cares of the resume (as of now, that's the xpcshell
+      // harness)
+      if (Services.obs) {
+        Services.obs.notifyObservers(this, "devtools-thread-resumed", null);
+      }
       return packet;
     }, error => {
       return error instanceof Error
         ? { error: "unknownError",
             message: DevToolsUtils.safeErrorString(error) }
         // It is a known error, and the promise was rejected with an error
         // packet.
         : error;
@@ -1317,17 +1325,17 @@ ThreadActor.prototype = {
    */
   _breakOnEnter: function(script) {
     let offsets = script.getAllOffsets();
     let sourceActor = this.sources.source({ source: script.source });
 
     for (let line = 0, n = offsets.length; line < n; line++) {
       if (offsets[line]) {
         let location = { line: line };
-        let resp = sourceActor._createAndStoreBreakpoint(location);
+        let resp = sourceActor.createAndStoreBreakpoint(location);
         dbg_assert(!resp.actualLocation, "No actualLocation should be returned");
         if (resp.error) {
           reportError(new Error("Unable to set breakpoint on event listener"));
           return;
         }
         let bp = this.breakpointStore.getBreakpoint({
           source: sourceActor.form(),
           line: location.line
@@ -2511,21 +2519,20 @@ SourceActor.prototype = {
       }
       else {
         // XXX bug 865252: Don't load from the cache if this is a source mapped
         // source because we can't guarantee that the cache has the most up to date
         // content for this source like we can if it isn't source mapped.
         let sourceFetched = fetch(this.url, { loadFromCache: !this.source });
 
         // Record the contentType we just learned during fetching
-        sourceFetched.then(({ contentType }) => {
-          this._contentType = contentType;
+        return sourceFetched.then(result => {
+          this._contentType = result.contentType;
+          return result;
         });
-
-        return sourceFetched;
       }
     });
   },
 
   /**
    * Get all executable lines from the current source
    * @return Array - Executable lines of the current script
    **/
@@ -2843,17 +2850,17 @@ SourceActor.prototype = {
       else {
         return this._createBreakpoint(genLoc, originalLoc, aRequest.condition);
       }
     });
   },
 
   _createBreakpoint: function(loc, originalLoc, condition) {
     return resolve(null).then(() => {
-      return this._createAndStoreBreakpoint({
+      return this.createAndStoreBreakpoint({
         line: loc.line,
         column: loc.column,
         condition: condition
       });
     }).then(response => {
       var actual = response.actualLocation;
       if (actual) {
         if (this.source) {
@@ -2910,22 +2917,24 @@ SourceActor.prototype = {
       DevToolsUtils.reportException("onSetBreakpoint", error);
     });
   },
 
   /**
    * Create a breakpoint at the specified location and store it in the
    * cache. Takes ownership of `aRequest`. This is the
    * generated location if this source is sourcemapped.
+   * Used by the XPCShell test harness to set breakpoints in a script before
+   * it has loaded.
    *
    * @param Object aRequest
    *        An object of the form { line[, column, condition] }. The
    *        location is in the generated source, if sourcemapped.
    */
-  _createAndStoreBreakpoint: function (aRequest) {
+  createAndStoreBreakpoint: function (aRequest) {
     let bp = update({}, aRequest, { source: this.form() });
     this.breakpointStore.addBreakpoint(bp);
     return this._setBreakpoint(aRequest);
   },
 
   /** Get or create the BreakpointActor for the breakpoint at the given location.
    *
    * NB: This will override a pre-existing BreakpointActor's condition with
--- a/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js
+++ b/toolkit/devtools/server/actors/utils/map-uri-to-addon-id.js
@@ -8,20 +8,29 @@
 
 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 const Services = require("Services");
 const { Cc, Ci } = require("chrome");
 
 Object.defineProperty(this, "addonManager", {
   get: (function () {
     let cached;
-    return () => cached
-      ? cached
-      : (cached = Cc["@mozilla.org/addons/integration;1"]
-                    .getService(Ci.amIAddonManager))
+    return () => {
+      if (cached === undefined) {
+        // catch errors as the addonManager might not exist in this environment
+        // (eg, xpcshell)
+        try {
+          cached = Cc["@mozilla.org/addons/integration;1"]
+                      .getService(Ci.amIAddonManager);
+        } catch (ex) {
+          cached = null;
+        }
+      }
+      return cached;
+    }
   }())
 });
 
 const B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
 
 /**
  * This is a wrapper around amIAddonManager.mapURIToAddonID which always returns
  * false on B2G to avoid loading the add-on manager there and reports any