Bug 739629 - Expose safe account creation API for profile migration. r=rnewman

This commit is contained in:
Nick Alexander 2012-04-13 19:41:40 -07:00
parent 5810b206c9
commit b8ad793dc1
7 changed files with 239 additions and 72 deletions

View File

@ -190,10 +190,7 @@ public class GlobalSession implements CredentialsSource, PrefsSource, HttpRespon
// TODO: more stages.
stages.put(Stage.syncTabs, new FennecTabsServerSyncStage());
// Disable until Bug 744816 is fixed.
// stages.put(Stage.syncPasswords, new PasswordsServerSyncStage());
stages.put(Stage.syncPasswords, new PasswordsServerSyncStage());
stages.put(Stage.syncBookmarks, new AndroidBrowserBookmarksServerSyncStage());
stages.put(Stage.syncHistory, new AndroidBrowserHistoryServerSyncStage());
stages.put(Stage.syncFormHistory, new FormHistoryServerSyncStage());

View File

@ -192,6 +192,8 @@ public class SyncConfiguration implements CredentialsSource {
public PrefsSource prefsSource;
public static final String CLIENT_RECORD_TIMESTAMP = "serverClientRecordTimestamp";
public static final String PREF_CLUSTER_URL = "clusterURL";
public static final String PREF_SYNC_ID = "syncID";
/**
* Create a new SyncConfiguration instance. Pass in a PrefsSource to
@ -220,8 +222,8 @@ public class SyncConfiguration implements CredentialsSource {
public void loadFromPrefs(SharedPreferences prefs) {
if (prefs.contains("clusterURL")) {
String u = prefs.getString("clusterURL", null);
if (prefs.contains(PREF_CLUSTER_URL)) {
String u = prefs.getString(PREF_CLUSTER_URL, null);
try {
clusterURL = new URI(u);
Logger.info(LOG_TAG, "Set clusterURL from bundle: " + u);
@ -229,8 +231,8 @@ public class SyncConfiguration implements CredentialsSource {
Logger.warn(LOG_TAG, "Ignoring bundle clusterURL (" + u + "): invalid URI.", e);
}
}
if (prefs.contains("syncID")) {
syncID = prefs.getString("syncID", null);
if (prefs.contains(PREF_SYNC_ID)) {
syncID = prefs.getString(PREF_SYNC_ID, null);
Logger.info(LOG_TAG, "Set syncID from bundle: " + syncID);
}
// We don't set crypto/keys here because we need the syncKeyBundle to decrypt the JSON
@ -245,12 +247,12 @@ public class SyncConfiguration implements CredentialsSource {
public void persistToPrefs(SharedPreferences prefs) {
Editor edit = prefs.edit();
if (clusterURL == null) {
edit.remove("clusterURL");
edit.remove(PREF_CLUSTER_URL);
} else {
edit.putString("clusterURL", clusterURL.toASCIIString());
edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString());
}
if (syncID != null) {
edit.putString("syncID", syncID);
edit.putString(PREF_SYNC_ID, syncID);
}
edit.commit();
// TODO: keys.
@ -339,7 +341,7 @@ public class SyncConfiguration implements CredentialsSource {
clusterURL = u;
if (shouldPersist) {
Editor edit = prefs.edit();
edit.putString("clusterURL", clusterURL.toASCIIString());
edit.putString(PREF_CLUSTER_URL, clusterURL.toASCIIString());
edit.commit();
}
}

View File

@ -6,13 +6,16 @@ package org.mozilla.gecko.sync.setup;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
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.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
@ -20,27 +23,28 @@ import android.util.Log;
/**
* This class contains utilities that are of use to Fennec
* and Sync setup activities.
*
* <p>
* Do not break these APIs without correcting upstream code!
*/
public class SyncAccounts {
private final static String DEFAULT_SERVER = "https://auth.services.mozilla.com/";
public final static String DEFAULT_SERVER = "https://auth.services.mozilla.com/";
private static final String LOG_TAG = "SyncAccounts";
private static final String GLOBAL_LOG_TAG = "FxSync";
/**
* Returns true if a Sync account is set up.
*
* <p>
* Do not call this method from the main thread.
*/
public static boolean syncAccountsExist(Context c) {
return AccountManager.get(c).getAccountsByType("org.mozilla.firefox_sync").length > 0;
return AccountManager.get(c).getAccountsByType(Constants.ACCOUNTTYPE_SYNC).length > 0;
}
/**
* This class provides background-thread abstracted access to whether a
* Firefox Sync account has been set up on this device.
*
* <p>
* Subclass this task and override `onPostExecute` to act on the result.
*/
public static class AccountsExistTask extends AsyncTask<Context, Void, Boolean> {
@ -51,53 +55,160 @@ public class SyncAccounts {
}
}
public static Intent createAccount(Context context,
AccountManager accountManager,
String username,
String syncKey,
String password,
String serverURL) {
/**
* This class encapsulates the parameters needed to create a new Firefox Sync
* account.
*/
public static class SyncAccountParameters {
public final Context context;
public final AccountManager accountManager;
public final String username; // services.sync.account
public final String syncKey; // in password manager: "chrome://weave (Mozilla Services Password)"
public final String password; // in password manager: "chrome://weave (Mozilla Services Encryption Passphrase)"
public final String serverURL; // services.sync.serverURL
public final String clusterURL; // services.sync.clusterURL
public final String clientName; // services.sync.client.name
public final String clientGuid; // services.sync.client.GUID
/**
* Encapsulate the parameters needed to create a new Firefox Sync account.
*
* @param context
* the current <code>Context</code>; cannot be null.
* @param accountManager
* an <code>AccountManager</code> instance to use; if null, get it
* from <code>context</code>.
* @param username
* the desired username; cannot be null.
* @param syncKey
* the desired sync key; cannot be null.
* @param password
* the desired password; cannot be null.
* @param serverURL
* the server URL to use; if null, use the default.
* @param clusterURL
* the cluster URL to use; if null, a fresh cluster URL will be
* retrieved from the server during the next sync.
* @param clientName
* the client name; if null, a fresh client record will be uploaded
* to the server during the next sync.
* @param clientGuid
* the client GUID; if null, a fresh client record will be uploaded
* to the server during the next sync.
*/
public SyncAccountParameters(Context context, AccountManager accountManager,
String username, String syncKey, String password,
String serverURL, String clusterURL,
String clientName, String clientGuid) {
if (context == null) {
throw new IllegalArgumentException("Null context passed to SyncAccountParameters constructor.");
}
if (username == null) {
throw new IllegalArgumentException("Null username passed to SyncAccountParameters constructor.");
}
if (syncKey == null) {
throw new IllegalArgumentException("Null syncKey passed to SyncAccountParameters constructor.");
}
if (password == null) {
throw new IllegalArgumentException("Null password passed to SyncAccountParameters constructor.");
}
this.context = context;
this.accountManager = accountManager;
this.username = username;
this.syncKey = syncKey;
this.password = password;
this.serverURL = serverURL;
this.clusterURL = clusterURL;
this.clientName = clientName;
this.clientGuid = clientGuid;
}
public SyncAccountParameters(Context context, AccountManager accountManager,
String username, String syncKey, String password, String serverURL) {
this(context, accountManager, username, syncKey, password, serverURL, null, null, null);
}
}
/**
* This class provides background-thread abstracted access to creating a
* Firefox Sync account.
* <p>
* Subclass this task and override `onPostExecute` to act on the result. The
* <code>Result</code> (of type <code>Account</code>) is null if an error
* occurred and the account could not be added.
*/
public static class CreateSyncAccountTask extends AsyncTask<SyncAccountParameters, Void, Account> {
@Override
protected Account doInBackground(SyncAccountParameters... params) {
SyncAccountParameters syncAccount = params[0];
try {
return createSyncAccount(syncAccount);
} catch (Exception e) {
Log.e(GLOBAL_LOG_TAG, "Unable to create account.", e);
return null;
}
}
}
/**
* Create a sync account.
* <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.
*/
public static Account createSyncAccount(SyncAccountParameters syncAccount) {
final Context context = syncAccount.context;
final AccountManager accountManager = (syncAccount.accountManager == null) ?
AccountManager.get(syncAccount.context) : syncAccount.accountManager;
final String username = syncAccount.username;
final String syncKey = syncAccount.syncKey;
final String password = syncAccount.password;
final String serverURL = (syncAccount.serverURL == null) ?
DEFAULT_SERVER : syncAccount.serverURL;
Logger.debug(LOG_TAG, "Using account manager " + accountManager);
if (!RepoUtils.stringsEqual(syncAccount.serverURL, DEFAULT_SERVER)) {
Logger.info(LOG_TAG, "Setting explicit server URL: " + serverURL);
}
final Account account = new Account(username, Constants.ACCOUNTTYPE_SYNC);
final Bundle userbundle = new Bundle();
// Add sync key and server URL.
userbundle.putString(Constants.OPTION_SYNCKEY, syncKey);
if (serverURL != null) {
Logger.info(LOG_TAG, "Setting explicit server URL: " + serverURL);
userbundle.putString(Constants.OPTION_SERVER, serverURL);
} else {
userbundle.putString(Constants.OPTION_SERVER, DEFAULT_SERVER);
}
userbundle.putString(Constants.OPTION_SERVER, serverURL);
Logger.debug(LOG_TAG, "Adding account for " + Constants.ACCOUNTTYPE_SYNC);
boolean result = false;
try {
result = accountManager.addAccountExplicitly(account, password, userbundle);
} catch (SecurityException e) {
// We use Log rather than Logger here to avoid possibly hiding these errors.
final String message = e.getMessage();
if (message != null && (message.indexOf("is different than the authenticator's uid") > 0)) {
Log.wtf("FirefoxSync",
Log.wtf(GLOBAL_LOG_TAG,
"Unable to create account. " +
"If you have more than one version of " +
"Firefox/Beta/Aurora/Nightly/Fennec installed, that's why.",
e);
} else {
Log.e("FirefoxSync", "Unable to create account.", e);
Log.e(GLOBAL_LOG_TAG, "Unable to create account.", e);
}
}
Logger.debug(LOG_TAG, "Account: " + account + " added successfully? " + result);
if (!result) {
Logger.error(LOG_TAG, "Failed to add account!");
Logger.error(LOG_TAG, "Failed to add account " + account + "!");
return null;
}
Logger.debug(LOG_TAG, "Account " + account + " added successfully.");
// Set components to sync (default: all).
ContentResolver.setMasterSyncAutomatically(true);
String authority = BrowserContract.AUTHORITY;
Logger.debug(LOG_TAG, "Setting authority " + authority + " to sync automatically.");
ContentResolver.setSyncAutomatically(account, authority, true);
ContentResolver.setIsSyncable(account, authority, 1);
setSyncAutomatically(account);
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
@ -106,15 +217,33 @@ public class SyncAccounts {
// TODO: correctly implement Sync Options.
Logger.info(LOG_TAG, "Clearing preferences for this account.");
try {
Utils.getSharedPreferences(context, username, serverURL).edit().clear().commit();
SharedPreferences.Editor editor = Utils.getSharedPreferences(context, username, serverURL).edit().clear();
if (syncAccount.clusterURL != null) {
editor.putString(SyncConfiguration.PREF_CLUSTER_URL, syncAccount.clusterURL);
}
editor.commit();
} catch (Exception e) {
Logger.error(LOG_TAG, "Could not clear prefs path!", e);
}
return account;
}
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, username);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNTTYPE_SYNC);
intent.putExtra(AccountManager.KEY_AUTHTOKEN, Constants.ACCOUNTTYPE_SYNC);
return intent;
protected static void setSyncAutomatically(Account account) {
ContentResolver.setMasterSyncAutomatically(true);
String authority = BrowserContract.AUTHORITY;
Logger.debug(LOG_TAG, "Setting authority " + authority + " to sync automatically.");
ContentResolver.setSyncAutomatically(account, authority, true);
ContentResolver.setIsSyncable(account, authority, 1);
}
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;
}
Logger.debug(LOG_TAG, "Client name and guid not both non-null, so not setting client data.");
}
}

View File

@ -10,7 +10,9 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.InvalidSyncKeyException;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.content.Context;
@ -178,21 +180,34 @@ public class AccountActivity extends AccountAuthenticatorActivity {
private void authCallback() {
// Create and add account to AccountManager
// TODO: only allow one account to be added?
Log.d(LOG_TAG, "Using account manager " + mAccountManager);
final Intent intent = SyncAccounts.createAccount(mContext, mAccountManager,
username,
key, password, server);
final SyncAccountParameters syncAccount = new SyncAccountParameters(mContext, mAccountManager,
username, key, password, server);
final Account account = SyncAccounts.createSyncAccount(syncAccount);
final boolean result = (account != null);
final Intent intent = new Intent(); // The intent to return.
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, syncAccount.username);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNTTYPE_SYNC);
intent.putExtra(AccountManager.KEY_AUTHTOKEN, Constants.ACCOUNTTYPE_SYNC);
setAccountAuthenticatorResult(intent.getExtras());
// Testing out the authFailure case
// authFailure();
if (!result) {
// Failed to add account!
setResult(RESULT_CANCELED, intent);
runOnUiThread(new Runnable() {
@Override
public void run() {
authFailure();
}
});
return;
}
// TODO: Currently, we do not actually authenticate username/pass against
// Moz sync server.
// Successful authentication result
// Successfully added account.
setResult(RESULT_OK, intent);
runOnUiThread(new Runnable() {
@Override
public void run() {
@ -201,7 +216,6 @@ public class AccountActivity extends AccountAuthenticatorActivity {
});
}
@SuppressWarnings("unused")
private void authFailure() {
enableCredEntry(true);
Intent intent = new Intent(mContext, SetupFailureActivity.class);

View File

@ -14,6 +14,7 @@ import org.mozilla.gecko.sync.jpake.JPakeClient;
import org.mozilla.gecko.sync.jpake.JPakeNoActivePairingException;
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.AccountAuthenticatorActivity;
@ -356,26 +357,38 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
* @param jCreds
*/
public void onComplete(JSONObject jCreds) {
boolean result = true;
if (!pairWithPin) {
String accountName = (String) jCreds.get(Constants.JSON_KEY_ACCOUNT);
String password = (String) jCreds.get(Constants.JSON_KEY_PASSWORD);
String syncKey = (String) jCreds.get(Constants.JSON_KEY_SYNCKEY);
String serverURL = (String) jCreds.get(Constants.JSON_KEY_SERVER);
Logger.debug(LOG_TAG, "Using account manager " + mAccountManager);
final Intent intent = SyncAccounts.createAccount(mContext, mAccountManager,
accountName,
syncKey, password, serverURL);
final SyncAccountParameters syncAccount = new SyncAccountParameters(mContext, mAccountManager,
accountName, syncKey, password, serverURL);
final Account account = SyncAccounts.createSyncAccount(syncAccount);
result = (account != null);
final Intent intent = new Intent(); // The intent to return.
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, syncAccount.username);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, Constants.ACCOUNTTYPE_SYNC);
intent.putExtra(AccountManager.KEY_AUTHTOKEN, Constants.ACCOUNTTYPE_SYNC);
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
if (result) {
setResult(RESULT_OK, intent);
} else {
setResult(RESULT_CANCELED, intent);
}
}
jClient = null; // Sync is set up. Kill reference to JPakeClient object.
jClient = null; // Sync should be set up. Kill reference to JPakeClient object.
final boolean res = result;
runOnUiThread(new Runnable() {
@Override
public void run() {
displayAccount(true);
displayResult(res);
}
});
}
@ -396,16 +409,20 @@ public class SetupSyncActivity extends AccountAuthenticatorActivity {
}
/**
* Displays Sync account setup completed feedback to user.
* Displays Sync account setup result to user.
*
* @param isSetup
* boolean for whether success screen is reached during setup
* completion, or otherwise.
* true is account was set up successfully, false otherwise.
*/
private void displayAccount(boolean isSetup) {
Intent intent = new Intent(mContext, SetupSuccessActivity.class);
private void displayResult(boolean isSuccess) {
Intent intent = null;
if (isSuccess) {
intent = new Intent(mContext, SetupSuccessActivity.class);
} else {
intent = new Intent(mContext, SetupFailureActivity.class);
}
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, isSetup);
intent.putExtra(Constants.INTENT_EXTRA_IS_SETUP, !pairWithPin);
startActivity(intent);
finish();
}

View File

@ -25,7 +25,7 @@ public interface GlobalSyncStage {
updateEnabledEngines,
*/
syncTabs,
// syncPasswords,
syncPasswords,
syncBookmarks,
syncHistory,
syncFormHistory,

View File

@ -421,21 +421,29 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter implements GlobalSe
if (accountGUID == null) {
Logger.info(LOG_TAG, "Account GUID was null. Creating a new one.");
accountGUID = Utils.generateGuid();
mAccountManager.setUserData(localAccount, Constants.ACCOUNT_GUID, accountGUID);
setAccountGUID(mAccountManager, localAccount, accountGUID);
}
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);
if (clientName == null) {
clientName = GlobalConstants.PRODUCT_NAME + " on " + android.os.Build.MODEL;
mAccountManager.setUserData(localAccount, Constants.CLIENT_NAME, clientName);
setClientName(mAccountManager, localAccount, clientName);
}
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,