Bug 1119070 - Add migration=sync11 query parameter to post-migration /account/login. r=rnewman, a=lmandel
authorNick Alexander <nalexander@mozilla.com>
Mon, 12 Jan 2015 15:01:22 -0800
changeset 240370 cd408977f6ca2e14fc935bb05dc5dc837f9102ba
parent 240369 2201483639877b5b02a19dfa86438c255431f7d2
child 240371 7b01079e4265f04cc5ad611fafea3cb032059741
push id7528
push userryanvm@gmail.com
push dateWed, 28 Jan 2015 15:23:46 +0000
treeherdermozilla-aurora@e3367427f25b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrnewman, lmandel
bugs1119070
milestone37.0a2
Bug 1119070 - Add migration=sync11 query parameter to post-migration /account/login. r=rnewman, a=lmandel ======== https://github.com/mozilla-services/android-sync/commit/fb5159ea46d59164e21b0a21dbb953c4386dfcb4 Author: Nick Alexander <nalexander@mozilla.com> Bug 1119070 - Part 3: Re-instate service=sync; add migration=sync11. ======== https://github.com/mozilla-services/android-sync/commit/00ab8e45e0261799ee35fad1a7a3486ecceecb5c Author: Nick Alexander <nalexander@mozilla.com> Date: Mon Jan 12 14:23:32 2015 -0800 Bug 1119070 - Part 2: Thread query parameters through to relevant API calls. ======== https://github.com/mozilla-services/android-sync/commit/b4029585aac5cc07f17027b804295258f8390c78 Author: Nick Alexander <nalexander@mozilla.com> Date: Mon Jan 12 12:46:22 2015 -0800 Bug 1119070 - Part 1: Extract uniform getBaseResource.
mobile/android/base/background/fxa/FxAccountClient.java
mobile/android/base/background/fxa/FxAccountClient10.java
mobile/android/base/background/fxa/FxAccountClient20.java
mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
mobile/android/base/fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java
mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
mobile/android/base/fxa/activities/FxAccountFinishMigratingActivity.java
mobile/android/base/fxa/activities/FxAccountSignInActivity.java
mobile/android/base/fxa/tasks/FxAccountCodeResender.java
mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java
mobile/android/base/fxa/tasks/FxAccountSetupTask.java
mobile/android/base/fxa/tasks/FxAccountSignInTask.java
mobile/android/base/fxa/tasks/FxAccountUnlockCodeResender.java
--- a/mobile/android/base/background/fxa/FxAccountClient.java
+++ b/mobile/android/base/background/fxa/FxAccountClient.java
@@ -1,21 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.background.fxa;
 
+import java.util.Map;
+
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
 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 createAccountAndGetKeys(final byte[] emailUTF8, final PasswordStretcher passwordStretcher, final Map<String, String> queryParameters, final RequestDelegate<LoginResponse> delegate);
+  public void loginAndGetKeys(final byte[] emailUTF8, final PasswordStretcher passwordStretcher, final Map<String, String> queryParameters, 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
@@ -3,21 +3,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.background.fxa;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.net.URLEncoder;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.Arrays;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.concurrent.Executor;
 
 import javax.crypto.Mac;
 
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientMalformedResponseException;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.fxa.FxAccountConstants;
@@ -66,17 +69,16 @@ public class FxAccountClient10 {
   public static final String JSON_KEY_UID = "uid";
   public static final String JSON_KEY_VERIFIED = "verified";
   public static final String JSON_KEY_ERROR = "error";
   public static final String JSON_KEY_MESSAGE = "message";
   public static final String JSON_KEY_INFO = "info";
   public static final String JSON_KEY_CODE = "code";
   public static final String JSON_KEY_ERRNO = "errno";
 
-
   protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE, JSON_KEY_INFO };
   protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
 
   /**
    * The server's URI.
    * <p>
    * We assume throughout that this ends with a trailing slash (and guarantee as
    * much in the constructor).
@@ -94,16 +96,58 @@ public class FxAccountClient10 {
     }
     this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
     if (!this.serverURI.endsWith("/")) {
       throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
     }
     this.executor = executor;
   }
 
+  protected BaseResource getBaseResource(String path, Map<String, String> queryParameters) throws UnsupportedEncodingException, URISyntaxException {
+    if (queryParameters == null || queryParameters.isEmpty()) {
+      return getBaseResource(path);
+    }
+    final String[] array = new String[2 * queryParameters.size()];
+    int i = 0;
+    for (Entry<String, String> entry : queryParameters.entrySet()) {
+      array[i++] = entry.getKey();
+      array[i++] = entry.getValue();
+    }
+    return getBaseResource(path, array);
+  }
+
+  /**
+   * Create <code>BaseResource</code>, encoding query parameters carefully.
+   * <p>
+   * This is equivalent to <code>android.net.Uri.Builder</code>, which is not
+   * present in our JUnit 4 tests.
+   *
+   * @param path fragment.
+   * @param queryParameters list of key/value query parameter pairs.  Must be even length!
+   * @return <code>BaseResource<instance>
+   * @throws URISyntaxException
+   * @throws UnsupportedEncodingException
+   */
+  protected BaseResource getBaseResource(String path, String... queryParameters) throws URISyntaxException, UnsupportedEncodingException {
+    final StringBuilder sb = new StringBuilder(serverURI);
+    sb.append(path);
+    if (queryParameters != null) {
+      int i = 0;
+      while (i < queryParameters.length) {
+        sb.append(i > 0 ? "&" : "?");
+        final String key = queryParameters[i++];
+        final String val = queryParameters[i++];
+        sb.append(URLEncoder.encode(key, "UTF-8"));
+        sb.append("=");
+        sb.append(URLEncoder.encode(val, "UTF-8"));
+      }
+    }
+    return new BaseResource(new URI(sb.toString()));
+  }
+
   /**
    * Process a typed value extracted from a successful response (in an
    * endpoint-dependent way).
    */
   public interface RequestDelegate<T> {
     public void handleError(Exception e);
     public void handleFailure(FxAccountClientRemoteException e);
     public void handleSuccess(T result);
@@ -348,18 +392,18 @@ public class FxAccountClient10 {
       body = createDelegate.getCreateBody();
     } catch (FxAccountClientException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "account/create"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("account/create");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<String>(resource, delegate) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         String uid = body.getString("uid");
@@ -379,18 +423,18 @@ public class FxAccountClient10 {
       body = authDelegate.getAuthStartBody();
     } catch (FxAccountClientException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "auth/start"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("auth/start");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<AuthDelegate>(resource, delegate) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
@@ -411,18 +455,18 @@ public class FxAccountClient10 {
       body = authDelegate.getAuthFinishBody();
     } catch (FxAccountClientException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "auth/finish"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("auth/finish");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<byte[]>(resource, delegate) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
@@ -475,18 +519,18 @@ public class FxAccountClient10 {
       HKDF.deriveMany(authToken, new byte[0], FxAccountUtils.KW("authToken"), tokenId, reqHMACKey, requestKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "session/create"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("session/create");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<TwoTokens>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
@@ -511,18 +555,18 @@ public class FxAccountClient10 {
       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "session/destroy"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("session/destroy");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         delegate.handleSuccess(null);
@@ -600,18 +644,18 @@ public class FxAccountClient10 {
       HKDF.deriveMany(keyFetchToken, new byte[0], FxAccountUtils.KW("keyFetchToken"), tokenId, reqHMACKey, requestKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "account/keys"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("account/keys");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<TwoKeys>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
@@ -662,18 +706,18 @@ public class FxAccountClient10 {
       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "recovery_email/status"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("recovery_email/status");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<StatusResponse>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
@@ -704,18 +748,18 @@ public class FxAccountClient10 {
       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "certificate/sign"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("certificate/sign");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<String>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         String cert = body.getString("cert");
@@ -745,18 +789,18 @@ public class FxAccountClient10 {
       HKDF.deriveMany(sessionToken, new byte[0], FxAccountUtils.KW("sessionToken"), tokenId, reqHMACKey, requestKey);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     BaseResource resource;
     try {
-      resource = new BaseResource(new URI(serverURI + "recovery_email/resend_code"));
-    } catch (URISyntaxException e) {
+      resource = getBaseResource("recovery_email/resend_code");
+    } catch (URISyntaxException | UnsupportedEncodingException e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<Void>(resource, delegate, tokenId, reqHMACKey) {
       @Override
       public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
         try {
@@ -783,17 +827,17 @@ public class FxAccountClient10 {
    * @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"));
+      resource = getBaseResource("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;
     }
--- a/mobile/android/base/background/fxa/FxAccountClient20.java
+++ b/mobile/android/base/background/fxa/FxAccountClient20.java
@@ -1,18 +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.background.fxa;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.concurrent.Executor;
 
 import org.json.simple.JSONObject;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.net.BaseResource;
@@ -48,22 +46,30 @@ public class FxAccountClient20 extends F
       this.verified = verified;
       this.sessionToken = sessionToken;
       this.keyFetchToken = keyFetchToken;
     }
   }
 
   // Public for testing only; prefer login and loginAndGetKeys (without boolean parameter).
   public void login(final byte[] emailUTF8, final byte[] quickStretchedPW, final boolean getKeys,
+      final Map<String, String> queryParameters,
       final RequestDelegate<LoginResponse> delegate) {
-    BaseResource resource;
-    JSONObject body;
-    final String path = getKeys ? "account/login?keys=true" : "account/login";
+    final BaseResource resource;
+    final JSONObject body;
     try {
-      resource = new BaseResource(new URI(serverURI + path));
+      final String path = "account/login";
+      final Map<String, String> modifiedParameters = new HashMap<>();
+      if (queryParameters != null) {
+        modifiedParameters.putAll(queryParameters);
+      }
+      if (getKeys) {
+        modifiedParameters.put("keys", "true");
+      }
+      resource = getBaseResource(path, modifiedParameters);
       body = new FxAccount20LoginDelegate(emailUTF8, quickStretchedPW).getCreateBody();
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate) {
       @Override
@@ -91,45 +97,33 @@ public class FxAccountClient20 extends F
           return;
         }
       }
     };
 
     post(resource, body, delegate);
   }
 
-  /**
-   * Create account/create URI, encoding query parameters carefully.
-   * <p>
-   * This is equivalent to <code>android.net.Uri.Builder</code>, which is not
-   * present in our JUnit 4 tests.
-   */
-  protected URI getCreateAccountURI(final boolean getKeys, final String service) throws UnsupportedEncodingException, URISyntaxException {
-    if (service == null) {
-      throw new IllegalArgumentException("service must not be null");
-    }
-    final StringBuilder sb = new StringBuilder(serverURI); // serverURI always has a trailing slash.
-    sb.append("account/create?service=");
-    // Be very careful that query parameters are encoded correctly!
-    sb.append(URLEncoder.encode(service, "UTF-8"));
-    if (getKeys) {
-      sb.append("&keys=true");
-    }
-    return new URI(sb.toString());
-  }
-
   public void createAccount(final byte[] emailUTF8, final byte[] quickStretchedPW,
       final boolean getKeys,
       final boolean preVerified,
-      final String service,
+      final Map<String, String> queryParameters,
       final RequestDelegate<LoginResponse> delegate) {
     final BaseResource resource;
     final JSONObject body;
     try {
-      resource = new BaseResource(getCreateAccountURI(getKeys, service));
+      final String path = "account/create";
+      final Map<String, String> modifiedParameters = new HashMap<>();
+      if (queryParameters != null) {
+        modifiedParameters.putAll(queryParameters);
+      }
+      if (getKeys) {
+        modifiedParameters.put("keys", "true");
+      }
+      resource = getBaseResource(path, modifiedParameters);
       body = new FxAccount20CreateDelegate(emailUTF8, quickStretchedPW, preVerified).getCreateBody();
     } catch (Exception e) {
       invokeHandleError(delegate, e);
       return;
     }
 
     // This is very similar to login, except verified is not required.
     resource.delegate = new ResourceDelegate<LoginResponse>(resource, delegate) {
@@ -158,28 +152,28 @@ public class FxAccountClient20 extends F
         }
       }
     };
 
     post(resource, body, delegate);
   }
 
   @Override
-  public void createAccountAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, RequestDelegate<LoginResponse> delegate) {
+  public void createAccountAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, final Map<String, String> queryParameters, RequestDelegate<LoginResponse> delegate) {
     try {
       byte[] quickStretchedPW = passwordStretcher.getQuickStretchedPW(emailUTF8);
-      createAccount(emailUTF8, quickStretchedPW, true, false, "sync", delegate);
+      createAccount(emailUTF8, quickStretchedPW, true, false, queryParameters, delegate);
     } catch (Exception e) {
       invokeHandleError(delegate, e);
     }
   }
 
   @Override
-  public void loginAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, RequestDelegate<LoginResponse> delegate) {
-    login(emailUTF8, passwordStretcher, true, delegate);
+  public void loginAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, final Map<String, String> queryParameters, RequestDelegate<LoginResponse> delegate) {
+    login(emailUTF8, passwordStretcher, true, queryParameters, delegate);
   }
 
   /**
    * We want users to be able to enter their email address case-insensitively.
    * We stretch the password locally using the email address as a salt, to make
    * dictionary attacks more expensive. This means that a client with a
    * case-differing email address is unable to produce the correct
    * authorization, even though it knows the password. In this case, the server
@@ -193,31 +187,33 @@ public class FxAccountClient20 extends F
    *
    * @param emailUTF8
    *          user entered email address.
    * @param stretcher
    *          delegate to stretch and re-stretch password.
    * @param getKeys
    *          true if a <code>keyFetchToken</code> should be returned (in
    *          addition to the standard <code>sessionToken</code>).
+   * @param queryParameters
    * @param delegate
    *          to invoke callbacks.
    */
   public void login(final byte[] emailUTF8, final PasswordStretcher stretcher, final boolean getKeys,
+      final Map<String, String> queryParameters,
       final RequestDelegate<LoginResponse> delegate) {
     byte[] quickStretchedPW;
     try {
       FxAccountUtils.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
       quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8);
     } catch (Exception e) {
       delegate.handleError(e);
       return;
     }
 
-    this.login(emailUTF8, quickStretchedPW, getKeys, new RequestDelegate<LoginResponse>() {
+    this.login(emailUTF8, quickStretchedPW, getKeys, queryParameters, new RequestDelegate<LoginResponse>() {
       @Override
       public void handleSuccess(LoginResponse result) {
         delegate.handleSuccess(result);
       }
 
       @Override
       public void handleError(Exception e) {
         delegate.handleError(e);
@@ -234,17 +230,17 @@ public class FxAccountClient20 extends F
         Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email.");
         FxAccountUtils.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
 
         try {
           // Nota bene: this is not recursive, since we call the fixed password
           // signature here, which invokes a non-retrying version.
           byte[] alternateEmailUTF8 = alternateEmail.getBytes("UTF-8");
           byte[] alternateQuickStretchedPW = stretcher.getQuickStretchedPW(alternateEmailUTF8);
-          login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, delegate);
+          login(alternateEmailUTF8, alternateQuickStretchedPW, getKeys, queryParameters, delegate);
         } catch (Exception innerException) {
           delegate.handleError(innerException);
           return;
         }
       }
     });
   }
 }
--- a/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractSetupActivity.java
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.activities;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
 import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
@@ -560,9 +561,15 @@ abstract public class FxAccountAbstractS
 
     setCustomServerViewVisibility(View.VISIBLE);
   }
 
   protected void setCustomServerViewVisibility(int visibility) {
     ensureFindViewById(null, R.id.account_server_layout, "account server layout").setVisibility(visibility);
     ensureFindViewById(null, R.id.sync_server_layout, "sync server layout").setVisibility(visibility);
   }
+
+  protected Map<String, String> getQueryParameters() {
+    final Map<String, String> queryParameters = new HashMap<>();
+    queryParameters.put("service", "sync");
+    return queryParameters;
+  }
 }
--- a/mobile/android/base/fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountAbstractUpdateCredentialsActivity.java
@@ -156,17 +156,17 @@ public abstract class FxAccountAbstractU
   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();
+      new FxAccountSignInTask(this, this, email, passwordStretcher, client, getQueryParameters(), 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() {
--- a/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountCreateAccountActivity.java
@@ -216,17 +216,17 @@ public class FxAccountCreateAccountActiv
         showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
       }
     };
 
     Executor executor = Executors.newSingleThreadExecutor();
     FxAccountClient client = new FxAccountClient20(serverURI, executor);
     try {
       hideRemoteError();
-      new FxAccountCreateAccountTask(this, this, email, passwordStretcher, client, delegate).execute();
+      new FxAccountCreateAccountTask(this, this, email, passwordStretcher, client, getQueryParameters(), delegate).execute();
     } catch (Exception e) {
       showRemoteError(e, R.string.fxaccount_create_account_unknown_error);
     }
   }
 
   @Override
   protected boolean shouldButtonBeEnabled() {
     return super.shouldButtonBeEnabled() &&
--- a/mobile/android/base/fxa/activities/FxAccountFinishMigratingActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountFinishMigratingActivity.java
@@ -1,14 +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.util.Map;
+
 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;
 
@@ -46,9 +48,16 @@ public class FxAccountFinishMigratingAct
   @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;
   }
+
+  @Override
+  protected Map<String, String> getQueryParameters() {
+    final Map<String, String> queryParameters = super.getQueryParameters();
+    queryParameters.put("migration", "sync11");
+    return queryParameters;
+  }
 }
--- a/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
+++ b/mobile/android/base/fxa/activities/FxAccountSignInActivity.java
@@ -106,17 +106,17 @@ public class FxAccountSignInActivity ext
         showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
       }
     };
 
     Executor executor = Executors.newSingleThreadExecutor();
     FxAccountClient client = new FxAccountClient20(serverURI, executor);
     try {
       hideRemoteError();
-      new FxAccountSignInTask(this, this, email, passwordStretcher, client, delegate).execute();
+      new FxAccountSignInTask(this, this, email, passwordStretcher, client, getQueryParameters(), delegate).execute();
     } catch (Exception e) {
       showRemoteError(e, R.string.fxaccount_sign_in_unknown_error);
     }
   }
 
   protected void createSignInButton() {
     button.setOnClickListener(new OnClickListener() {
       @Override
--- a/mobile/android/base/fxa/tasks/FxAccountCodeResender.java
+++ b/mobile/android/base/fxa/tasks/FxAccountCodeResender.java
@@ -12,32 +12,33 @@ import org.mozilla.gecko.background.comm
 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 org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.fxa.login.Engaged;
 
 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 verification email to be resent.
  */
 public class FxAccountCodeResender {
   private static final String LOG_TAG = FxAccountCodeResender.class.getSimpleName();
 
   private static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
     protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
 
     protected final byte[] sessionToken;
 
     public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
-      super(context, null, client, delegate);
+      super(context, null, client, null, delegate);
       this.sessionToken = sessionToken;
     }
 
     @Override
     protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
       try {
         client.resendCode(sessionToken, innerDelegate);
         latch.await();
--- a/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java
+++ b/mobile/android/base/fxa/tasks/FxAccountCreateAccountTask.java
@@ -1,40 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.tasks;
 
 import java.io.UnsupportedEncodingException;
+import java.util.Map;
 
 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.LoginResponse;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
 
 import android.content.Context;
 
 public class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
   private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
 
   protected final byte[] emailUTF8;
   protected final PasswordStretcher passwordStretcher;
 
-  public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
-    super(context, progressDisplay, client, delegate);
+  public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, Map<String, String> queryParameters, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+    super(context, progressDisplay, client, queryParameters, delegate);
     this.emailUTF8 = email.getBytes("UTF-8");
     this.passwordStretcher = passwordStretcher;
   }
 
   @Override
   protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
     try {
-      client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
+      client.createAccountAndGetKeys(emailUTF8, passwordStretcher, queryParameters, innerDelegate);
       latch.await();
       return innerDelegate;
     } catch (Exception e) {
       Logger.error(LOG_TAG, "Got exception logging in.", e);
       delegate.handleError(e);
     }
     return null;
   }
--- a/mobile/android/base/fxa/tasks/FxAccountSetupTask.java
+++ b/mobile/android/base/fxa/tasks/FxAccountSetupTask.java
@@ -1,14 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.tasks;
 
+import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 
 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.FxAccountClientException.FxAccountClientRemoteException;
 import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.InnerRequestDelegate;
 
@@ -38,23 +39,26 @@ public abstract class FxAccountSetupTask
 
   // Initialized lazily.
   protected byte[] quickStretchedPW;
 
   // AsyncTask's are one-time-use, so final members are fine.
   protected final CountDownLatch latch = new CountDownLatch(1);
   protected final InnerRequestDelegate<T> innerDelegate = new InnerRequestDelegate<T>(latch);
 
+  protected final Map<String, String> queryParameters;
+
   protected final RequestDelegate<T> delegate;
 
-  public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, RequestDelegate<T> delegate) {
+  public FxAccountSetupTask(Context context, ProgressDisplay progressDisplay, FxAccountClient client, Map<String, String> queryParameters, RequestDelegate<T> delegate) {
     this.context = context;
     this.client = client;
     this.delegate = delegate;
     this.progressDisplay = progressDisplay;
+    this.queryParameters = queryParameters;
   }
 
   @Override
   protected void onPreExecute() {
     if (progressDisplay != null) {
       progressDisplay.showProgress();
     }
   }
--- a/mobile/android/base/fxa/tasks/FxAccountSignInTask.java
+++ b/mobile/android/base/fxa/tasks/FxAccountSignInTask.java
@@ -1,40 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.fxa.tasks;
 
 import java.io.UnsupportedEncodingException;
+import java.util.Map;
 
 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.LoginResponse;
 import org.mozilla.gecko.background.fxa.PasswordStretcher;
 
 import android.content.Context;
 
 public class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
   protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
 
   protected final byte[] emailUTF8;
   protected final PasswordStretcher passwordStretcher;
 
-  public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
-    super(context, progressDisplay, client, delegate);
+  public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, Map<String, String> queryParameters, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
+    super(context, progressDisplay, client, queryParameters, delegate);
     this.emailUTF8 = email.getBytes("UTF-8");
     this.passwordStretcher = passwordStretcher;
   }
 
   @Override
   protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
     try {
-      client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
+      client.loginAndGetKeys(emailUTF8, passwordStretcher, queryParameters, innerDelegate);
       latch.await();
       return innerDelegate;
     } catch (Exception e) {
       Logger.error(LOG_TAG, "Got exception signing in.", e);
       delegate.handleError(e);
     }
     return null;
   }
--- a/mobile/android/base/fxa/tasks/FxAccountUnlockCodeResender.java
+++ b/mobile/android/base/fxa/tasks/FxAccountUnlockCodeResender.java
@@ -26,17 +26,17 @@ 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);
+      super(context, null, client, null, delegate);
       this.emailUTF8 = emailUTF8;
     }
 
     @Override
     protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
       try {
         client.resendUnlockCode(emailUTF8, innerDelegate);
         latch.await();