diff -r 000000000000 -r 6474c204b198 mobile/android/base/sync/setup/SyncAccounts.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/sync/setup/SyncAccounts.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,597 @@ +/* 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.setup; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; + +import org.mozilla.gecko.background.common.GlobalConstants; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.db.BrowserContract; +import org.mozilla.gecko.sync.CredentialException; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.SyncConfiguration; +import org.mozilla.gecko.sync.SyncConstants; +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 android.accounts.Account; +import android.accounts.AccountManager; +import android.content.ActivityNotFoundException; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; + +/** + * This class contains utilities that are of use to Fennec + * and Sync setup activities. + *
+ * Do not break these APIs without correcting upstream code! + */ +public class SyncAccounts { + private static final String LOG_TAG = "SyncAccounts"; + + private static final String MOTO_BLUR_SETTINGS_ACTIVITY = "com.motorola.blur.settings.AccountsAndServicesPreferenceActivity"; + private static final String MOTO_BLUR_PACKAGE = "com.motorola.blur.setup"; + + /** + * Return Sync accounts. + * + * @param c + * Android context. + * @return Sync accounts. + */ + public static Account[] syncAccounts(final Context c) { + return AccountManager.get(c).getAccountsByType(SyncConstants.ACCOUNTTYPE_SYNC); + } + + /** + * 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. + *
+ * Do not call this method from the main thread.
+ */
+ public static boolean syncAccountsExist(Context c) {
+ final boolean accountsExist = AccountManager.get(c).getAccountsByType(SyncConstants.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);
+ }
+
+ /**
+ * 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 Encryption Passphrase)"
+ public final String password; // in password manager: "chrome://weave (Mozilla Services Password)"
+ 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 Context
; cannot be null.
+ * @param accountManager
+ * an AccountManager
instance to use; if null, get it
+ * from context
.
+ * @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);
+ }
+
+ 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;
+ }
+ }
+
+ /**
+ * Create a sync account, clearing any existing preferences, and set it to
+ * sync automatically.
+ *
+ * Do not call this method from the main thread.
+ *
+ * @param syncAccount
+ * parameters of the account to be created.
+ * @return created Account
, 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.
+ *
+ * Do not call this method from the main thread. + *
+ * Intended for testing; use
+ * createSyncAccount(SyncAccountParameters)
instead.
+ *
+ * @param syncAccount
+ * parameters of the account to be created.
+ * @param syncAutomatically
+ * whether to start syncing this Account automatically (
+ * false
for test accounts).
+ * @return created Android Account
, 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.
+ *
+ * Do not call this method from the main thread. + *
+ * Intended for testing; use
+ * createSyncAccount(SyncAccountParameters)
instead.
+ *
+ * @param syncAccount
+ * parameters of the account to be created.
+ * @param syncAutomatically
+ * whether to start syncing this Account automatically (
+ * false
for test accounts).
+ * @param clearPreferences
+ * true
to clear existing preferences before creating.
+ * @return created Android Account
, 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;
+ final String username = syncAccount.username;
+ final String syncKey = syncAccount.syncKey;
+ final String password = syncAccount.password;
+ final String serverURL = (syncAccount.serverURL == null) ?
+ SyncConstants.DEFAULT_AUTH_SERVER : syncAccount.serverURL;
+
+ Logger.debug(LOG_TAG, "Using account manager " + accountManager);
+ if (!RepoUtils.stringsEqual(syncAccount.serverURL, SyncConstants.DEFAULT_AUTH_SERVER)) {
+ Logger.info(LOG_TAG, "Setting explicit server URL: " + serverURL);
+ }
+
+ final Account account = new Account(username, SyncConstants.ACCOUNTTYPE_SYNC);
+ final Bundle userbundle = new Bundle();
+
+ // Add sync key and server URL.
+ userbundle.putString(Constants.OPTION_SYNCKEY, syncKey);
+ userbundle.putString(Constants.OPTION_SERVER, serverURL);
+ Logger.debug(LOG_TAG, "Adding account for " + SyncConstants.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(SyncConstants.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(SyncConstants.GLOBAL_LOG_TAG, "Unable to create account.", e);
+ }
+ }
+
+ if (!result) {
+ Logger.error(LOG_TAG, "Failed to add account " + account + "!");
+ return null;
+ }
+ Logger.debug(LOG_TAG, "Account " + account + " added successfully.");
+
+ setSyncAutomatically(account, syncAutomatically);
+ setIsSyncable(account, syncAutomatically);
+ Logger.debug(LOG_TAG, "Set account to sync automatically? " + syncAutomatically + ".");
+
+ try {
+ 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) {
+ 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);
+ }
+ return account;
+ }
+
+ public static void setIsSyncable(Account account, boolean isSyncable) {
+ String authority = BrowserContract.AUTHORITY;
+ ContentResolver.setIsSyncable(account, authority, isSyncable ? 1 : 0);
+ }
+
+ public static void setSyncAutomatically(Account account, boolean syncAutomatically) {
+ if (syncAutomatically) {
+ ContentResolver.setMasterSyncAutomatically(true);
+ }
+
+ String authority = BrowserContract.AUTHORITY;
+ Logger.debug(LOG_TAG, "Setting authority " + authority + " to " +
+ (syncAutomatically ? "" : "not ") + "sync automatically.");
+ ContentResolver.setSyncAutomatically(account, authority, syncAutomatically);
+ }
+
+ public static void backgroundSetSyncAutomatically(final Account account, final boolean syncAutomatically) {
+ ThreadPool.run(new Runnable() {
+ @Override
+ public void run() {
+ setSyncAutomatically(account, syncAutomatically);
+ }
+ });
+ }
+ /**
+ * Bug 721760: try to start a vendor-specific Accounts & Sync activity on Moto
+ * Blur devices.
+ *
+ * Bug 773562: actually start and catch ActivityNotFoundException
,
+ * rather than just returning the Intent
only, because some
+ * Moto devices fail to start the activity.
+ *
+ * @param context
+ * current Android context.
+ * @param vendorPackage
+ * vendor specific package name.
+ * @param vendorClass
+ * vendor specific class name.
+ * @return null on failure, otherwise the Intent
started.
+ */
+ protected static Intent openVendorSyncSettings(Context context, final String vendorPackage, final String vendorClass) {
+ try {
+ final int contextFlags = Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY;
+ Context foreignContext = context.createPackageContext(vendorPackage, contextFlags);
+ Class> klass = foreignContext.getClassLoader().loadClass(vendorClass);
+
+ final Intent intent = new Intent(foreignContext, klass);
+ context.startActivity(intent);
+ Logger.info(LOG_TAG, "Vendor package " + vendorPackage + " and class " +
+ vendorClass + " found, and activity launched.");
+ return intent;
+ } catch (NameNotFoundException e) {
+ Logger.debug(LOG_TAG, "Vendor package " + vendorPackage + " not found. Skipping.");
+ } catch (ClassNotFoundException e) {
+ Logger.debug(LOG_TAG, "Vendor package " + vendorPackage + " found but class " +
+ vendorClass + " not found. Skipping.", e);
+ } catch (ActivityNotFoundException e) {
+ // Bug 773562 - android.content.ActivityNotFoundException on Motorola devices.
+ Logger.warn(LOG_TAG, "Vendor package " + vendorPackage + " and class " +
+ vendorClass + " found, but activity not launched. Skipping.", e);
+ } catch (Exception e) {
+ // Just in case.
+ Logger.warn(LOG_TAG, "Caught exception launching activity from vendor package " + vendorPackage +
+ " and class " + vendorClass + ". Ignoring.", e);
+ }
+ return null;
+ }
+
+ /**
+ * Start Sync settings activity.
+ *
+ * @param context
+ * current Android context.
+ * @return the Intent
started.
+ */
+ public static Intent openSyncSettings(Context context) {
+ // Bug 721760 - opening Sync settings takes user to Battery & Data Manager
+ // on a variety of Motorola devices. This work around tries to load the
+ // correct Intent by hand. Oh, Android.
+ Intent intent = openVendorSyncSettings(context, MOTO_BLUR_PACKAGE, MOTO_BLUR_SETTINGS_ACTIVITY);
+ if (intent != null) {
+ return intent;
+ }
+
+ // Open default Sync settings activity.
+ intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
+ // Bug 774233: do not start activity as a new task (second run fails on some HTC devices).
+ context.startActivity(intent); // We should always find this Activity.
+ return intent;
+ }
+
+ /**
+ * Synchronously extract Sync account parameters from Android account version
+ * 0, using plain auth token type.
+ *
+ * Safe to call from main thread.
+ *
+ * @param context
+ * Android 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 {
+ String username;
+ try {
+ username = Utils.usernameFromAccount(account.name);
+ } catch (NoSuchAlgorithmException e) {
+ throw new CredentialException.MissingCredentialException("username");
+ } catch (UnsupportedEncodingException e) {
+ throw new CredentialException.MissingCredentialException("username");
+ }
+
+ /*
+ * If we are accessing an Account that we don't own, Android will throw an
+ * unchecked SecurityException
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 password;
+ String syncKey;
+ String serverURL;
+ try {
+ password = accountManager.getPassword(account);
+ 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);
+ }
+ }
+
+ /**
+ * Bug 790931: create an intent announcing that a Sync account will be
+ * deleted.
+ *
+ * This intent must be broadcast with secure permissions, because it + * contains sensitive user information including the Sync account password and + * Sync key. + *
+ * Version 1 of the created intent includes extras with keys
+ * Constants.JSON_KEY_VERSION
,
+ * Constants.JSON_KEY_TIMESTAMP
, and
+ * Constants.JSON_KEY_ACCOUNT
(which is the Android Account name,
+ * not the encoded Sync Account name).
+ *
+ * If possible, it contains the key Constants.JSON_KEY_PAYLOAD
+ * with value the Sync account parameters as JSON, except the Sync key has
+ * been replaced with the empty string. (We replace, rather than remove,
+ * the Sync key because SyncAccountParameters expects a non-null Sync key.)
+ *
+ * @see SyncAccountParameters#asJSON
+ *
+ * @param context
+ * Android context.
+ * @param accountManager
+ * Android account manager.
+ * @param account
+ * Android account being removed.
+ * @return Intent
to broadcast.
+ */
+ public static Intent makeSyncAccountDeletedIntent(final Context context, final AccountManager accountManager, final Account account) {
+ final Intent intent = new Intent(SyncConstants.SYNC_ACCOUNT_DELETED_ACTION);
+
+ intent.putExtra(Constants.JSON_KEY_VERSION, Long.valueOf(SyncConstants.SYNC_ACCOUNT_DELETED_INTENT_VERSION));
+ intent.putExtra(Constants.JSON_KEY_TIMESTAMP, Long.valueOf(System.currentTimeMillis()));
+ intent.putExtra(Constants.JSON_KEY_ACCOUNT, account.name);
+
+ SyncAccountParameters accountParameters = null;
+ try {
+ accountParameters = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account);
+ } catch (Exception e) {
+ Logger.warn(LOG_TAG, "Caught exception fetching account parameters.", e);
+ }
+
+ if (accountParameters != null) {
+ ExtendedJSONObject json = accountParameters.asJSON();
+ json.put(Constants.JSON_KEY_SYNCKEY, ""); // Reduce attack surface area by removing Sync key.
+ intent.putExtra(Constants.JSON_KEY_PAYLOAD, json.toJSONString());
+ }
+
+ return intent;
+ }
+
+ /**
+ * Synchronously fetch SharedPreferences of a profile associated with a Sync
+ * account.
+ *
+ * Safe to call from main thread. + * + * @param context + * Android context. + * @param accountManager + * Android account manager. + * @param account + * Android Account. + * @param product + * package. + * @param profile + * of account. + * @param version + * number. + * @return SharedPreferences associated with Sync account. + * @throws CredentialException + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + */ + public static SharedPreferences blockingPrefsFromAndroidAccountV0(final Context context, final AccountManager accountManager, final Account account, + final String product, final String profile, final long version) + throws CredentialException, NoSuchAlgorithmException, UnsupportedEncodingException { + SyncAccountParameters params = SyncAccounts.blockingFromAndroidAccountV0(context, accountManager, account); + String prefsPath = Utils.getPrefsPath(product, params.username, params.serverURL, profile, version); + + return context.getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE); + } + + /** + * Synchronously fetch SharedPreferences of a profile associated with the + * default Firefox profile of a Sync Account. + *
+ * Uses the default package, default profile, and current version. + *
+ * Safe to call from main thread. + * + * @param context + * Android context. + * @param accountManager + * Android account manager. + * @param account + * Android Account. + * @return SharedPreferences associated with Sync account. + * @throws CredentialException + * @throws NoSuchAlgorithmException + * @throws UnsupportedEncodingException + */ + public static SharedPreferences blockingPrefsFromDefaultProfileV0(final Context context, final AccountManager accountManager, final Account account) + throws CredentialException, NoSuchAlgorithmException, UnsupportedEncodingException { + final String product = GlobalConstants.BROWSER_INTENT_PACKAGE; + final String profile = Constants.DEFAULT_PROFILE; + final long version = SyncConfiguration.CURRENT_PREFS_VERSION; + + return blockingPrefsFromAndroidAccountV0(context, accountManager, account, product, profile, version); + } +}