/* 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); } }