Bug 1408555 p2 - Remove bundling from AndroidFxAccount. r?Grisha draft
authorEdouard Oger <eoger@fastmail.com>
Fri, 13 Oct 2017 17:10:07 -0400
changeset 682839 eb55fb24f0616932c8db82a8ec41eaa724ede089
parent 682838 ee46c9205d45e4ab5358a208b2892112f822a6e2
child 736457 5c3dcaef548af88166026116c19493b9e0df7462
push id85174
push userbmo:eoger@fastmail.com
push dateWed, 18 Oct 2017 20:49:21 +0000
reviewersGrisha
bugs1408555
milestone58.0a1
Bug 1408555 p2 - Remove bundling from AndroidFxAccount. r?Grisha MozReview-Commit-ID: B56P2dPIDBo
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
@@ -70,18 +70,21 @@ public class AccountPickler {
   public static final String KEY_EMAIL = "email";
   public static final String KEY_PROFILE = "profile";
   public static final String KEY_IDP_SERVER_URI = "idpServerURI";
   public static final String KEY_TOKEN_SERVER_URI = "tokenServerURI";
   public static final String KEY_PROFILE_SERVER_URI = "profileServerURI";
 
   public static final String KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP = "authoritiesToSyncAutomaticallyMap";
 
-  // Deprecated, but maintained for migration purposes.
+  // All deprecated, but maintained for migration purposes.
   public static final String KEY_IS_SYNCING_ENABLED = "isSyncingEnabled";
+  public static final String KEY_STATE_LABEL = "stateLabel";
+  public static final String KEY_STATE = "state";
+  public static final String KEY_PROFILE_JSON = "profileJSON";
 
   public static final String KEY_BUNDLE = "bundle";
 
   /**
    * Remove Firefox account persisted to disk.
    * This operation is synchronized to avoid race condition while deleting the account.
    *
    * @param context Android context.
@@ -99,43 +102,39 @@ public class AccountPickler {
 
     o.put(KEY_ACCOUNT_VERSION, AndroidFxAccount.CURRENT_ACCOUNT_VERSION);
     o.put(KEY_ACCOUNT_TYPE, FxAccountConstants.ACCOUNT_TYPE);
     o.put(KEY_EMAIL, account.getEmail());
     o.put(KEY_PROFILE, account.getProfile());
     o.put(KEY_IDP_SERVER_URI, account.getAccountServerURI());
     o.put(KEY_TOKEN_SERVER_URI, account.getTokenServerURI());
     o.put(KEY_PROFILE_SERVER_URI, account.getProfileServerURI());
+    final ExtendedJSONObject profileJSON = account.getProfileJSON();
+    if (profileJSON != null) {
+      o.put(KEY_PROFILE_JSON, profileJSON.toJSONString());
+    }
+    final State state = account.getState();
+    o.put(KEY_STATE_LABEL, state.getStateLabel().name());
+    o.put(KEY_STATE, state.toJSONObject().toJSONString());
 
     final ExtendedJSONObject p = new ExtendedJSONObject();
     for (Entry<String, Boolean> pair : account.getAuthoritiesToSyncAutomaticallyMap().entrySet()) {
       p.put(pair.getKey(), pair.getValue());
     }
     o.put(KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP, p);
 
     // TODO: If prefs version changes under us, SyncPrefsPath will change, "clearing" prefs.
 
-    final ExtendedJSONObject bundle = account.unbundle();
-    if (bundle == null) {
-      Logger.warn(LOG_TAG, "Unable to obtain account bundle; aborting.");
-      return null;
-    }
-    o.put(KEY_BUNDLE, bundle);
-
     return o;
   }
 
   /**
    * Persist Firefox account to disk as a JSON object.
    * This operation is synchronized to avoid race condition while deleting the account.
    *
-   * Note that pickling is different from bundling, which involves operations on a
-   * {@link android.os.Bundle Bundle} object of miscellaneous data associated with the account.
-   * See {@link AndroidFxAccount#persistBundle} and {@link AndroidFxAccount#unbundle} for more.
-   *
    * @param account the AndroidFxAccount to persist to disk
    * @param filename name of file to persist to; must not contain path separators.
    * @deprecated Pickling/Un-pickling logic will be removed entirely in the future. This method is
    *             still present to make sure un-pickling tests pass.
    */
   public synchronized static void pickle(final AndroidFxAccount account, final String filename) {
     final ExtendedJSONObject o = toJSON(account, System.currentTimeMillis());
     writeToDisk(account.context, filename, o);
@@ -196,17 +195,17 @@ public class AccountPickler {
     }
 
     final AndroidFxAccount account;
     try {
       account = AndroidFxAccount.addAndroidAccount(context, params.state.uid, params.email, params.profile,
           params.authServerURI, params.tokenServerURI, params.profileServerURI, params.state,
           params.authoritiesToSyncAutomaticallyMap,
           params.accountVersion,
-          true, params.bundle);
+          true, params.profileJSON);
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Exception when adding Android Account; aborting.", e);
       return null;
     }
 
     if (account == null) {
       Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
       return null;
@@ -224,18 +223,18 @@ public class AccountPickler {
     private int accountVersion;
     private String email;
     private String profile;
     private String authServerURI;
     private String tokenServerURI;
     private String profileServerURI;
     private final Map<String, Boolean> authoritiesToSyncAutomaticallyMap = new HashMap<>();
 
-    private ExtendedJSONObject bundle;
     private State state;
+    private String profileJSON;
 
     private UnpickleParams() {
     }
 
     private static UnpickleParams fromJSON(final ExtendedJSONObject json)
         throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
       final UnpickleParams params = new UnpickleParams();
       params.pickleVersion = json.getLong(KEY_PICKLE_VERSION);
@@ -309,21 +308,29 @@ public class AccountPickler {
         this.profileServerURI = FxAccountConstants.DEFAULT_AUTH_SERVER_ENDPOINT.equals(this.authServerURI)
             ? FxAccountConstants.DEFAULT_PROFILE_SERVER_ENDPOINT
             : FxAccountConstants.STAGE_PROFILE_SERVER_ENDPOINT;
       }
 
       // We get the default value for everything except syncing browser data.
       this.authoritiesToSyncAutomaticallyMap.put(BrowserContract.AUTHORITY, json.getBoolean(KEY_IS_SYNCING_ENABLED));
 
-      this.bundle = json.getObject(KEY_BUNDLE);
-      if (bundle == null) {
-        throw new IllegalStateException("Pickle bundle is null.");
+      final ExtendedJSONObject bundle = json.getObject(KEY_BUNDLE);
+      if (bundle != null) {
+        this.state = getState(bundle);
+        this.profileJSON = bundle.getString(AndroidFxAccount.BUNDLE_KEY_PROFILE_JSON);
+        return;
       }
-      this.state = getState(bundle);
+      final String stateLabelString = json.getString(KEY_STATE_LABEL);
+      final String stateString = json.getString(KEY_STATE);
+      if (stateLabelString == null || stateString == null) {
+        throw new IllegalStateException("Unable to get pickled state.");
+      }
+      this.state = getState(stateLabelString, stateString);
+      this.profileJSON = json.getString(KEY_PROFILE_JSON);
     }
 
     private void unpickleV3(final ExtendedJSONObject json)
         throws NonObjectJSONException, NoSuchAlgorithmException, InvalidKeySpecException {
       // We'll overwrite the extracted sync automatically map.
       unpickleV1(json);
 
       // Extract the map of authorities to sync automatically.
@@ -339,27 +346,31 @@ public class AccountPickler {
         }
       }
     }
 
     private State getState(final ExtendedJSONObject bundle) throws InvalidKeySpecException,
             NonObjectJSONException, NoSuchAlgorithmException {
       // TODO: Should copy-pasta BUNDLE_KEY_STATE & LABEL to this file to ensure we maintain
       // old versions?
-      final StateLabel stateLabelString = StateLabel.valueOf(
-          bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL));
+      final String stateLabelString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE_LABEL);
       final String stateString = bundle.getString(AndroidFxAccount.BUNDLE_KEY_STATE);
-      if (stateLabelString == null || stateString == null) {
+      return getState(stateLabelString, stateString);
+    }
+
+    private State getState(String stateLabelString, String stateString) {
+      final StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
+      if (stateLabel == null || stateString == null) {
         throw new IllegalStateException("stateLabel and stateString must not be null, but: " +
-            "(stateLabel == null) = " + (stateLabelString == null) +
-            " and (stateString == null) = " + (stateString == null));
+                "(stateLabel == null) = " + (stateLabel == null) +
+                " and (stateString == null) = " + (stateString == null));
       }
 
       try {
-        return StateFactory.fromJSONObject(stateLabelString, new ExtendedJSONObject(stateString));
+        return StateFactory.fromJSONObject(stateLabel, new ExtendedJSONObject(stateString));
       } catch (Exception e) {
         throw new IllegalStateException("could not get state", e);
       }
     }
 
     @Nullable
     /* package-private */ String getUID() {
       return this.state.uid;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AndroidFxAccount.java
@@ -91,21 +91,24 @@ public class AndroidFxAccount {
   // It's important that when renaming is complete - or fails - that this flag is reset, otherwise
   // normal account deletion will not function correctly.
   /* package-private */ static final String ACCOUNT_KEY_RENAME_IN_PROGRESS = "accountBeingRenamed";
   /* package-private */ static final String ACCOUNT_VALUE_RENAME_IN_PROGRESS = "true";
 
   private static final String ACCOUNT_KEY_TOKEN_SERVER = "tokenServerURI";       // Sync-specific.
   private static final String ACCOUNT_KEY_DESCRIPTOR = "descriptor";
 
-  private static final int CURRENT_BUNDLE_VERSION = 2;
-  private static final String BUNDLE_KEY_BUNDLE_VERSION = "version";
-  /* package-private */static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
+  // Deprecated, but maintained for migration purposes.
+  /* package-private */ static final String BUNDLE_KEY_STATE_LABEL = "stateLabel";
   /* package-private */ static final String BUNDLE_KEY_STATE = "state";
-  private static final String BUNDLE_KEY_PROFILE_JSON = "profile";
+  /* package-private */ static final String BUNDLE_KEY_PROFILE_JSON = "profile";
+
+  private static final String ACCOUNT_KEY_STATE_LABEL = "stateLabel";
+  private static final String ACCOUNT_KEY_STATE = "state";
+  private static final String ACCOUNT_KEY_PROFILE_JSON = "profile";
 
   private static final String ACCOUNT_KEY_DEVICE_ID = "deviceId";
   private static final String ACCOUNT_KEY_DEVICE_REGISTRATION_VERSION = "deviceRegistrationVersion";
   private static final String ACCOUNT_KEY_DEVICE_REGISTRATION_TIMESTAMP = "deviceRegistrationTimestamp";
   private static final String ACCOUNT_KEY_DEVICE_PUSH_REGISTRATION_ERROR = "devicePushRegistrationError";
   private static final String ACCOUNT_KEY_DEVICE_PUSH_REGISTRATION_ERROR_TIME = "devicePushRegistrationErrorTime";
 
   // Account authentication token type for fetching account profile.
@@ -135,17 +138,18 @@ public class AndroidFxAccount {
   private static final List<String> ACCOUNT_KEY_TO_CARRY_OVER_ON_RENAME_SET;
   static {
     ArrayList<String> keysToCarryOver = new ArrayList<>(6);
     keysToCarryOver.add(ACCOUNT_KEY_ACCOUNT_VERSION);
     keysToCarryOver.add(ACCOUNT_KEY_IDP_SERVER);
     keysToCarryOver.add(ACCOUNT_KEY_TOKEN_SERVER);
     keysToCarryOver.add(ACCOUNT_KEY_PROFILE_SERVER);
     keysToCarryOver.add(ACCOUNT_KEY_PROFILE);
-    keysToCarryOver.add(ACCOUNT_KEY_DESCRIPTOR);
+    keysToCarryOver.add(ACCOUNT_KEY_STATE_LABEL);
+    keysToCarryOver.add(ACCOUNT_KEY_STATE);
     ACCOUNT_KEY_TO_CARRY_OVER_ON_RENAME_SET = Collections.unmodifiableList(keysToCarryOver);
   }
 
   private static final String PREF_KEY_LAST_SYNCED_TIMESTAMP = "lastSyncedTimestamp";
 
   protected final Context context;
   private final AccountManager accountManager;
   private final long neverSynced = -1;
@@ -169,16 +173,17 @@ public class AndroidFxAccount {
    *          to use as long-lived ambient Android context.
    * @param account
    *          Android account to use for storage.
    */
   public AndroidFxAccount(Context applicationContext, Account account) {
     this.context = applicationContext;
     this.account = account;
     this.accountManager = AccountManager.get(this.context);
+    maybeMigrateBundle();
   }
 
   public static AndroidFxAccount fromContext(Context context) {
     context = context.getApplicationContext();
     Account account = FirefoxAccounts.getFirefoxAccount(context);
     if (account == null) {
       return null;
     }
@@ -230,90 +235,16 @@ public class AndroidFxAccount {
 
     // Persist our UID into userData, so that we never have to do this dance again.
     accountManager.setUserData(account, ACCOUNT_KEY_UID, unpickledAccountUID);
 
     return unpickledAccountUID;
   }
 
   /**
-   * Saves the given data as the internal bundle associated with this account.
-   * @param bundle to write to account.
-   */
-  private synchronized void persistBundle(ExtendedJSONObject bundle) {
-    accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
-  }
-
-  /**
-   * Retrieve the internal bundle associated with this account.
-   * @return bundle associated with account.
-   */
-  /* package-private */ synchronized ExtendedJSONObject unbundle() {
-    final String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
-
-    final int version = getAccountVersion();
-    if (version < CURRENT_ACCOUNT_VERSION) {
-      // Needs upgrade. For now, do nothing. We'd like to just put your account
-      // into the Separated state here and have you update your credentials.
-      return null;
-    }
-
-    if (version > CURRENT_ACCOUNT_VERSION) {
-      // Oh dear.
-      throw new IllegalStateException("Invalid account bundle version. Current: " + CURRENT_ACCOUNT_VERSION + ", bundle version: " + version);
-    }
-
-    if (bundleString == null) {
-      return null;
-    }
-
-    return unbundleAccountV2(bundleString);
-  }
-
-  private String getBundleData(String key) {
-    ExtendedJSONObject o = unbundle();
-    if (o == null) {
-      return null;
-    }
-    return o.getString(key);
-  }
-
-  private void updateBundleValues(String key, String value, String... more) {
-    if (more.length % 2 != 0) {
-      throw new IllegalArgumentException("more must be a list of key, value pairs");
-    }
-    ExtendedJSONObject descriptor = unbundle();
-    if (descriptor == null) {
-      return;
-    }
-    descriptor.put(key, value);
-    for (int i = 0; i + 1 < more.length; i += 2) {
-      descriptor.put(more[i], more[i+1]);
-    }
-    persistBundle(descriptor);
-  }
-
-  private ExtendedJSONObject unbundleAccountV1(String bundle) {
-    ExtendedJSONObject o;
-    try {
-      o = new ExtendedJSONObject(bundle);
-    } catch (Exception e) {
-      return null;
-    }
-    if (CURRENT_BUNDLE_VERSION == o.getIntegerSafely(BUNDLE_KEY_BUNDLE_VERSION)) {
-      return o;
-    }
-    return null;
-  }
-
-  private ExtendedJSONObject unbundleAccountV2(String bundle) {
-    return unbundleAccountV1(bundle);
-  }
-
-  /**
    * Note that if the user clears data, an account will be left pointing to a
    * deleted profile. Such is life.
    */
   public String getProfile() {
     return accountManager.getUserData(account, ACCOUNT_KEY_PROFILE);
   }
 
   public String getAccountServerURI() {
@@ -384,16 +315,40 @@ public class AndroidFxAccount {
   }
 
   private String getReadingListPrefsPath(final String accountKey) throws GeneralSecurityException, UnsupportedEncodingException {
     final String product = GlobalConstants.BROWSER_INTENT_PACKAGE + ".reading";
     final long version = CURRENT_RL_PREFS_VERSION;
     return constructPrefsPath(accountKey, product, version, "");
   }
 
+  private void maybeMigrateBundle() {
+    final String bundleString = accountManager.getUserData(account, ACCOUNT_KEY_DESCRIPTOR);
+    if (TextUtils.isEmpty(bundleString)) {
+      return;
+    }
+    try {
+      final ExtendedJSONObject o = new ExtendedJSONObject(bundleString);
+
+      final String stateLabel = o.getString(BUNDLE_KEY_STATE_LABEL);
+      final String state = o.getString(BUNDLE_KEY_STATE);
+      final String profileJSON = o.getString(BUNDLE_KEY_PROFILE_JSON);
+
+      // Unwrap bundle values and save them individually.
+      accountManager.setUserData(account, ACCOUNT_KEY_STATE_LABEL, stateLabel);
+      accountManager.setUserData(account, ACCOUNT_KEY_STATE, state);
+      accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_JSON, profileJSON);
+    } catch (Exception e) {
+      Log.e(LOG_TAG, "Error in maybeMigrateBundle", e);
+    } finally {
+      // Delete the old bundle.
+      accountManager.setUserData(account, ACCOUNT_KEY_DESCRIPTOR, null);
+    }
+  }
+
   @SuppressWarnings("unchecked")
   private static void migrateSharedPreferencesValues(SharedPreferences sharedPreferences, Map<String, ?> values) {
     final SharedPreferences.Editor editor = sharedPreferences.edit();
     for (String key : values.keySet()) {
       final Object value = values.get(key);
       if (value instanceof String) {
         editor.putString(key, (String) value);
       } else if (value instanceof Integer) {
@@ -474,26 +429,30 @@ public class AndroidFxAccount {
    * <p>
    * <b>For debugging use only!</b> The contents of this JSON object completely
    * determine the user's Firefox Account status and yield access to whatever
    * user data the device has access to.
    *
    * @return JSON-object of Strings.
    */
   public ExtendedJSONObject toJSONObject() {
-    ExtendedJSONObject o = unbundle();
+    ExtendedJSONObject o = new ExtendedJSONObject();
     o.put("email", account.name);
     try {
       o.put("emailUTF8", Utils.byte2Hex(account.name.getBytes("UTF-8")));
     } catch (UnsupportedEncodingException e) {
       // Ignore.
     }
     o.put("fxaDeviceId", getDeviceId());
     o.put("fxaDeviceRegistrationVersion", getDeviceRegistrationVersion());
     o.put("fxaDeviceRegistrationTimestamp", getDeviceRegistrationTimestamp());
+    final State state = getState();
+    o.put("stateLabel", state.getStateLabel().name());
+    o.put("state", state.toJSONObject().toJSONString());
+    o.put("profile", getProfileJSON());
     return o;
   }
 
   public static AndroidFxAccount addAndroidAccount(
       @NonNull Context context,
       @NonNull String uid,
       @NonNull String email,
       @NonNull String profile,
@@ -515,17 +474,17 @@ public class AndroidFxAccount {
       @NonNull String profile,
       @NonNull String idpServerURI,
       @NonNull String tokenServerURI,
       @NonNull String profileServerURI,
       @NonNull State state,
       final Map<String, Boolean> authoritiesToSyncAutomaticallyMap,
       final int accountVersion,
       final boolean fromPickle,
-      ExtendedJSONObject bundle)
+      final String profileJSON /* remove this once pickling is removed */)
           throws UnsupportedEncodingException, GeneralSecurityException, URISyntaxException {
     if (uid == null) {
       throw new IllegalArgumentException("uid must not be null");
     }
     if (email == null) {
       throw new IllegalArgumentException("email must not be null");
     }
     if (profile == null) {
@@ -554,26 +513,18 @@ public class AndroidFxAccount {
     // bundle to be strings. *sigh*
     Bundle userdata = new Bundle();
     userdata.putString(ACCOUNT_KEY_ACCOUNT_VERSION, "" + CURRENT_ACCOUNT_VERSION);
     userdata.putString(ACCOUNT_KEY_IDP_SERVER, idpServerURI);
     userdata.putString(ACCOUNT_KEY_TOKEN_SERVER, tokenServerURI);
     userdata.putString(ACCOUNT_KEY_PROFILE_SERVER, profileServerURI);
     userdata.putString(ACCOUNT_KEY_PROFILE, profile);
     userdata.putString(ACCOUNT_KEY_UID, uid);
-
-    if (bundle == null) {
-      bundle = new ExtendedJSONObject();
-      // TODO: How to upgrade?
-      bundle.put(BUNDLE_KEY_BUNDLE_VERSION, CURRENT_BUNDLE_VERSION);
-    }
-    bundle.put(BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name());
-    bundle.put(BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
-
-    userdata.putString(ACCOUNT_KEY_DESCRIPTOR, bundle.toJSONString());
+    userdata.putString(ACCOUNT_KEY_STATE_LABEL, state.getStateLabel().name());
+    userdata.putString(ACCOUNT_KEY_STATE, state.toJSONObject().toJSONString());
 
     Account account = new Account(email, FxAccountConstants.ACCOUNT_TYPE);
     AccountManager accountManager = AccountManager.get(context);
     // We don't set an Android password, because we don't want to persist the
     // password (or anything else as powerful as the password). Instead, we
     // internally manage a sessionToken with a remotely owned lifecycle.
     boolean added = accountManager.addAccountExplicitly(account, null, userdata);
     if (!added) {
@@ -590,16 +541,20 @@ public class AndroidFxAccount {
     }
 
     final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
 
     if (!fromPickle) {
       fxAccount.clearSyncPrefs();
     }
 
+    if (profileJSON != null) {
+      accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_JSON, profileJSON);
+    }
+
     fxAccount.setAuthoritiesToSyncAutomaticallyMap(authoritiesToSyncAutomaticallyMap);
 
     return fxAccount;
   }
 
   private void clearSyncPrefs() throws UnsupportedEncodingException, GeneralSecurityException {
     getSyncPrefs().edit().clear().apply();
   }
@@ -667,19 +622,18 @@ public class AndroidFxAccount {
   }
 
   public synchronized void setState(State state) {
     if (state == null) {
       throw new IllegalArgumentException("state must not be null");
     }
     Logger.info(LOG_TAG, "Moving account named like " + getObfuscatedEmail() +
         " to state " + state.getStateLabel().toString());
-    updateBundleValues(
-        BUNDLE_KEY_STATE_LABEL, state.getStateLabel().name(),
-        BUNDLE_KEY_STATE, state.toJSONObject().toJSONString());
+    accountManager.setUserData(account, ACCOUNT_KEY_STATE_LABEL, state.getStateLabel().name());
+    accountManager.setUserData(account, ACCOUNT_KEY_STATE, state.toJSONObject().toJSONString());
     broadcastAccountStateChangedIntent();
   }
 
   private void broadcastAccountStateChangedIntent() {
     final Intent intent = new Intent(FxAccountConstants.ACCOUNT_STATE_CHANGED_ACTION);
     intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
     LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
   }
@@ -687,18 +641,19 @@ public class AndroidFxAccount {
   public synchronized State getState() {
     // Ensure we're working with the latest 'account' state. It might have changed underneath us.
     // Note that this "state refresh" is inefficient for some callers, since they might have
     // created this object (and thus fetched an account from the accounts system) just moments ago.
     // Other callers maintain this object for a while, and thus might be out-of-sync with the world.
     // See Bug 1407316 for higher-order improvements that will make this unnecessary.
     account = FirefoxAccounts.getFirefoxAccount(context);
 
-    String stateLabelString = getBundleData(BUNDLE_KEY_STATE_LABEL);
-    String stateString = getBundleData(BUNDLE_KEY_STATE);
+    final String stateLabelString = accountManager.getUserData(account, ACCOUNT_KEY_STATE_LABEL);
+    final String stateString = accountManager.getUserData(account, ACCOUNT_KEY_STATE);
+
     if (stateLabelString == null || stateString == null) {
       throw new IllegalStateException("stateLabelString and stateString must not be null, but: " +
           "(stateLabelString == null) = " + (stateLabelString == null) +
           " and (stateString == null) = " + (stateString == null));
     }
 
     try {
       StateLabel stateLabel = StateLabel.valueOf(stateLabelString);
@@ -865,17 +820,17 @@ public class AndroidFxAccount {
   }
 
   /**
    * Returns the current profile JSON if available, or null.
    *
    * @return profile JSON object.
    */
   public ExtendedJSONObject getProfileJSON() {
-    final String profileString = getBundleData(BUNDLE_KEY_PROFILE_JSON);
+    String profileString = accountManager.getUserData(account, ACCOUNT_KEY_PROFILE_JSON);
     if (profileString == null) {
       return null;
     }
 
     try {
       return new ExtendedJSONObject(profileString);
     } catch (Exception e) {
       Logger.error(LOG_TAG, "Failed to parse profile JSON; ignoring and returning null.", e);
@@ -1005,17 +960,17 @@ public class AndroidFxAccount {
         case Activity.RESULT_OK:
           Logger.info(LOG_TAG, "Profile JSON fetch succeeded!");
           final String resultData = bundle.getString(FxAccountProfileService.KEY_RESULT_STRING);
           FxAccountUtils.pii(LOG_TAG, "Profile JSON fetch returned: " + resultData);
 
           renameAccountIfNecessary(resultData, new Runnable() {
             @Override
             public void run() {
-              updateBundleValues(BUNDLE_KEY_PROFILE_JSON, resultData);
+              accountManager.setUserData(account, ACCOUNT_KEY_PROFILE_JSON, resultData);
               LocalBroadcastManager.getInstance(context).sendBroadcast(makeProfileJSONUpdatedIntent());
             }
           });
           break;
         case Activity.RESULT_CANCELED:
           Logger.warn(LOG_TAG, "Failed to fetch profile JSON; ignoring.");
           break;
         default:
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java
@@ -80,17 +80,18 @@ public class TestAccountPickler extends 
 
     assertEquals(TEST_USERNAME, o.getString(AccountPickler.KEY_EMAIL));
     assertEquals(TEST_PROFILE, o.getString(AccountPickler.KEY_PROFILE));
     assertEquals(TEST_AUTH_SERVER_URI, o.getString(AccountPickler.KEY_IDP_SERVER_URI));
     assertEquals(TEST_TOKEN_SERVER_URI, o.getString(AccountPickler.KEY_TOKEN_SERVER_URI));
     assertEquals(TEST_PROFILE_SERVER_URI, o.getString(AccountPickler.KEY_PROFILE_SERVER_URI));
 
     assertNotNull(o.getObject(AccountPickler.KEY_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP));
-    assertNotNull(o.get(AccountPickler.KEY_BUNDLE));
+    assertNotNull(o.get(AccountPickler.KEY_STATE_LABEL));
+    assertNotNull(o.get(AccountPickler.KEY_STATE));
   }
 
   public void testPickleAndUnpickle() throws Exception {
     final AndroidFxAccount inputAccount = addTestAccount();
 
     AccountPickler.pickle(inputAccount, PICKLE_FILENAME);
     final ExtendedJSONObject inputJSON = AccountPickler.toJSON(inputAccount, 0);
     final State inputState = inputAccount.getState();