michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0:
michael@0: package org.mozilla.gecko.sync.config;
michael@0:
michael@0: import java.util.HashMap;
michael@0: import java.util.Map;
michael@0: import java.util.Map.Entry;
michael@0:
michael@0: import org.mozilla.gecko.background.common.log.Logger;
michael@0: import org.mozilla.gecko.sync.SyncConfiguration;
michael@0: import org.mozilla.gecko.sync.Utils;
michael@0:
michael@0: import android.accounts.Account;
michael@0: import android.accounts.AccountManager;
michael@0: import android.content.Context;
michael@0: import android.content.SharedPreferences;
michael@0: import android.content.SharedPreferences.Editor;
michael@0:
michael@0: /**
michael@0: * Migrate Sync preferences between versions.
michael@0: *
michael@0: * The original preferences were un-versioned; we refer to that as "version 0".
michael@0: * The original preferences were stored in three places:
michael@0: *
michael@0: * - most prefs were kept in per-Sync account Android shared prefs;
michael@0: * - some prefs were kept in per-App Android shared prefs;
michael@0: * - some client prefs were kept in the (assumed unique) Android Account.
michael@0: *
michael@0: *
michael@0: * Post version 0, all preferences are stored in per-Sync account Android shared prefs.
michael@0: */
michael@0: public class ConfigurationMigrator {
michael@0: public static final String LOG_TAG = "ConfigMigrator";
michael@0:
michael@0: /**
michael@0: * Copy and rename preferences.
michael@0: *
michael@0: * @param from source.
michael@0: * @param to sink.
michael@0: * @param map map from old preference names to new preference names.
michael@0: * @return the number of preferences migrated.
michael@0: */
michael@0: protected static int copyPreferences(final SharedPreferences from, final Map map, final Editor to) {
michael@0: int count = 0;
michael@0:
michael@0: // SharedPreferences has no way to get a key/value pair without specifying the value type, so we do this instead.
michael@0: for (Entry entry : from.getAll().entrySet()) {
michael@0: String fromKey = entry.getKey();
michael@0: String toKey = map.get(fromKey);
michael@0: if (toKey == null) {
michael@0: continue;
michael@0: }
michael@0:
michael@0: Object value = entry.getValue();
michael@0: if (value instanceof Boolean) {
michael@0: to.putBoolean(toKey, ((Boolean) value).booleanValue());
michael@0: } else if (value instanceof Float) {
michael@0: to.putFloat(toKey, ((Float) value).floatValue());
michael@0: } else if (value instanceof Integer) {
michael@0: to.putInt(toKey, ((Integer) value).intValue());
michael@0: } else if (value instanceof Long) {
michael@0: to.putLong(toKey, ((Long) value).longValue());
michael@0: } else if (value instanceof String) {
michael@0: to.putString(toKey, (String) value);
michael@0: } else {
michael@0: // Do nothing -- perhaps SharedPreferences accepts types we don't know about.
michael@0: }
michael@0:
michael@0: if (Logger.LOG_PERSONAL_INFORMATION) {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + value + ").");
michael@0: } else {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
michael@0: }
michael@0: count += 1;
michael@0: }
michael@0:
michael@0: return count;
michael@0: }
michael@0:
michael@0: protected final static String V0_PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
michael@0: protected final static String V1_PREF_CLUSTER_URL_IS_STALE = V0_PREF_CLUSTER_URL_IS_STALE;
michael@0: protected final static String V0_PREF_EARLIEST_NEXT_SYNC = "earliestnextsync";
michael@0: protected final static String V1_PREF_EARLIEST_NEXT_SYNC = V0_PREF_EARLIEST_NEXT_SYNC;
michael@0:
michael@0: /**
michael@0: * Extract version 0 preferences from per-App Android shared prefs and write to version 1 per-Sync account shared prefs.
michael@0: *
michael@0: * @param from per-App version 0 Android shared prefs.
michael@0: * @param to per-Sync account version 1 shared prefs.
michael@0: * @return the number of preferences migrated.
michael@0: * @throws Exception
michael@0: */
michael@0: protected static int upgradeGlobals0to1(final SharedPreferences from, final SharedPreferences to) throws Exception {
michael@0: Map map = new HashMap();
michael@0: map.put(V0_PREF_CLUSTER_URL_IS_STALE, V1_PREF_CLUSTER_URL_IS_STALE);
michael@0: map.put(V0_PREF_EARLIEST_NEXT_SYNC, V1_PREF_EARLIEST_NEXT_SYNC);
michael@0:
michael@0: Editor editor = to.edit();
michael@0: int count = copyPreferences(from, map, editor);
michael@0: if (count > 0) {
michael@0: editor.commit();
michael@0: }
michael@0: return count;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Extract version 1 per-Sync account shared prefs and write to version 0 preferences from per-App Android shared prefs.
michael@0: *
michael@0: * @param from per-Sync account version 1 shared prefs.
michael@0: * @param to per-App version 0 Android shared prefs.
michael@0: * @return the number of preferences migrated.
michael@0: * @throws Exception
michael@0: */
michael@0: protected static int downgradeGlobals1to0(final SharedPreferences from, final SharedPreferences to) throws Exception {
michael@0: Map map = new HashMap();
michael@0: map.put(V1_PREF_CLUSTER_URL_IS_STALE, V0_PREF_CLUSTER_URL_IS_STALE);
michael@0: map.put(V1_PREF_EARLIEST_NEXT_SYNC, V0_PREF_EARLIEST_NEXT_SYNC);
michael@0:
michael@0: Editor editor = to.edit();
michael@0: int count = copyPreferences(from, map, editor);
michael@0: if (count > 0) {
michael@0: editor.commit();
michael@0: }
michael@0: return count;
michael@0: }
michael@0:
michael@0: protected static final String V0_PREF_ACCOUNT_GUID = "account.guid";
michael@0: protected static final String V1_PREF_ACCOUNT_GUID = V0_PREF_ACCOUNT_GUID;
michael@0: protected static final String V0_PREF_CLIENT_NAME = "account.clientName";
michael@0: protected static final String V1_PREF_CLIENT_NAME = V0_PREF_CLIENT_NAME;
michael@0: protected static final String V0_PREF_NUM_CLIENTS = "account.numClients";
michael@0: protected static final String V1_PREF_NUM_CLIENTS = V0_PREF_NUM_CLIENTS;
michael@0:
michael@0: /**
michael@0: * Extract version 0 per-Android account user data and write to version 1 per-Sync account shared prefs.
michael@0: *
michael@0: * @param accountManager Android account manager.
michael@0: * @param account Android account.
michael@0: * @param to per-Sync account version 1 shared prefs.
michael@0: * @return the number of preferences migrated.
michael@0: * @throws Exception
michael@0: */
michael@0: protected static int upgradeAndroidAccount0to1(final AccountManager accountManager, final Account account, final SharedPreferences to) throws Exception {
michael@0: final String V0_PREF_ACCOUNT_GUID = "account.guid";
michael@0: final String V1_PREF_ACCOUNT_GUID = V0_PREF_ACCOUNT_GUID;
michael@0: final String V0_PREF_CLIENT_NAME = "account.clientName";
michael@0: final String V1_PREF_CLIENT_NAME = V0_PREF_CLIENT_NAME;
michael@0: final String V0_PREF_NUM_CLIENTS = "account.numClients";
michael@0: final String V1_PREF_NUM_CLIENTS = V0_PREF_NUM_CLIENTS;
michael@0:
michael@0: String accountGUID = null;
michael@0: String clientName = null;
michael@0: long numClients = -1;
michael@0: try {
michael@0: accountGUID = accountManager.getUserData(account, V0_PREF_ACCOUNT_GUID);
michael@0: } catch (Exception e) {
michael@0: // Do nothing.
michael@0: }
michael@0: try {
michael@0: clientName = accountManager.getUserData(account, V0_PREF_CLIENT_NAME);
michael@0: } catch (Exception e) {
michael@0: // Do nothing.
michael@0: }
michael@0: try {
michael@0: numClients = Long.parseLong(accountManager.getUserData(account, V0_PREF_NUM_CLIENTS));
michael@0: } catch (Exception e) {
michael@0: // Do nothing.
michael@0: }
michael@0:
michael@0: final Editor editor = to.edit();
michael@0:
michael@0: int count = 0;
michael@0: if (accountGUID != null) {
michael@0: final String fromKey = V0_PREF_ACCOUNT_GUID;
michael@0: final String toKey = V1_PREF_ACCOUNT_GUID;
michael@0: if (Logger.LOG_PERSONAL_INFORMATION) {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + accountGUID + ").");
michael@0: } else {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
michael@0: }
michael@0: editor.putString(toKey, accountGUID);
michael@0: count += 1;
michael@0: }
michael@0: if (clientName != null) {
michael@0: final String fromKey = V0_PREF_CLIENT_NAME;
michael@0: final String toKey = V1_PREF_CLIENT_NAME;
michael@0: if (Logger.LOG_PERSONAL_INFORMATION) {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + clientName + ").");
michael@0: } else {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
michael@0: }
michael@0: editor.putString(toKey, clientName);
michael@0: count += 1;
michael@0: }
michael@0: if (numClients > -1) {
michael@0: final String fromKey = V0_PREF_NUM_CLIENTS;
michael@0: final String toKey = V1_PREF_NUM_CLIENTS;
michael@0: if (Logger.LOG_PERSONAL_INFORMATION) {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + numClients + ").");
michael@0: } else {
michael@0: Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
michael@0: }
michael@0: editor.putLong(toKey, numClients);
michael@0: count += 1;
michael@0: }
michael@0:
michael@0: if (count > 0) {
michael@0: editor.commit();
michael@0: }
michael@0: return count;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Extract version 1 per-Sync account shared prefs and write to version 0 per-Android account user data.
michael@0: *
michael@0: * @param from per-Sync account version 1 shared prefs.
michael@0: * @param accountManager Android account manager.
michael@0: * @param account Android account.
michael@0: * @return the number of preferences migrated.
michael@0: * @throws Exception
michael@0: */
michael@0: protected static int downgradeAndroidAccount1to0(final SharedPreferences from, final AccountManager accountManager, final Account account) throws Exception {
michael@0: final String accountGUID = from.getString(V1_PREF_ACCOUNT_GUID, null);
michael@0: final String clientName = from.getString(V1_PREF_CLIENT_NAME, null);
michael@0: final long numClients = from.getLong(V1_PREF_NUM_CLIENTS, -1L);
michael@0:
michael@0: int count = 0;
michael@0: if (accountGUID != null) {
michael@0: Logger.debug(LOG_TAG, "Migrated account GUID.");
michael@0: accountManager.setUserData(account, V0_PREF_ACCOUNT_GUID, accountGUID);
michael@0: count += 1;
michael@0: }
michael@0: if (clientName != null) {
michael@0: Logger.debug(LOG_TAG, "Migrated client name.");
michael@0: accountManager.setUserData(account, V1_PREF_CLIENT_NAME, clientName);
michael@0: count += 1;
michael@0: }
michael@0: if (numClients > -1) {
michael@0: Logger.debug(LOG_TAG, "Migrated clients count.");
michael@0: accountManager.setUserData(account, V1_PREF_NUM_CLIENTS, new Long(numClients).toString());
michael@0: count += 1;
michael@0: }
michael@0: return count;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Extract version 0 per-Android account user data and write to version 1 per-Sync account shared prefs.
michael@0: *
michael@0: * @param from per-Sync account version 0 shared prefs.
michael@0: * @param to per-Sync account version 1 shared prefs.
michael@0: * @return the number of preferences migrated.
michael@0: * @throws Exception
michael@0: */
michael@0: protected static int upgradeShared0to1(final SharedPreferences from, final SharedPreferences to) {
michael@0: final Map map = new HashMap();
michael@0: final String[] prefs = new String [] {
michael@0: "syncID",
michael@0: "clusterURL",
michael@0: "enabledEngineNames",
michael@0:
michael@0: "metaGlobalLastModified", "metaGlobalServerResponseBody",
michael@0:
michael@0: "crypto5KeysLastModified", "crypto5KeysServerResponseBody",
michael@0:
michael@0: "serverClientsTimestamp", "serverClientRecordTimestamp",
michael@0:
michael@0: "forms.remote", "forms.local", "forms.syncID",
michael@0: "tabs.remote", "tabs.local", "tabs.syncID",
michael@0: "passwords.remote", "passwords.local", "passwords.syncID",
michael@0: "history.remote", "history.local", "history.syncID",
michael@0: "bookmarks.remote", "bookmarks.local", "bookmarks.syncID",
michael@0: };
michael@0: for (String pref : prefs) {
michael@0: map.put(pref, pref);
michael@0: }
michael@0:
michael@0: Editor editor = to.edit();
michael@0: int count = copyPreferences(from, map, editor);
michael@0: if (count > 0) {
michael@0: editor.commit();
michael@0: }
michael@0: return count;
michael@0: }
michael@0:
michael@0: /**
michael@0: * Extract version 1 per-Sync account shared prefs and write to version 0 per-Android account user data.
michael@0: *
michael@0: * @param from per-Sync account version 1 shared prefs.
michael@0: * @param to per-Sync account version 0 shared prefs.
michael@0: * @return the number of preferences migrated.
michael@0: * @throws Exception
michael@0: */
michael@0: protected static int downgradeShared1to0(final SharedPreferences from, final SharedPreferences to) {
michael@0: // Strictly a copy, no re-naming, no deletions -- so just invert.
michael@0: return upgradeShared0to1(from, to);
michael@0: }
michael@0:
michael@0: public static void upgrade0to1(final Context context, final AccountManager accountManager, final Account account,
michael@0: final String product, final String username, final String serverURL, final String profile) throws Exception {
michael@0:
michael@0: final String GLOBAL_SHARED_PREFS = "sync.prefs.global";
michael@0:
michael@0: final SharedPreferences globalPrefs = context.getSharedPreferences(GLOBAL_SHARED_PREFS, Utils.SHARED_PREFERENCES_MODE);
michael@0: final SharedPreferences accountPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 0);
michael@0: final SharedPreferences newPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 1);
michael@0:
michael@0: upgradeGlobals0to1(globalPrefs, newPrefs);
michael@0: upgradeAndroidAccount0to1(accountManager, account, newPrefs);
michael@0: upgradeShared0to1(accountPrefs, newPrefs);
michael@0: }
michael@0:
michael@0: public static void downgrade1to0(final Context context, final AccountManager accountManager, final Account account,
michael@0: final String product, final String username, final String serverURL, final String profile) throws Exception {
michael@0:
michael@0: final String GLOBAL_SHARED_PREFS = "sync.prefs.global";
michael@0:
michael@0: final SharedPreferences globalPrefs = context.getSharedPreferences(GLOBAL_SHARED_PREFS, Utils.SHARED_PREFERENCES_MODE);
michael@0: final SharedPreferences accountPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 0);
michael@0: final SharedPreferences oldPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 1);
michael@0:
michael@0: downgradeGlobals1to0(oldPrefs, globalPrefs);
michael@0: downgradeAndroidAccount1to0(oldPrefs, accountManager, account);
michael@0: downgradeShared1to0(oldPrefs, accountPrefs);
michael@0: }
michael@0:
michael@0: /**
michael@0: * Migrate, if necessary, existing prefs to a certain version.
michael@0: *
michael@0: * Stores current prefs version in Android shared prefs with root
michael@0: * "sync.prefs.version", which corresponds to the file
michael@0: * "sync.prefs.version.xml".
michael@0: *
michael@0: * @param desiredVersion
michael@0: * version to finish it.
michael@0: * @param context
michael@0: * @param accountManager
michael@0: * @param account
michael@0: * @param product
michael@0: * @param username
michael@0: * @param serverURL
michael@0: * @param profile
michael@0: * @throws Exception
michael@0: */
michael@0: public static void ensurePrefsAreVersion(final long desiredVersion,
michael@0: final Context context, final AccountManager accountManager, final Account account,
michael@0: final String product, final String username, final String serverURL, final String profile) throws Exception {
michael@0: if (desiredVersion < 0 || desiredVersion > SyncConfiguration.CURRENT_PREFS_VERSION) {
michael@0: throw new IllegalArgumentException("Cannot migrate to unknown version " + desiredVersion + ".");
michael@0: }
michael@0:
michael@0: SharedPreferences versionPrefs = context.getSharedPreferences("sync.prefs.version", Utils.SHARED_PREFERENCES_MODE);
michael@0:
michael@0: // We default to 0 since clients getting this code for the first time will
michael@0: // not have "sync.prefs.version.xml" *at all*, and upgrading when all old
michael@0: // data is missing is expected to be safe.
michael@0: long currentVersion = versionPrefs.getLong(SyncConfiguration.PREF_PREFS_VERSION, 0);
michael@0: if (currentVersion == desiredVersion) {
michael@0: Logger.info(LOG_TAG, "Current version (" + currentVersion + ") is desired version; no need to migrate.");
michael@0: return;
michael@0: }
michael@0:
michael@0: if (currentVersion < 0 || currentVersion > SyncConfiguration.CURRENT_PREFS_VERSION) {
michael@0: throw new IllegalStateException("Cannot migrate from unknown version " + currentVersion + ".");
michael@0: }
michael@0:
michael@0: // Now we're down to either version 0 or version 1.
michael@0: if (currentVersion == 0 && desiredVersion == 1) {
michael@0: Logger.info(LOG_TAG, "Upgrading from version 0 to version 1.");
michael@0: upgrade0to1(context, accountManager, account, product, username, serverURL, profile);
michael@0: } else if (currentVersion == 1 && desiredVersion == 0) {
michael@0: Logger.info(LOG_TAG, "Upgrading from version 0 to version 1.");
michael@0: upgrade0to1(context, accountManager, account, product, username, serverURL, profile);
michael@0: } else {
michael@0: Logger.warn(LOG_TAG, "Don't know how to migrate from version " + currentVersion + " to " + desiredVersion + ".");
michael@0: }
michael@0:
michael@0: Logger.info(LOG_TAG, "Migrated from version " + currentVersion + " to version " + desiredVersion + ".");
michael@0: versionPrefs.edit().putLong(SyncConfiguration.PREF_PREFS_VERSION, desiredVersion).commit();
michael@0: }
michael@0: }