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: *

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: }