Bug 761682, Bug 777973 - Version prefs; don't always invalidate auth token. r=rnewman

This commit is contained in:
Nick Alexander 2012-08-08 17:13:20 -07:00
Родитель 78d197e3b7
Коммит aaf4a4129a
14 изменённых файлов: 1003 добавлений и 319 удалений

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Просмотреть файл

@ -8,7 +8,7 @@
<ListView
android:id="@+id/device_list"
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/sendtab_top"
android:layout_above="@+id/sendtab_bottom" >
@ -25,4 +25,4 @@
android:enabled="false"
android:text="@string/sync_button_send" />
</LinearLayout>
</RelativeLayout>
</RelativeLayout>

Просмотреть файл

@ -0,0 +1,55 @@
/* 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;
import android.content.SyncResult;
/**
* There was a problem with the Sync account's credentials: bad username,
* missing password, malformed sync key, etc.
*/
public abstract class CredentialException extends SyncException {
private static final long serialVersionUID = 833010553314100538L;
public CredentialException() {
super();
}
public CredentialException(final Throwable e) {
super(e);
}
public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
syncResult.stats.numAuthExceptions += 1;
}
/**
* No credentials at all.
*/
public static class MissingAllCredentialsException extends CredentialException {
private static final long serialVersionUID = 3763937096217604611L;
public MissingAllCredentialsException() {
super();
}
public MissingAllCredentialsException(final Throwable e) {
super(e);
}
}
/**
* Some credential is missing.
*/
public static class MissingCredentialException extends CredentialException {
private static final long serialVersionUID = -7543031216547596248L;
public final String missingCredential;
public MissingCredentialException(final String missingCredential) {
this.missingCredential = missingCredential;
}
}
}

Просмотреть файл

@ -216,6 +216,9 @@ public class SyncConfiguration implements CredentialsSource {
public String prefsPath;
public PrefsSource prefsSource;
public static final String PREF_PREFS_VERSION = "prefs.version";
public static final long CURRENT_PREFS_VERSION = 1;
public static final String CLIENTS_COLLECTION_TIMESTAMP = "serverClientsTimestamp"; // When the collection was touched.
public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp"; // When our record was touched.
@ -223,6 +226,13 @@ public class SyncConfiguration implements CredentialsSource {
public static final String PREF_SYNC_ID = "syncID";
public static final String PREF_ENABLED_ENGINE_NAMES = "enabledEngineNames";
public static final String PREF_EARLIEST_NEXT_SYNC = "earliestnextsync";
public static final String PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
public static final String PREF_ACCOUNT_GUID = "account.guid";
public static final String PREF_CLIENT_NAME = "account.clientName";
public static final String PREF_NUM_CLIENTS = "account.numClients";
/**
* Create a new SyncConfiguration instance. Pass in a PrefsSource to
* provide access to preferences.

Просмотреть файл

@ -7,14 +7,25 @@ package org.mozilla.gecko.sync;
import android.content.SyncResult;
public abstract class SyncException extends Exception {
public Exception cause = null;
public SyncException(Exception ex) {
cause = ex;
}
private static final long serialVersionUID = -6928990004393234738L;
public SyncException() {
super();
}
private static final long serialVersionUID = -6928990004393234738L;
public SyncException(final Throwable e) {
super(e);
}
/**
* Update sync result statistics with information particular to this
* exception.
*
* @param globalSession
* current session, or null.
* @param syncResult
* Android sync result to update.
*/
public void updateStats(GlobalSession globalSession, SyncResult syncResult) {
// Assume storage error.
// TODO: this logic is overly simplistic.

Просмотреть файл

@ -215,34 +215,34 @@ public class Utils {
return sha1Base32(account.toLowerCase(Locale.US));
}
public static SharedPreferences getSharedPreferences(final Context context, final String product, final String username, final String serverURL, final String profile, final long version)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
String prefsPath = getPrefsPath(product, username, serverURL, profile, version);
return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
}
/**
* Get shared preferences path for a Sync account.
*
* @param product the Firefox Sync product package name (like "org.mozilla.firefox").
* @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>.
* @param serverURL the Sync account server URL.
* @param profile the Firefox profile name.
* @param version the version of preferences to reference.
* @return the path.
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static String getPrefsPath(String username, String serverURL)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
return "sync.prefs." + sha1Base32(serverURL + ":" + usernameFromAccount(username));
}
public static String getPrefsPath(final String product, final String username, final String serverURL, final String profile, final long version)
throws NoSuchAlgorithmException, UnsupportedEncodingException {
final String encodedAccount = sha1Base32(serverURL + ":" + usernameFromAccount(username));
/**
* Get shared preferences for a Sync account.
*
* @param context Android <code>Context</code>.
* @param username the Sync account name, optionally encoded with <code>Utils.usernameFromAccount</code>.
* @param serverURL the Sync account server URL.
* @return a <code>SharedPreferences</code> instance.
* @throws NoSuchAlgorithmException
* @throws UnsupportedEncodingException
*/
public static SharedPreferences getSharedPreferences(Context context, String username, String serverURL) throws NoSuchAlgorithmException, UnsupportedEncodingException {
String prefsPath = getPrefsPath(username, serverURL);
Logger.debug(LOG_TAG, "Shared preferences: " + prefsPath);
return context.getSharedPreferences(prefsPath, SHARED_PREFERENCES_MODE);
if (version <= 0) {
return "sync.prefs." + encodedAccount;
} else {
final String sanitizedProduct = product.replace('.', '!').replace(' ', '!');
return "sync.prefs." + sanitizedProduct + "." + encodedAccount + "." + profile + "." + version;
}
}
public static void addToIndexBucketMap(TreeMap<Long, ArrayList<String>> map, long index, String value) {

Просмотреть файл

@ -0,0 +1,382 @@
/* 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();
}
}

Просмотреть файл

@ -4,10 +4,15 @@
package org.mozilla.gecko.sync.receivers;
import org.mozilla.gecko.sync.CredentialException;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.config.ConfigurationMigrator;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import android.accounts.Account;
import android.accounts.AccountManager;
@ -26,8 +31,9 @@ public class UpgradeReceiver extends BroadcastReceiver {
ThreadPool.run(new Runnable() {
@Override
public void run() {
AccountManager accountManager = AccountManager.get(context);
Account[] accounts = accountManager.getAccounts();
final AccountManager accountManager = AccountManager.get(context);
final Account[] accounts = SyncAccounts.syncAccounts(context);
for (Account a : accounts) {
if ("1".equals(accountManager.getUserData(a, Constants.DATA_ENABLE_ON_UPGRADE))) {
SyncAccounts.setSyncAutomatically(a, true);
@ -36,5 +42,40 @@ public class UpgradeReceiver extends BroadcastReceiver {
}
}
});
/**
* Bug 761682: migrate preferences forward.
*/
ThreadPool.run(new Runnable() {
@Override
public void run() {
AccountManager accountManager = AccountManager.get(context);
final Account[] accounts = SyncAccounts.syncAccounts(context);
for (Account account : accounts) {
Logger.info(LOG_TAG, "Migrating preferences on upgrade for Android account named " + account.name + ".");
SyncAccountParameters params;
try {
params = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
} catch (CredentialException e) {
Logger.warn(LOG_TAG, "Caught exception fetching account parameters while trying to migrate preferences; ignoring.", e);
continue;
}
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
final String username = params.username;
final String serverURL = params.serverURL;
final String profile = "default";
try {
ConfigurationMigrator.ensurePrefsAreVersion(SyncConfiguration.CURRENT_PREFS_VERSION, context, accountManager, account,
product, username, serverURL, profile);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception trying to migrate preferences; ignoring.", e);
continue;
}
}
}
});
}
}

Просмотреть файл

@ -12,11 +12,10 @@ public class Constants {
public static final String OPTION_USERNAME = "option.username";
public static final String AUTHTOKEN_TYPE_PLAIN = "auth.plain";
public static final String OPTION_SERVER = "option.serverUrl";
public static final String ACCOUNT_GUID = "account.guid";
public static final String CLIENT_NAME = "account.clientName";
public static final String NUM_CLIENTS = "account.numClients";
public static final String DATA_ENABLE_ON_UPGRADE = "data.enableOnUpgrade";
public static final String DEFAULT_PROFILE = "default";
/**
* Name of file to pickle current account preferences to each sync.
* <p>

Просмотреть файл

@ -5,19 +5,26 @@
package org.mozilla.gecko.sync.setup;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.CredentialException;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.AccountPickler;
import org.mozilla.gecko.sync.repositories.android.RepoUtils;
import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
@ -43,6 +50,44 @@ public class SyncAccounts {
public final static String DEFAULT_SERVER = "https://auth.services.mozilla.com/";
/**
* Return Sync accounts.
*
* @param c Android context.
* @return Sync accounts.
*/
public static Account[] syncAccounts(final Context c) {
return AccountManager.get(c).getAccountsByType(GlobalConstants.ACCOUNTTYPE_SYNC);
}
/**
* Asynchronously invalidate the auth token for a Sync account.
*
* @param accountManager Android account manager.
* @param account Android account.
*/
public static void invalidateAuthToken(final AccountManager accountManager, final Account account) {
if (account == null) {
return;
}
// blockingGetAuthToken must not be called from the main thread.
ThreadPool.run(new Runnable() {
@Override
public void run() {
String authToken;
try {
authToken = accountManager.blockingGetAuthToken(account, Constants.AUTHTOKEN_TYPE_PLAIN, true);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception while invalidating auth token.", e);
return;
}
accountManager.invalidateAuthToken(GlobalConstants.ACCOUNTTYPE_SYNC, authToken);
}
});
}
/**
* Returns true if a Sync account is set up, or we have a pickled Sync account
* on disk that should be un-pickled (Bug 769745). If we have a pickled Sync
@ -321,28 +366,30 @@ public class SyncAccounts {
setIsSyncable(account, syncAutomatically);
Logger.debug(LOG_TAG, "Set account to sync automatically? " + syncAutomatically + ".");
setClientRecord(context, accountManager, account, syncAccount.clientName, syncAccount.clientGuid);
// TODO: add other ContentProviders as needed (e.g. passwords)
// TODO: for each, also add to res/xml to make visible in account settings
Logger.debug(LOG_TAG, "Finished setting syncables.");
// Purging global prefs assumes we have only a single Sync account at one time.
// TODO: Bug 761682: don't do anything with global prefs here.
if (clearPreferences) {
Logger.info(LOG_TAG, "Clearing global prefs.");
SyncAdapter.purgeGlobalPrefs(context);
}
try {
SharedPreferences.Editor editor = Utils.getSharedPreferences(context, username, serverURL).edit();
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
final String profile = Constants.DEFAULT_PROFILE;
final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
final SharedPreferences.Editor editor = Utils.getSharedPreferences(context, product, username, serverURL, profile, version).edit();
if (clearPreferences) {
Logger.info(LOG_TAG, "Clearing preferences path " + Utils.getPrefsPath(username, serverURL) + " for this account.");
final String prefsPath = Utils.getPrefsPath(product, username, serverURL, profile, version);
Logger.info(LOG_TAG, "Clearing preferences path " + prefsPath + " for this account.");
editor.clear();
}
if (syncAccount.clusterURL != null) {
editor.putString(SyncConfiguration.PREF_CLUSTER_URL, syncAccount.clusterURL);
}
if (syncAccount.clientName != null && syncAccount.clientGuid != null) {
Logger.debug(LOG_TAG, "Setting client name to " + syncAccount.clientName + " and client GUID to " + syncAccount.clientGuid + ".");
editor.putString(SyncConfiguration.PREF_CLIENT_NAME, syncAccount.clientName);
editor.putString(SyncConfiguration.PREF_ACCOUNT_GUID, syncAccount.clientGuid);
} else {
Logger.debug(LOG_TAG, "Client name and guid not both non-null, so not setting client data.");
}
editor.commit();
} catch (Exception e) {
Logger.error(LOG_TAG, "Could not clear prefs path!", e);
@ -433,14 +480,126 @@ public class SyncAccounts {
return intent;
}
protected static void setClientRecord(Context context, AccountManager accountManager, Account account,
String clientName, String clientGuid) {
if (clientName != null && clientGuid != null) {
Logger.debug(LOG_TAG, "Setting client name to " + clientName + " and client GUID to " + clientGuid + ".");
SyncAdapter.setAccountGUID(accountManager, account, clientGuid);
SyncAdapter.setClientName(accountManager, account, clientName);
return;
protected static class SyncAccountVersion0Callback implements AccountManagerCallback<Bundle> {
protected final Context context;
protected final CountDownLatch latch;
public String authToken = null;
public SyncAccountVersion0Callback(final Context context, final CountDownLatch latch) {
this.context = context;
this.latch = latch;
}
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
Bundle bundle = future.getResult(60L, TimeUnit.SECONDS);
if (bundle.containsKey(AccountManager.KEY_INTENT)) {
throw new IllegalStateException("KEY_INTENT included in AccountManagerFuture bundle.");
}
if (bundle.containsKey(AccountManager.KEY_ERROR_MESSAGE)) {
throw new IllegalStateException("KEY_ERROR_MESSAGE (= " + bundle.getString(AccountManager.KEY_ERROR_MESSAGE) + ") "
+ " included in AccountManagerFuture bundle.");
}
authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
} catch (Exception e) {
// Do nothing -- caller will find null authToken.
Logger.warn(LOG_TAG, "Got exception fetching auth token; ignoring and returning null auth token instead.", e);
} finally {
latch.countDown();
}
}
}
/**
* Synchronously extract Sync account parameters from Android account version
* 0, using plain auth token type.
* <p>
* Safe to call from main thread.
*
* @param context
* @param accountManager
* Android account manager.
* @param account
* Android account.
* @return Sync account parameters, always non-null; fields username,
* password, serverURL, and syncKey always non-null.
*/
public static SyncAccountParameters blockingFromAndroidAccountV0(final Context context, final AccountManager accountManager, final Account account)
throws CredentialException {
final CountDownLatch latch = new CountDownLatch(1);
final SyncAccountVersion0Callback callback = new SyncAccountVersion0Callback(context, latch);
new Thread(new Runnable() {
@Override
public void run() {
// Get an auth token.
accountManager.getAuthToken(account, Constants.AUTHTOKEN_TYPE_PLAIN, true, callback, null);
}
}).start();
try {
latch.await();
} catch (InterruptedException e) {
Logger.warn(LOG_TAG, "Got exception waiting for Sync account parameters; throwing.");
throw new CredentialException.MissingAllCredentialsException(e);
}
String username;
try {
username = Utils.usernameFromAccount(account.name);
} catch (NoSuchAlgorithmException e) {
throw new CredentialException.MissingCredentialException("username");
} catch (UnsupportedEncodingException e) {
throw new CredentialException.MissingCredentialException("username");
}
final String password = callback.authToken;
/*
* If we are accessing an Account that we don't own, Android will throw an
* unchecked <code>SecurityException</code> saying
* "W FxSync(XXXX) java.lang.SecurityException: caller uid XXXXX is different than the authenticator's uid".
* We catch that error and throw accordingly.
*/
String syncKey;
String serverURL;
try {
syncKey = accountManager.getUserData(account, Constants.OPTION_SYNCKEY);
serverURL = accountManager.getUserData(account, Constants.OPTION_SERVER);
} catch (SecurityException e) {
Logger.warn(LOG_TAG, "Got security exception fetching Sync account parameters; throwing.");
throw new CredentialException.MissingAllCredentialsException(e);
}
if (password == null &&
username == null &&
syncKey == null &&
serverURL == null) {
throw new CredentialException.MissingAllCredentialsException();
}
if (password == null) {
throw new CredentialException.MissingCredentialException("password");
}
if (syncKey == null) {
throw new CredentialException.MissingCredentialException("syncKey");
}
if (serverURL == null) {
throw new CredentialException.MissingCredentialException("serverURL");
}
try {
// SyncAccountParameters constructor throws on null inputs. This shouldn't
// happen, but let's be safe.
return new SyncAccountParameters(context, accountManager, username, syncKey, password, serverURL);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception fetching Sync account parameters; throwing.");
throw new CredentialException.MissingAllCredentialsException(e);
}
Logger.debug(LOG_TAG, "Client name and guid not both non-null, so not setting client data.");
}
}

Просмотреть файл

@ -49,6 +49,87 @@ public class SyncAuthenticatorService extends Service {
return sAccountAuthenticator;
}
/**
* Generate a "plain" auth token.
* <p>
* Android caches only the value of the key
* <code>AccountManager.KEY_AUTHTOKEN</code>, so if a caller needs the other
* keys in this bundle, it needs to invalidate the token (so that the bundle
* is re-generated).
*
* @param context
* Android context.
* @param account
* Android account.
* @return a <code>Bundle</code> instance containing a subset of the following
* keys: (caller's must check for missing keys)
* <ul>
* <li><code>AccountManager.KEY_ACCOUNT_TYPE</code>: the Android
* Account's type</li>
*
* <li><code>AccountManager.KEY_ACCOUNT_NAME</code>: the Android
* Account's name</li>
*
* <li><code>AccountManager.KEY_AUTHTOKEN</code>: the Sync account's
* password </li>
*
* <li><code> Constants.OPTION_USERNAME</code>: the Sync account's
* hashed username</li>
*
* <li><code>Constants.OPTION_SERVER</code>: the Sync account's
* server</li>
*
* <li><code> Constants.OPTION_SYNCKEY</code>: the Sync account's
* sync key</li>
*
* </ul>
* @throws NetworkErrorException
*/
public static Bundle getPlainAuthToken(final Context context, final Account account)
throws NetworkErrorException {
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
final AccountManager am = AccountManager.get(context);
final String password = am.getPassword(account);
if (password == null) {
Logger.warn(LOG_TAG, "Returning null bundle for getPlainAuthToken since Account password is null.");
return null;
}
final Bundle result = new Bundle();
// This is a Sync account.
result.putString(AccountManager.KEY_ACCOUNT_TYPE, GlobalConstants.ACCOUNTTYPE_SYNC);
// Server.
String serverURL = am.getUserData(account, Constants.OPTION_SERVER);
result.putString(Constants.OPTION_SERVER, serverURL);
// Full username, before hashing.
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
// Username after hashing.
try {
String username = Utils.usernameFromAccount(account.name);
Logger.pii(LOG_TAG, "Account " + account.name + " hashes to " + username + ".");
Logger.debug(LOG_TAG, "Setting username. Null? " + (username == null));
result.putString(Constants.OPTION_USERNAME, username);
} catch (NoSuchAlgorithmException e) {
// Do nothing. Calling code must check for missing value.
} catch (UnsupportedEncodingException e) {
// Do nothing. Calling code must check for missing value.
}
// Sync key.
final String syncKey = am.getUserData(account, Constants.OPTION_SYNCKEY);
Logger.debug(LOG_TAG, "Setting sync key. Null? " + (syncKey == null));
result.putString(Constants.OPTION_SYNCKEY, syncKey);
// Password.
result.putString(AccountManager.KEY_AUTHTOKEN, password);
return result;
}
private static class SyncAccountAuthenticator extends AbstractAccountAuthenticator {
private Context mContext;
public SyncAccountAuthenticator(Context context) {
@ -93,53 +174,14 @@ public class SyncAuthenticatorService extends Service {
Account account, String authTokenType, Bundle options)
throws NetworkErrorException {
Logger.debug(LOG_TAG, "getAuthToken()");
if (!authTokenType.equals(Constants.AUTHTOKEN_TYPE_PLAIN)) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ERROR_MESSAGE,
"invalid authTokenType");
return result;
if (Constants.AUTHTOKEN_TYPE_PLAIN.equals(authTokenType)) {
return getPlainAuthToken(mContext, account);
}
// Extract the username and password from the Account Manager, and ask
// the server for an appropriate AuthToken.
final AccountManager am = AccountManager.get(mContext);
final String password = am.getPassword(account);
if (password != null) {
final Bundle result = new Bundle();
// This is a Sync account.
result.putString(AccountManager.KEY_ACCOUNT_TYPE, GlobalConstants.ACCOUNTTYPE_SYNC);
// Server.
String serverURL = am.getUserData(account, Constants.OPTION_SERVER);
result.putString(Constants.OPTION_SERVER, serverURL);
// Full username, before hashing.
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
// Username after hashing.
try {
String username = Utils.usernameFromAccount(account.name);
Logger.pii(LOG_TAG, "Account " + account.name + " hashes to " + username + ".");
Logger.debug(LOG_TAG, "Setting username. Null? " + (username == null));
result.putString(Constants.OPTION_USERNAME, username);
} catch (NoSuchAlgorithmException e) {
// Do nothing. Calling code must check for missing value.
} catch (UnsupportedEncodingException e) {
// Do nothing. Calling code must check for missing value.
}
// Sync key.
final String syncKey = am.getUserData(account, Constants.OPTION_SYNCKEY);
Logger.debug(LOG_TAG, "Setting sync key. Null? " + (syncKey == null));
result.putString(Constants.OPTION_SYNCKEY, syncKey);
// Password.
result.putString(AccountManager.KEY_AUTHTOKEN, password);
return result;
}
Logger.warn(LOG_TAG, "Returning null bundle for getAuthToken.");
return null;
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ERROR_MESSAGE, "invalid authTokenType");
return result;
}
@Override

Просмотреть файл

@ -9,13 +9,18 @@ import java.util.List;
import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.CommandProcessor;
import org.mozilla.gecko.sync.CommandRunner;
import org.mozilla.gecko.sync.CredentialException;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import org.mozilla.gecko.sync.stage.SyncClientsEngineStage;
import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
@ -24,6 +29,7 @@ import android.accounts.AccountManager;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
@ -106,17 +112,47 @@ public class SendTabActivity extends Activity {
* @return Return null if there is no account set up. Return the account GUID otherwise.
*/
private String getAccountGUID() {
if (accountManager == null || localAccount == null) {
if (localAccount == null) {
Logger.warn(LOG_TAG, "Null local account; aborting.");
return null;
}
SyncAccountParameters params;
try {
params = SyncAccounts.blockingFromAndroidAccountV0(this, accountManager, localAccount);
} catch (CredentialException e) {
Logger.warn(LOG_TAG, "Could not get sync account parameters; aborting.");
return null;
}
SharedPreferences prefs;
try {
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
final String profile = Constants.DEFAULT_PROFILE;
final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
prefs = Utils.getSharedPreferences(getApplicationContext(), product, params.username, params.serverURL, profile, version);
return prefs.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
} catch (Exception e) {
return null;
}
return accountManager.getUserData(localAccount, Constants.ACCOUNT_GUID);
}
public void sendClickHandler(View view) {
Logger.info(LOG_TAG, "Send was clicked.");
Bundle extras = this.getIntent().getExtras();
if (extras == null) {
Logger.warn(LOG_TAG, "extras was null; aborting without sending tab.");
notifyAndFinish(false);
return;
}
final String uri = extras.getString(Intent.EXTRA_TEXT);
final String title = extras.getString(Intent.EXTRA_SUBJECT);
final List<String> guids = arrayAdapter.getCheckedGUIDs();
if (title == null) {
Logger.warn(LOG_TAG, "title was null; ignoring and sending tab anyway.");
}
if (uri == null) {
Logger.warn(LOG_TAG, "uri was null; aborting without sending tab.");
@ -124,37 +160,43 @@ public class SendTabActivity extends Activity {
return;
}
if (title == null) {
Logger.warn(LOG_TAG, "title was null; ignoring and sending tab anyway.");
}
final String clientGUID = getAccountGUID();
final List<String> guids = arrayAdapter.getCheckedGUIDs();
if (clientGUID == null || guids == null) {
if (guids == null) {
// Should never happen.
Logger.warn(LOG_TAG, "clientGUID? " + (clientGUID == null) + " or guids? " + (guids == null) +
" was null; aborting without sending tab.");
Logger.warn(LOG_TAG, "guids was null; aborting without sending tab.");
notifyAndFinish(false);
return;
}
// Perform tab sending on another thread.
new Thread() {
// Fetching local client GUID hits the DB, and we want to update the UI
// afterward, so we perform the tab sending on another thread.
new AsyncTask<Void, Void, Boolean>() {
@Override
public void run() {
protected Boolean doInBackground(Void... params) {
final CommandProcessor processor = CommandProcessor.getProcessor();
final String accountGUID = getAccountGUID();
Logger.debug(LOG_TAG, "Retrieved local account GUID '" + accountGUID + "'.");
if (accountGUID == null) {
return false;
}
for (String guid : guids) {
processor.sendURIToClientForDisplay(uri, guid, title, clientGUID, getApplicationContext());
processor.sendURIToClientForDisplay(uri, guid, title, accountGUID, getApplicationContext());
}
Logger.info(LOG_TAG, "Requesting immediate clients stage sync.");
SyncAdapter.requestImmediateSync(localAccount, new String[] { SyncClientsEngineStage.COLLECTION_NAME });
}
}.start();
notifyAndFinish(true);
return true;
}
@Override
protected void onPostExecute(final Boolean success) {
// We're allowed to update the UI from here.
notifyAndFinish(success.booleanValue());
}
}.execute();
}
/**

Просмотреть файл

@ -7,11 +7,12 @@ package org.mozilla.gecko.sync.syncadapter;
import java.io.IOException;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.json.simple.parser.ParseException;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.AlreadySyncingException;
import org.mozilla.gecko.sync.CredentialException;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.Logger;
@ -34,8 +35,6 @@ import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.AbstractThreadedSyncAdapter;
@ -48,21 +47,14 @@ import android.content.SyncResult;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteException;
import android.os.Bundle;
import android.os.Handler;
public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSessionCallback, ClientsDataDelegate {
private static final String LOG_TAG = "SyncAdapter";
private static final String PREFS_EARLIEST_NEXT_SYNC = "earliestnextsync";
private static final String PREFS_INVALIDATE_AUTH_TOKEN = "invalidateauthtoken";
private static final String PREFS_CLUSTER_URL_IS_STALE = "clusterurlisstale";
private static final int SHARED_PREFERENCES_MODE = 0;
private static final int BACKOFF_PAD_SECONDS = 5;
public static final int MULTI_DEVICE_INTERVAL_MILLISECONDS = 5 * 60 * 1000; // 5 minutes.
public static final int SINGLE_DEVICE_INTERVAL_MILLISECONDS = 24 * 60 * 60 * 1000; // 24 hours.
private final AccountManager mAccountManager;
private final Context mContext;
protected long syncStartTimestamp;
@ -70,60 +62,43 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
public SyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mContext = context;
mAccountManager = AccountManager.get(context);
}
public static SharedPreferences getGlobalPrefs(Context context) {
return context.getSharedPreferences("sync.prefs.global", SHARED_PREFERENCES_MODE);
}
public static void purgeGlobalPrefs(Context context) {
getGlobalPrefs(context).edit().clear().commit();
}
/**
* Backoff.
*/
public synchronized long getEarliestNextSync() {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
return sharedPreferences.getLong(PREFS_EARLIEST_NEXT_SYNC, 0);
return accountSharedPreferences.getLong(SyncConfiguration.PREF_EARLIEST_NEXT_SYNC, 0);
}
public synchronized void setEarliestNextSync(long next) {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
Editor edit = sharedPreferences.edit();
edit.putLong(PREFS_EARLIEST_NEXT_SYNC, next);
Editor edit = accountSharedPreferences.edit();
edit.putLong(SyncConfiguration.PREF_EARLIEST_NEXT_SYNC, next);
edit.commit();
}
public synchronized void extendEarliestNextSync(long next) {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
if (sharedPreferences.getLong(PREFS_EARLIEST_NEXT_SYNC, 0) >= next) {
if (accountSharedPreferences.getLong(SyncConfiguration.PREF_EARLIEST_NEXT_SYNC, 0) >= next) {
return;
}
Editor edit = sharedPreferences.edit();
edit.putLong(PREFS_EARLIEST_NEXT_SYNC, next);
Editor edit = accountSharedPreferences.edit();
edit.putLong(SyncConfiguration.PREF_EARLIEST_NEXT_SYNC, next);
edit.commit();
}
public synchronized boolean getShouldInvalidateAuthToken() {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
return sharedPreferences.getBoolean(PREFS_INVALIDATE_AUTH_TOKEN, false);
}
public synchronized void clearShouldInvalidateAuthToken() {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
Editor edit = sharedPreferences.edit();
edit.remove(PREFS_INVALIDATE_AUTH_TOKEN);
edit.commit();
}
public synchronized void setShouldInvalidateAuthToken() {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
Editor edit = sharedPreferences.edit();
edit.putBoolean(PREFS_INVALIDATE_AUTH_TOKEN, true);
edit.commit();
}
private void handleException(Exception e, SyncResult syncResult) {
setShouldInvalidateAuthToken();
/**
* Handle an exception: update stats, invalidate auth token, log errors, etc.
*
* @param globalSession
* current global session, or null.
* @param e
* Exception to handle.
*/
protected void processException(final GlobalSession globalSession, final Exception e) {
try {
// Just in case, invalidate auth token.
SyncAccounts.invalidateAuthToken(AccountManager.get(mContext), localAccount);
if (e instanceof SQLiteConstraintException) {
Logger.error(LOG_TAG, "Constraint exception. Aborting sync.", e);
syncResult.stats.numParseExceptions++; // This is as good as we can do.
@ -149,27 +124,50 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
e.printStackTrace();
return;
}
syncResult.stats.numIoExceptions++;
// Blanket stats updating for SyncException subclasses.
if (e instanceof SyncException) {
((SyncException) e).updateStats(globalSession, syncResult);
} else {
// Generic exception.
syncResult.stats.numIoExceptions++;
}
if (e instanceof CredentialException.MissingAllCredentialsException) {
// This is bad: either we couldn't fetch credentials, or the credentials
// were totally blank. Most likely the user has two copies of Firefox
// installed, and something is misbehaving.
// Either way, disable this account.
if (localAccount == null) {
// Should not happen, but be safe.
Logger.error(LOG_TAG, "No credentials attached to account. Aborting sync.");
return;
}
Logger.error(LOG_TAG, "No credentials attached to account " + localAccount.name + ". Aborting sync.");
try {
SyncAccounts.setSyncAutomatically(localAccount, false);
} catch (Exception ex) {
Logger.error(LOG_TAG, "Unable to disable account " + localAccount.name + ".", ex);
}
return;
}
if (e instanceof CredentialException.MissingCredentialException) {
Logger.error(LOG_TAG, "Credentials attached to account, but missing " +
((CredentialException.MissingCredentialException) e).missingCredential + ". Aborting sync.");
return;
}
if (e instanceof CredentialException) {
Logger.error(LOG_TAG, "Credentials attached to account were bad.");
return;
}
// Generic exception.
Logger.error(LOG_TAG, "Unknown exception. Aborting sync.", e);
} catch (Exception ex) {
Logger.error(LOG_TAG, "Unknown exception. Aborting sync.", e);
} finally {
notifyMonitor();
}
}
private AccountManagerFuture<Bundle> getAuthToken(final Account account,
AccountManagerCallback<Bundle> callback,
Handler handler) {
return mAccountManager.getAuthToken(account, Constants.AUTHTOKEN_TYPE_PLAIN, true, callback, handler);
}
private void invalidateAuthToken(Account account) {
AccountManagerFuture<Bundle> future = getAuthToken(account, null, null);
String token;
try {
token = future.getResult().getString(AccountManager.KEY_AUTHTOKEN);
mAccountManager.invalidateAuthToken(GlobalConstants.ACCOUNTTYPE_SYNC, token);
} catch (Exception e) {
Logger.error(LOG_TAG, "Couldn't invalidate auth token: " + e);
}
}
@ -187,6 +185,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
public Account localAccount;
protected boolean thisSyncIsForced = false;
public SharedPreferences accountSharedPreferences;
/**
* Return the number of milliseconds until we're allowed to sync again,
@ -277,112 +276,86 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
this.syncResult = syncResult;
this.localAccount = account;
Logger.info(LOG_TAG,
"Syncing account named " + account.name +
" for client named '" + getClientName() +
"' with client guid " + getAccountGUID() +
" (sync account has " + getClientsCount() + " clients).");
thisSyncIsForced = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
long delay = delayMilliseconds();
if (delay > 0) {
if (thisSyncIsForced) {
Logger.info(LOG_TAG, "Forced sync: overruling remaining backoff of " + delay + "ms.");
} else {
Logger.info(LOG_TAG, "Not syncing: must wait another " + delay + "ms.");
long remainingSeconds = delay / 1000;
syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
return;
}
SyncAccountParameters params;
try {
params = SyncAccounts.blockingFromAndroidAccountV0(mContext, AccountManager.get(mContext), this.localAccount);
} catch (Exception e) {
// Updates syncResult and (harmlessly) calls notifyMonitor().
processException(null, e);
return;
}
// TODO: don't clear the auth token unless we have a sync error.
Logger.debug(LOG_TAG, "Got onPerformSync. Extras bundle is " + extras);
// TODO: don't always invalidate; use getShouldInvalidateAuthToken.
// However, this fixes Bug 716815, so it'll do for now.
Logger.trace(LOG_TAG, "Invalidating auth token.");
invalidateAuthToken(account);
// params and the following fields are non-null at this point.
final String username = params.username; // Encoded with Utils.usernameFromAccount.
final String password = params.password;
final String serverURL = params.serverURL;
final String syncKey = params.syncKey;
final AtomicBoolean setNextSync = new AtomicBoolean(true);
final SyncAdapter self = this;
final AccountManagerCallback<Bundle> callback = new AccountManagerCallback<Bundle>() {
final Runnable runnable = new Runnable() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
public void run() {
Logger.trace(LOG_TAG, "AccountManagerCallback invoked.");
// TODO: N.B.: Future must not be used on the main thread.
try {
Bundle bundle = future.getResult(60L, TimeUnit.SECONDS);
if (bundle.containsKey("KEY_INTENT")) {
Logger.warn(LOG_TAG, "KEY_INTENT included in AccountManagerFuture bundle. Problem?");
}
String username = bundle.getString(Constants.OPTION_USERNAME);
String syncKey = bundle.getString(Constants.OPTION_SYNCKEY);
String serverURL = bundle.getString(Constants.OPTION_SERVER);
String password = bundle.getString(AccountManager.KEY_AUTHTOKEN);
Logger.info(LOG_TAG, "Syncing account named " + account.name +
" for authority " + authority + ".");
// We dump this information right away to help with debugging.
Logger.debug(LOG_TAG, "Username: " + username);
Logger.debug(LOG_TAG, "Server: " + serverURL);
Logger.debug(LOG_TAG, "Password? " + (password != null));
Logger.debug(LOG_TAG, "Key? " + (syncKey != null));
if (password == null &&
username == null &&
syncKey == null &&
serverURL == null) {
// Totally blank. Most likely the user has two copies of Firefox
// installed, and something is misbehaving.
// Disable this account.
Logger.error(LOG_TAG, "No credentials attached to account. Aborting sync.");
try {
SyncAccounts.setSyncAutomatically(account, false);
} catch (Exception e) {
Logger.error(LOG_TAG, "Unable to disable account " + account.name + " for " + authority + ".", e);
}
syncResult.stats.numAuthExceptions++;
localAccount = null;
notifyMonitor();
return;
}
// Now catch the individual cases.
if (password == null) {
Logger.error(LOG_TAG, "No password: aborting sync.");
syncResult.stats.numAuthExceptions++;
notifyMonitor();
return;
}
if (syncKey == null) {
Logger.error(LOG_TAG, "No Sync Key: aborting sync.");
syncResult.stats.numAuthExceptions++;
notifyMonitor();
return;
if (Logger.LOG_PERSONAL_INFORMATION) {
Logger.debug(LOG_TAG, "Password: " + password);
Logger.debug(LOG_TAG, "Sync key: " + syncKey);
} else {
Logger.debug(LOG_TAG, "Password? " + (password != null));
Logger.debug(LOG_TAG, "Sync key? " + (syncKey != null));
}
// Support multiple accounts by mapping each server/account pair to a branch of the
// shared preferences space.
String prefsPath = Utils.getPrefsPath(username, serverURL);
final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
final String profile = Constants.DEFAULT_PROFILE;
final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
self.accountSharedPreferences = Utils.getSharedPreferences(mContext, product, username, serverURL, profile, version);
Logger.info(LOG_TAG,
"Client is named '" + getClientName() + "'" +
", has client guid " + getAccountGUID() +
", and has " + getClientsCount() + " clients.");
thisSyncIsForced = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
long delay = delayMilliseconds();
if (delay > 0) {
if (thisSyncIsForced) {
Logger.info(LOG_TAG, "Forced sync: overruling remaining backoff of " + delay + "ms.");
} else {
Logger.info(LOG_TAG, "Not syncing: must wait another " + delay + "ms.");
long remainingSeconds = delay / 1000;
syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
setNextSync.set(false);
self.notifyMonitor();
return;
}
}
final String prefsPath = Utils.getPrefsPath(product, username, serverURL, profile, version);
self.performSync(account, extras, authority, provider, syncResult,
username, password, prefsPath, serverURL, syncKey);
} catch (Exception e) {
self.handleException(e, syncResult);
self.processException(null, e);
notifyMonitor();
return;
}
}
};
final Handler handler = null;
final Runnable fetchAuthToken = new Runnable() {
@Override
public void run() {
getAuthToken(account, callback, handler);
}
};
synchronized (syncMonitor) {
// Perform the work in a new thread from within this synchronized block,
// which allows us to be waiting on the monitor before the callback can
// notify us in a failure case. Oh, concurrent programming.
new Thread(fetchAuthToken).start();
new Thread(runnable).start();
// Start our stale connection monitor thread.
ConnectionMonitorThread stale = new ConnectionMonitorThread();
@ -391,10 +364,13 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
Logger.trace(LOG_TAG, "Waiting on sync monitor.");
try {
syncMonitor.wait();
long interval = getSyncInterval();
long next = System.currentTimeMillis() + interval;
Logger.info(LOG_TAG, "Setting minimum next sync time to " + next + " (" + interval + "ms from now).");
extendEarliestNextSync(next);
if (setNextSync.get()) {
long interval = getSyncInterval();
long next = System.currentTimeMillis() + interval;
Logger.info(LOG_TAG, "Setting minimum next sync time to " + next + " (" + interval + "ms from now).");
extendEarliestNextSync(next);
}
Logger.info(LOG_TAG, "Sync took " + Utils.formatDuration(syncStartTimestamp, System.currentTimeMillis()) + ".");
} catch (InterruptedException e) {
Logger.warn(LOG_TAG, "Waiting on sync monitor interrupted.", e);
@ -403,7 +379,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
stale.shutdown();
}
}
}
}
public int getSyncInterval() {
// Must have been a problem that means we can't access the Account.
@ -419,10 +395,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
return MULTI_DEVICE_INTERVAL_MILLISECONDS;
}
/**
* Now that we have a sync key and password, go ahead and do the work.
* @param prefsPath TODO
* @throws NoSuchAlgorithmException
* @throws IllegalArgumentException
* @throws SyncConfigurationException
@ -457,7 +431,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
*/
try {
// Constructor can throw on nulls, which should not happen -- but let's be safe.
final SyncAccountParameters params = new SyncAccountParameters(mContext, mAccountManager,
final SyncAccountParameters params = new SyncAccountParameters(mContext, null,
account.name, // Un-encoded, like "test@mozilla.com".
syncKey,
password,
@ -503,9 +477,8 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
// Implementing GlobalSession callbacks.
@Override
public void handleError(GlobalSession globalSession, Exception ex) {
Logger.info(LOG_TAG, "GlobalSession indicated error. Flagging auth token as invalid, just in case.");
setShouldInvalidateAuthToken();
this.updateStats(globalSession, ex);
Logger.info(LOG_TAG, "GlobalSession indicated error.");
this.processException(globalSession, ex);
notifyMonitor();
}
@ -515,22 +488,6 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
notifyMonitor();
}
/**
* Introspect the exception, incrementing the appropriate stat counters.
* TODO: increment number of inserts, deletes, conflicts.
*
* @param globalSession
* @param ex
*/
private void updateStats(GlobalSession globalSession,
Exception ex) {
if (ex instanceof SyncException) {
((SyncException) ex).updateStats(globalSession, syncResult);
}
// TODO: non-SyncExceptions.
// TODO: wouldn't it be nice to update stats for *every* exception we get?
}
@Override
public void handleSuccess(GlobalSession globalSession) {
Logger.info(LOG_TAG, "GlobalSession indicated success.");
@ -547,37 +504,28 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
@Override
public synchronized String getAccountGUID() {
String accountGUID = mAccountManager.getUserData(localAccount, Constants.ACCOUNT_GUID);
String accountGUID = accountSharedPreferences.getString(SyncConfiguration.PREF_ACCOUNT_GUID, null);
if (accountGUID == null) {
Logger.debug(LOG_TAG, "Account GUID was null. Creating a new one.");
accountGUID = Utils.generateGuid();
setAccountGUID(mAccountManager, localAccount, accountGUID);
accountSharedPreferences.edit().putString(SyncConfiguration.PREF_ACCOUNT_GUID, accountGUID).commit();
}
return accountGUID;
}
public static void setAccountGUID(AccountManager accountManager, Account account, String accountGUID) {
accountManager.setUserData(account, Constants.ACCOUNT_GUID, accountGUID);
}
@Override
public synchronized String getClientName() {
String clientName = mAccountManager.getUserData(localAccount, Constants.CLIENT_NAME);
String clientName = accountSharedPreferences.getString(SyncConfiguration.PREF_CLIENT_NAME, null);
if (clientName == null) {
clientName = GlobalConstants.PRODUCT_NAME + " on " + android.os.Build.MODEL;
setClientName(mAccountManager, localAccount, clientName);
accountSharedPreferences.edit().putString(SyncConfiguration.PREF_CLIENT_NAME, clientName).commit();
}
return clientName;
}
public static void setClientName(AccountManager accountManager, Account account, String clientName) {
accountManager.setUserData(account, Constants.CLIENT_NAME, clientName);
}
@Override
public synchronized void setClientsCount(int clientsCount) {
mAccountManager.setUserData(localAccount, Constants.NUM_CLIENTS,
Integer.toString(clientsCount));
accountSharedPreferences.edit().putLong(SyncConfiguration.PREF_NUM_CLIENTS, (long) clientsCount).commit();
}
@Override
@ -587,23 +535,16 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
@Override
public synchronized int getClientsCount() {
String clientsCount = mAccountManager.getUserData(localAccount, Constants.NUM_CLIENTS);
if (clientsCount == null) {
clientsCount = "0";
mAccountManager.setUserData(localAccount, Constants.NUM_CLIENTS, clientsCount);
}
return Integer.parseInt(clientsCount);
return (int) accountSharedPreferences.getLong(SyncConfiguration.PREF_NUM_CLIENTS, 0);
}
public synchronized boolean getClusterURLIsStale() {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
return sharedPreferences.getBoolean(PREFS_CLUSTER_URL_IS_STALE, false);
return accountSharedPreferences.getBoolean(SyncConfiguration.PREF_CLUSTER_URL_IS_STALE, false);
}
public synchronized void setClusterURLIsStale(boolean clusterURLIsStale) {
SharedPreferences sharedPreferences = getGlobalPrefs(mContext);
Editor edit = sharedPreferences.edit();
edit.putBoolean(PREFS_CLUSTER_URL_IS_STALE, clusterURLIsStale);
Editor edit = accountSharedPreferences.edit();
edit.putBoolean(SyncConfiguration.PREF_CLUSTER_URL_IS_STALE, clusterURLIsStale);
edit.commit();
}
@ -631,7 +572,7 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
@Override
public void informUpgradeRequiredResponse(final GlobalSession session) {
final AccountManager manager = mAccountManager;
final AccountManager manager = AccountManager.get(mContext);
final Account toDisable = localAccount;
if (toDisable == null || manager == null) {
Logger.warn(LOG_TAG, "Attempting to disable account, but null found.");

Просмотреть файл

@ -3,6 +3,8 @@ sync/CollectionKeys.java
sync/CommandProcessor.java
sync/CommandRunner.java
sync/config/AccountPickler.java
sync/config/ConfigurationMigrator.java
sync/CredentialException.java
sync/CredentialsSource.java
sync/crypto/CryptoException.java
sync/crypto/CryptoInfo.java