gecko/mobile/android/base/sync/config/ConfigurationMigrator.java

383 lines
15 KiB
Java

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