Bug 769745 - Persist Android Sync account settings to disk on each sync and re-create account when checking if Sync is set up. r=rnewman

This commit is contained in:
Nick Alexander 2012-07-06 18:42:54 -07:00
parent ac853e9899
commit e27ca6cd41
8 changed files with 408 additions and 23 deletions

File diff suppressed because one or more lines are too long

View File

@ -4,6 +4,10 @@
package org.mozilla.gecko.sync;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.math.BigInteger;
@ -441,4 +445,50 @@ public class Utils {
bundle.putString(Constants.EXTRAS_KEY_STAGES_TO_SKIP, o.toJSONString());
}
}
/**
* Read contents of file as a string.
*
* @param context Android context.
* @param filename name of file to read; must not be null.
* @return <code>String</code> instance.
*/
public static String readFile(final Context context, final String filename) {
if (filename == null) {
throw new IllegalArgumentException("Passed null filename in readFile.");
}
FileInputStream fis = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
fis = context.openFileInput(filename);
isr = new InputStreamReader(fis);
br = new BufferedReader(isr);
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb.toString();
} catch (Exception e) {
return null;
} finally {
if (isr != null) {
try {
isr.close();
} catch (IOException e) {
// Ignore.
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// Ignore.
}
}
}
}
}

View File

@ -0,0 +1,159 @@
/* 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.io.FileOutputStream;
import java.io.PrintStream;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.Utils;
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.content.Context;
/**
* Bug 768102: Android deletes Account objects when the Authenticator that owns
* the Account disappears. This happens when an App is installed to the SD card
* and the SD card is un-mounted.
* <p>
* Bug 769745: We work around this by pickling the current Sync account data
* every sync and un-pickling when we check if Sync accounts exist (called from
* Fennec).
*/
public class AccountPickler {
public static final String LOG_TAG = "AccountPickler";
public static final long VERSION = 1;
/**
* Remove Sync account persisted to disk.
*
* @param context Android context.
* @param filename name of persisted pickle file; must not contain path separators.
* @return <code>true</code> if given pickle existed and was successfully deleted.
*/
public static boolean deletePickle(final Context context, final String filename) {
return context.deleteFile(filename);
}
/**
* Persist Sync account to disk as a JSON object.
* <p>
* JSON object has keys:
* <ul>
* <li><code>Constants.JSON_KEY_ACCOUNT</code>: the Sync account's un-encoded username,
* like "test@mozilla.com".</li>
*
* <li><code>Constants.JSON_KEY_PASSWORD</code>: the Sync account's password;</li>
*
* <li><code>Constants.JSON_KEY_SERVER</code>: the Sync account's server;</li>
*
* <li><code>Constants.JSON_KEY_SYNCKEY</code>: the Sync account's sync key;</li>
*
* <li><code>Constants.JSON_KEY_CLUSTER</code>: the Sync account's cluster (may be null);</li>
*
* <li><code>Constants.JSON_KEY_CLIENT_NAME</code>: the Sync account's client name (may be null);</li>
*
* <li><code>Constants.JSON_KEY_CLIENT_GUID</code>: the Sync account's client GUID (may be null);</li>
*
* <li><code>Constants.JSON_KEY_SYNC_AUTOMATICALLY</code>: true if the Android Account is syncing automically;</li>
*
* <li><code>Constants.JSON_KEY_VERSION</code>: version of this file;</li>
*
* <li><code>Constants.JSON_KEY_TIMESTAMP</code>: when this file was written.</li>
* </ul>
*
*
* @param context Android context.
* @param filename name of file to persist to; must not contain path separators.
* @param params the Sync account's parameters.
* @param syncAutomatically whether the Android Account object is syncing automatically.
*/
public static void pickle(final Context context, final String filename,
final SyncAccountParameters params, final boolean syncAutomatically) {
final ExtendedJSONObject o = params.asJSON();
o.put(Constants.JSON_KEY_SYNC_AUTOMATICALLY, new Boolean(syncAutomatically));
o.put(Constants.JSON_KEY_VERSION, new Long(VERSION));
o.put(Constants.JSON_KEY_TIMESTAMP, new Long(System.currentTimeMillis()));
PrintStream ps = null;
try {
final FileOutputStream fos = context.openFileOutput(filename, Context.MODE_PRIVATE);
ps = new PrintStream(fos);
ps.print(o.toJSONString());
Logger.debug(LOG_TAG, "Persisted " + o.keySet().size() + " account settings to " + filename + ".");
} catch (Exception e) {
Logger.warn(LOG_TAG, "Caught exception persisting account settings to " + filename + "; ignoring.", e);
} finally {
if (ps != null) {
ps.close();
}
}
}
/**
* Create Android account from saved JSON object.
*
* @param context
* Android context.
* @param filename
* name of file to read from; must not contain path separators.
* @return created Android account, or null on error.
*/
public static Account unpickle(final Context context, final String filename) {
final String jsonString = Utils.readFile(context, filename);
if (jsonString == null) {
Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
return null;
}
ExtendedJSONObject json = null;
try {
json = ExtendedJSONObject.parseJSONObject(jsonString);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
return null;
}
SyncAccountParameters params = null;
try {
// Null checking of inputs is done in constructor.
params = new SyncAccountParameters(context, null, json);
} catch (IllegalArgumentException e) {
Logger.warn(LOG_TAG, "Un-pickled data included null username, password, or serverURL; aborting.", e);
return null;
}
// Default to syncing automatically.
boolean syncAutomatically = true;
if (json.containsKey(Constants.JSON_KEY_SYNC_AUTOMATICALLY)) {
if ((new Boolean(false)).equals(json.get(Constants.JSON_KEY_SYNC_AUTOMATICALLY))) {
syncAutomatically = false;
}
}
final Account account = SyncAccounts.createSyncAccountPreservingExistingPreferences(params, syncAutomatically);
if (account == null) {
Logger.warn(LOG_TAG, "Failed to add Android Account; aborting.");
return null;
}
Integer version = json.getIntegerSafely(Constants.JSON_KEY_VERSION);
Integer timestamp = json.getIntegerSafely(Constants.JSON_KEY_TIMESTAMP);
if (version == null || timestamp == null) {
Logger.warn(LOG_TAG, "Did not find version or timestamp in pickle file; ignoring.");
version = new Integer(-1);
timestamp = new Integer(-1);
}
Logger.info(LOG_TAG, "Un-pickled Android account named " + params.username + " (version " + version + ", pickled at " + timestamp + ").");
return account;
}
}

View File

@ -18,6 +18,13 @@ public class Constants {
public static final String NUM_CLIENTS = "account.numClients";
public static final String DATA_ENABLE_ON_UPGRADE = "data.enableOnUpgrade";
/**
* Name of file to pickle current account preferences to each sync.
* <p>
* Must not contain path separators!
*/
public static final String ACCOUNT_PICKLE_FILENAME = "sync.account.json";
/**
* Key in sync extras bundle specifying stages to sync this sync session.
* <p>
@ -73,6 +80,11 @@ public class Constants {
public static final String JSON_KEY_PASSWORD = "password";
public static final String JSON_KEY_SYNCKEY = "synckey";
public static final String JSON_KEY_SERVER = "serverURL";
public static final String JSON_KEY_CLUSTER = "clusterURL";
public static final String JSON_KEY_CLIENT_NAME = "clientName";
public static final String JSON_KEY_CLIENT_GUID = "clientGUID";
public static final String JSON_KEY_SYNC_AUTOMATICALLY = "syncAutomatically";
public static final String JSON_KEY_TIMESTAMP = "timestamp";
public static final String CRYPTO_KEY_GR1 = "gr1";
public static final String CRYPTO_KEY_GR2 = "gr2";

View File

@ -4,10 +4,14 @@
package org.mozilla.gecko.sync.setup;
import java.io.File;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
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;
@ -38,12 +42,28 @@ public class SyncAccounts {
public final static String DEFAULT_SERVER = "https://auth.services.mozilla.com/";
/**
* Returns true if a Sync account is set up.
* 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
* account, try to un-pickle it and create the corresponding Sync account.
* <p>
* Do not call this method from the main thread.
*/
public static boolean syncAccountsExist(Context c) {
return AccountManager.get(c).getAccountsByType(Constants.ACCOUNTTYPE_SYNC).length > 0;
final boolean accountsExist = AccountManager.get(c).getAccountsByType(Constants.ACCOUNTTYPE_SYNC).length > 0;
if (accountsExist) {
return true;
}
final File file = c.getFileStreamPath(Constants.ACCOUNT_PICKLE_FILENAME);
if (!file.exists()) {
return false;
}
// There is a small race window here: if the user creates a new Sync account
// between our checks, this could erroneously report that no Sync accounts
// exist.
final Account account = AccountPickler.unpickle(c, Constants.ACCOUNT_PICKLE_FILENAME);
return (account != null);
}
/**
@ -134,6 +154,29 @@ public class SyncAccounts {
String username, String syncKey, String password, String serverURL) {
this(context, accountManager, username, syncKey, password, serverURL, null, null, null);
}
public SyncAccountParameters(final Context context, final AccountManager accountManager, final ExtendedJSONObject o) {
this(context, accountManager,
o.getString(Constants.JSON_KEY_ACCOUNT),
o.getString(Constants.JSON_KEY_SYNCKEY),
o.getString(Constants.JSON_KEY_PASSWORD),
o.getString(Constants.JSON_KEY_SERVER),
o.getString(Constants.JSON_KEY_CLUSTER),
o.getString(Constants.JSON_KEY_CLIENT_NAME),
o.getString(Constants.JSON_KEY_CLIENT_GUID));
}
public ExtendedJSONObject asJSON() {
final ExtendedJSONObject o = new ExtendedJSONObject();
o.put(Constants.JSON_KEY_ACCOUNT, username);
o.put(Constants.JSON_KEY_PASSWORD, password);
o.put(Constants.JSON_KEY_SERVER, serverURL);
o.put(Constants.JSON_KEY_SYNCKEY, syncKey);
o.put(Constants.JSON_KEY_CLUSTER, clusterURL);
o.put(Constants.JSON_KEY_CLIENT_NAME, clientName);
o.put(Constants.JSON_KEY_CLIENT_GUID, clientGuid);
return o;
}
}
/**
@ -145,11 +188,21 @@ public class SyncAccounts {
* occurred and the account could not be added.
*/
public static class CreateSyncAccountTask extends AsyncTask<SyncAccountParameters, Void, Account> {
protected final boolean syncAutomatically;
public CreateSyncAccountTask() {
this(true);
}
public CreateSyncAccountTask(final boolean syncAutomically) {
this.syncAutomatically = syncAutomically;
}
@Override
protected Account doInBackground(SyncAccountParameters... params) {
SyncAccountParameters syncAccount = params[0];
try {
return createSyncAccount(syncAccount);
return createSyncAccount(syncAccount, syncAutomatically);
} catch (Exception e) {
Log.e(Logger.GLOBAL_LOG_TAG, "Unable to create account.", e);
return null;
@ -158,16 +211,66 @@ public class SyncAccounts {
}
/**
* Create a sync account.
* Create a sync account, clearing any existing preferences, and set it to
* sync automatically.
* <p>
* Do not call this method from the main thread.
*
* @param syncAccount
* The parameters of the account to be created.
* @return The created <code>Account</code>, or null if an error occurred and
* the account could not be added.
* parameters of the account to be created.
* @return created <code>Account</code>, or null if an error occurred and the
* account could not be added.
*/
public static Account createSyncAccount(SyncAccountParameters syncAccount) {
return createSyncAccount(syncAccount, true, true);
}
/**
* Create a sync account, clearing any existing preferences.
* <p>
* Do not call this method from the main thread.
* <p>
* Intended for testing; use
* <code>createSyncAccount(SyncAccountParameters)</code> instead.
*
* @param syncAccount
* parameters of the account to be created.
* @param syncAutomatically
* whether to start syncing this Account automatically (
* <code>false</code> for test accounts).
* @return created Android <code>Account</code>, or null if an error occurred
* and the account could not be added.
*/
public static Account createSyncAccount(SyncAccountParameters syncAccount,
boolean syncAutomatically) {
return createSyncAccount(syncAccount, syncAutomatically, true);
}
public static Account createSyncAccountPreservingExistingPreferences(SyncAccountParameters syncAccount,
boolean syncAutomatically) {
return createSyncAccount(syncAccount, syncAutomatically, false);
}
/**
* Create a sync account.
* <p>
* Do not call this method from the main thread.
* <p>
* Intended for testing; use
* <code>createSyncAccount(SyncAccountParameters)</code> instead.
*
* @param syncAccount
* parameters of the account to be created.
* @param syncAutomatically
* whether to start syncing this Account automatically (
* <code>false</code> for test accounts).
* @param clearPreferences
* <code>true</code> to clear existing preferences before creating.
* @return created Android <code>Account</code>, or null if an error occurred
* and the account could not be added.
*/
protected static Account createSyncAccount(SyncAccountParameters syncAccount,
boolean syncAutomatically, boolean clearPreferences) {
final Context context = syncAccount.context;
final AccountManager accountManager = (syncAccount.accountManager == null) ?
AccountManager.get(syncAccount.context) : syncAccount.accountManager;
@ -212,7 +315,10 @@ public class SyncAccounts {
}
Logger.debug(LOG_TAG, "Account " + account + " added successfully.");
setSyncAutomatically(account);
setSyncAutomatically(account, syncAutomatically);
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)
@ -221,12 +327,17 @@ public class SyncAccounts {
// 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.
Logger.info(LOG_TAG, "Clearing global prefs.");
SyncAdapter.purgeGlobalPrefs(context);
if (clearPreferences) {
Logger.info(LOG_TAG, "Clearing global prefs.");
SyncAdapter.purgeGlobalPrefs(context);
}
try {
Logger.info(LOG_TAG, "Clearing preferences path " + Utils.getPrefsPath(username, serverURL) + " for this account.");
SharedPreferences.Editor editor = Utils.getSharedPreferences(context, username, serverURL).edit().clear();
SharedPreferences.Editor editor = Utils.getSharedPreferences(context, username, serverURL).edit();
if (clearPreferences) {
Logger.info(LOG_TAG, "Clearing preferences path " + Utils.getPrefsPath(username, serverURL) + " for this account.");
editor.clear();
}
if (syncAccount.clusterURL != null) {
editor.putString(SyncConfiguration.PREF_CLUSTER_URL, syncAccount.clusterURL);
}
@ -253,11 +364,6 @@ public class SyncAccounts {
ContentResolver.setSyncAutomatically(account, authority, syncAutomatically);
}
public static void setSyncAutomatically(Account account) {
setSyncAutomatically(account, true);
setIsSyncable(account, true);
}
public static Intent openSyncSettings(Context context) {
Intent intent = null;

View File

@ -9,6 +9,7 @@ import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.AccountPickler;
import org.mozilla.gecko.sync.setup.activities.SetupSyncActivity;
import android.accounts.AbstractAccountAuthenticator;
@ -160,5 +161,35 @@ public class SyncAuthenticatorService extends Service {
Logger.debug(LOG_TAG, "updateCredentials()");
return null;
}
/**
* Bug 769745: persist pickled Sync account settings so that we can unpickle
* after Fennec is moved to the SD card.
* <p>
* This is <b>not</b> called when an Android Account is blown away due to the
* SD card being unmounted.
* <p>
* This is a terrible hack, but it's better than the catching the generic
* "accounts changed" broadcast intent and trying to figure out whether our
* Account disappeared.
*/
@Override
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account) throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result != null &&
result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) &&
!result.containsKey(AccountManager.KEY_INTENT)) {
final boolean removalAllowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (removalAllowed) {
Logger.info(LOG_TAG, "Account named " + account.name + " being removed; " +
"deleting saved pickle file '" + Constants.ACCOUNT_PICKLE_FILENAME + "'.");
AccountPickler.deletePickle(mContext, Constants.ACCOUNT_PICKLE_FILENAME);
}
}
return result;
}
}
}

View File

@ -21,12 +21,15 @@ import org.mozilla.gecko.sync.SyncConfigurationException;
import org.mozilla.gecko.sync.SyncException;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.config.AccountPickler;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.net.ConnectionMonitorThread;
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.GlobalSyncStage.Stage;
import android.accounts.Account;
@ -341,13 +344,11 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
return;
}
KeyBundle keyBundle = new KeyBundle(username, syncKey);
// Support multiple accounts by mapping each server/account pair to a branch of the
// shared preferences space.
String prefsPath = Utils.getPrefsPath(username, serverURL);
self.performSync(account, extras, authority, provider, syncResult,
username, password, prefsPath, serverURL, keyBundle);
username, password, prefsPath, serverURL, syncKey);
} catch (Exception e) {
self.handleException(e, syncResult);
return;
@ -413,22 +414,47 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
* @throws NonObjectJSONException
* @throws ParseException
* @throws IOException
* @throws CryptoException
*/
protected void performSync(Account account, Bundle extras, String authority,
ContentProviderClient provider,
SyncResult syncResult,
String username, String password,
String prefsPath,
String serverURL, KeyBundle keyBundle)
String serverURL,
String syncKey)
throws NoSuchAlgorithmException,
SyncConfigurationException,
IllegalArgumentException,
AlreadySyncingException,
IOException, ParseException,
NonObjectJSONException {
NonObjectJSONException, CryptoException {
Logger.trace(LOG_TAG, "Performing sync.");
/**
* Bug 769745: pickle Sync account parameters to JSON file. Un-pickle in
* <code>SyncAccounts.syncAccountsExist</code>.
*/
try {
// Constructor can throw on nulls, which should not happen -- but let's be safe.
final SyncAccountParameters params = new SyncAccountParameters(mContext, mAccountManager,
account.name, // Un-encoded, like "test@mozilla.com".
syncKey,
password,
serverURL,
null, // We'll re-fetch cluster URL; not great, but not harmful.
getClientName(),
getAccountGUID());
final boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority);
AccountPickler.pickle(mContext, Constants.ACCOUNT_PICKLE_FILENAME, params, syncAutomatically);
} catch (IllegalArgumentException e) {
// Do nothing.
}
// TODO: default serverURL.
final KeyBundle keyBundle = new KeyBundle(username, syncKey);
GlobalSession globalSession = new GlobalSession(SyncConfiguration.DEFAULT_USER_API,
serverURL, username, password, prefsPath,
keyBundle, this, this.mContext, extras, this);

View File

@ -2,6 +2,7 @@ sync/AlreadySyncingException.java
sync/CollectionKeys.java
sync/CommandProcessor.java
sync/CommandRunner.java
sync/config/AccountPickler.java
sync/CredentialsSource.java
sync/crypto/CryptoException.java
sync/crypto/CryptoInfo.java