/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.accounts; import android.app.Activity; import android.content.Intent; import android.content.Context; import android.content.IntentFilter; import android.content.BroadcastReceiver; import android.database.SQLException; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.Parcelable; import android.util.Log; import java.io.IOException; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeUnit; import java.util.HashMap; import java.util.Map; import com.google.android.collect.Maps; /** * A class that helps with interactions with the AccountManager Service. It provides * methods to allow for account, password, and authtoken management for all accounts on the * device. One accesses the {@link AccountManager} by calling: * <pre> * AccountManager accountManager = AccountManager.get(context); * </pre> * * <p> * The AccountManager Service provides storage for the accounts known to the system, * provides methods to manage them, and allows the registration of authenticators to * which operations such as addAccount and getAuthToken are delegated. * <p> * Many of the calls take an {@link AccountManagerCallback} and {@link Handler} as parameters. * These calls return immediately but run asynchronously. If a callback is provided then * {@link AccountManagerCallback#run} will be invoked wen the request completes, successfully * or not. An {@link AccountManagerFuture} is returned by these requests and also passed into the * callback. The result if retrieved by calling {@link AccountManagerFuture#getResult()} which * either returns the result or throws an exception as appropriate. * <p> * The asynchronous request can be made blocking by not providing a callback and instead * calling {@link AccountManagerFuture#getResult()} on the future that is returned. This will * cause the running thread to block until the result is returned. Keep in mind that one * should not block the main thread in this way. Instead one should either use a callback, * thus making the call asynchronous, or make the blocking call on a separate thread. * <p> * If one wants to ensure that the callback is invoked from a specific handler then they should * pass the handler to the request. This makes it easier to ensure thread-safety by running * all of one's logic from a single handler. */ public class AccountManager { private static final String TAG = "AccountManager"; public static final int ERROR_CODE_REMOTE_EXCEPTION = 1; public static final int ERROR_CODE_NETWORK_ERROR = 3; public static final int ERROR_CODE_CANCELED = 4; public static final int ERROR_CODE_INVALID_RESPONSE = 5; public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6; public static final int ERROR_CODE_BAD_ARGUMENTS = 7; public static final int ERROR_CODE_BAD_REQUEST = 8; public static final String KEY_ACCOUNTS = "accounts"; public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types"; public static final String KEY_USERDATA = "userdata"; public static final String KEY_AUTHTOKEN = "authtoken"; public static final String KEY_PASSWORD = "password"; public static final String KEY_ACCOUNT_NAME = "authAccount"; public static final String KEY_ACCOUNT_TYPE = "accountType"; public static final String KEY_ERROR_CODE = "errorCode"; public static final String KEY_ERROR_MESSAGE = "errorMessage"; public static final String KEY_INTENT = "intent"; public static final String KEY_BOOLEAN_RESULT = "booleanResult"; public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse"; public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse"; public static final String KEY_AUTH_FAILED_MESSAGE = "authFailedMessage"; public static final String KEY_AUTH_TOKEN_LABEL = "authTokenLabelKey"; public static final String ACTION_AUTHENTICATOR_INTENT = "android.accounts.AccountAuthenticator"; public static final String AUTHENTICATOR_META_DATA_NAME = "android.accounts.AccountAuthenticator"; public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator"; private final Context mContext; private final IAccountManager mService; private final Handler mMainHandler; /** * Action sent as a broadcast Intent by the AccountsService * when accounts are added to and/or removed from the device's * database. */ public static final String LOGIN_ACCOUNTS_CHANGED_ACTION = "android.accounts.LOGIN_ACCOUNTS_CHANGED"; /** * @hide */ public AccountManager(Context context, IAccountManager service) { mContext = context; mService = service; mMainHandler = new Handler(mContext.getMainLooper()); } /** * @hide used for testing only */ public AccountManager(Context context, IAccountManager service, Handler handler) { mContext = context; mService = service; mMainHandler = handler; } /** * Retrieve an AccountManager instance that is associated with the context that is passed in. * Certain calls such as {@link #addOnAccountsUpdatedListener} use this context internally, * so the caller must take care to use a {@link Context} whose lifetime is associated with * the listener registration. * @param context The {@link Context} to use when necessary * @return an {@link AccountManager} instance that is associated with context */ public static AccountManager get(Context context) { return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); } /** * Get the password that is associated with the account. Returns null if the account does * not exist. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. */ public String getPassword(final Account account) { try { return mService.getPassword(account); } catch (RemoteException e) { // will never happen throw new RuntimeException(e); } } /** * Get the user data named by "key" that is associated with the account. * Returns null if the account does not exist or if it does not have a value for key. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. */ public String getUserData(final Account account, final String key) { try { return mService.getUserData(account, key); } catch (RemoteException e) { // will never happen throw new RuntimeException(e); } } /** * Query the AccountManager Service for an array that contains a * {@link AuthenticatorDescription} for each registered authenticator. * @return an array that contains all the authenticators known to the AccountManager service. * This array will be empty if there are no authenticators and will never return null. * <p> * No permission is required to make this call. */ public AuthenticatorDescription[] getAuthenticatorTypes() { try { return mService.getAuthenticatorTypes(); } catch (RemoteException e) { // will never happen throw new RuntimeException(e); } } /** * Query the AccountManager Service for all accounts. * @return an array that contains all the accounts known to the AccountManager service. * This array will be empty if there are no accounts and will never return null. * <p> * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} */ public Account[] getAccounts() { try { return mService.getAccounts(null); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Query the AccountManager for the set of accounts that have a given type. If null * is passed as the type than all accounts are returned. * @param type the account type by which to filter, or null to get all accounts * @return an array that contains the accounts that match the specified type. This array * will be empty if no accounts match. It will never return null. * <p> * Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS} */ public Account[] getAccountsByType(String type) { try { return mService.getAccounts(type); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Add an account to the AccountManager's set of known accounts. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. * @param account The account to add * @param password The password to associate with the account. May be null. * @param userdata A bundle of key/value pairs to set as the account's userdata. May be null. * @return true if the account was sucessfully added, false otherwise, for example, * if the account already exists or if the account is null */ public boolean addAccountExplicitly(Account account, String password, Bundle userdata) { try { return mService.addAccount(account, password, userdata); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Removes the given account. If this account does not exist then this call has no effect. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param account The {@link Account} to remove * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Boolean} that is true if the account is successfully removed * or false if the authenticator refuses to remove the account. */ public AccountManagerFuture<Boolean> removeAccount(final Account account, AccountManagerCallback<Boolean> callback, Handler handler) { return new Future2Task<Boolean>(handler, callback) { public void doWork() throws RemoteException { mService.removeAccount(mResponse, account); } public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException { if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) { throw new AuthenticatorException("no result in response"); } return bundle.getBoolean(KEY_BOOLEAN_RESULT); } }.start(); } /** * Removes the given authtoken. If this authtoken does not exist for the given account type * then this call has no effect. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * @param accountType the account type of the authtoken to invalidate * @param authToken the authtoken to invalidate */ public void invalidateAuthToken(final String accountType, final String authToken) { try { mService.invalidateAuthToken(accountType, authToken); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Gets the authtoken named by "authTokenType" for the specified account if it is cached * by the AccountManager. If no authtoken is cached then null is returned rather than * asking the authenticaticor to generate one. If the account or the * authtoken do not exist then null is returned. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. * @param account the account whose authtoken is to be retrieved, must not be null * @param authTokenType the type of authtoken to retrieve * @return an authtoken for the given account and authTokenType, if one is cached by the * AccountManager, null otherwise. */ public String peekAuthToken(final Account account, final String authTokenType) { if (account == null) { Log.e(TAG, "peekAuthToken: the account must not be null"); return null; } if (authTokenType == null) { return null; } try { return mService.peekAuthToken(account, authTokenType); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Sets the password for the account. The password may be null. If the account does not exist * then this call has no affect. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. * @param account the account whose password is to be set. Must not be null. * @param password the password to set for the account. May be null. */ public void setPassword(final Account account, final String password) { if (account == null) { Log.e(TAG, "the account must not be null"); return; } try { mService.setPassword(account, password); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Sets the password for account to null. If the account does not exist then this call * has no effect. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * @param account the account whose password is to be cleared. Must not be null. */ public void clearPassword(final Account account) { if (account == null) { Log.e(TAG, "the account must not be null"); return; } try { mService.clearPassword(account); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Sets account's userdata named "key" to the specified value. If the account does not * exist then this call has no effect. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. * @param account the account whose userdata is to be set. Must not be null. * @param key the key of the userdata to set. Must not be null. * @param value the value to set. May be null. */ public void setUserData(final Account account, final String key, final String value) { if (account == null) { Log.e(TAG, "the account must not be null"); return; } if (key == null) { Log.e(TAG, "the key must not be null"); return; } try { mService.setUserData(account, key, value); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Sets the authtoken named by "authTokenType" to the value specified by authToken. * If the account does not exist then this call has no effect. * <p> * Requires that the caller has permission * {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running * with the same UID as the Authenticator for the account. * @param account the account whose authtoken is to be set. Must not be null. * @param authTokenType the type of the authtoken to set. Must not be null. * @param authToken the authToken to set. May be null. */ public void setAuthToken(Account account, final String authTokenType, final String authToken) { try { mService.setAuthToken(account, authTokenType, authToken); } catch (RemoteException e) { // won't ever happen throw new RuntimeException(e); } } /** * Convenience method that makes a blocking call to * {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, Handler)} * then extracts and returns the value of {@link #KEY_AUTHTOKEN} from its result. * <p> * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. * @param account the account whose authtoken is to be retrieved, must not be null * @param authTokenType the type of authtoken to retrieve * @param notifyAuthFailure if true, cause the AccountManager to put up a "sign-on" notification * for the account if no authtoken is cached by the AccountManager and the the authenticator * does not have valid credentials to get an authtoken. * @return an authtoken for the given account and authTokenType, if one is cached by the * AccountManager, null otherwise. * @throws AuthenticatorException if the authenticator is not present, unreachable or returns * an invalid response. * @throws OperationCanceledException if the request is canceled for any reason * @throws java.io.IOException if the authenticator experiences an IOException while attempting * to communicate with its backend server. */ public String blockingGetAuthToken(Account account, String authTokenType, boolean notifyAuthFailure) throws OperationCanceledException, IOException, AuthenticatorException { Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */, null /* handler */).getResult(); return bundle.getString(KEY_AUTHTOKEN); } /** * Request that an authtoken of the specified type be returned for an account. * If the Account Manager has a cached authtoken of the requested type then it will * service the request itself. Otherwise it will pass the request on to the authenticator. * The authenticator can try to service this request with information it already has stored * in the AccountManager but may need to launch an activity to prompt the * user to enter credentials. If it is able to retrieve the authtoken it will be returned * in the result. * <p> * If the authenticator needs to prompt the user for credentials it will return an intent to * the activity that will do the prompting. If an activity is supplied then that activity * will be used to launch the intent and the result will come from it. Otherwise a result will * be returned that contains the intent. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. * * @param account The account whose credentials are to be updated. * @param authTokenType the auth token to retrieve as part of updating the credentials. * May be null. * @param options authenticator specific options for the request * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then * the intent will be started with this activity. If activity is null then the result will * be returned as-is. * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains: * <ul> * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} * </ul> * If the user presses "back" then the request will be canceled. */ public AccountManagerFuture<Bundle> getAuthToken( final Account account, final String authTokenType, final Bundle options, final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { if (activity == null) throw new IllegalArgumentException("activity is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.getAuthToken(mResponse, account, authTokenType, false /* notifyOnAuthFailure */, true /* expectActivityLaunch */, options); } }.start(); } /** * Request that an authtoken of the specified type be returned for an account. * If the Account Manager has a cached authtoken of the requested type then it will * service the request itself. Otherwise it will pass the request on to the authenticator. * The authenticator can try to service this request with information it already has stored * in the AccountManager but may need to launch an activity to prompt the * user to enter credentials. If it is able to retrieve the authtoken it will be returned * in the result. * <p> * If the authenticator needs to prompt the user for credentials it will return an intent for * an activity that will do the prompting. If an intent is returned and notifyAuthFailure * is true then a notification will be created that launches this intent. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}. * * @param account The account whose credentials are to be updated. * @param authTokenType the auth token to retrieve as part of updating the credentials. * May be null. * @param notifyAuthFailure if true and the authenticator returns a {@link #KEY_INTENT} in the * result then a "sign-on needed" notification will be created that will launch this intent. * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains either: * <ul> * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} * if the authenticator is able to retrieve the auth token * </ul> * If the user presses "back" then the request will be canceled. */ public AccountManagerFuture<Bundle> getAuthToken( final Account account, final String authTokenType, final boolean notifyAuthFailure, AccountManagerCallback<Bundle> callback, Handler handler) { if (account == null) throw new IllegalArgumentException("account is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); return new AmsTask(null, handler, callback) { public void doWork() throws RemoteException { mService.getAuthToken(mResponse, account, authTokenType, notifyAuthFailure, false /* expectActivityLaunch */, null /* options */); } }.start(); } /** * Request that an account be added with the given accountType. This request * is processed by the authenticator for the account type. If no authenticator is registered * in the system then {@link AuthenticatorException} is thrown. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param accountType The type of account to add. This must not be null. * @param authTokenType The account that is added should be able to service this auth token * type. This may be null. * @param requiredFeatures The account that is added should support these features. * This array may be null or empty. * @param addAccountOptions A bundle of authenticator-specific options that is passed on * to the authenticator. This may be null. * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then * the intent will be started with this activity. If activity is null then the result will * be returned as-is. * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains either: * <ul> * <li> {@link #KEY_INTENT}, or * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} * and {@link #KEY_AUTHTOKEN} (if an authTokenType was specified). * </ul> */ public AccountManagerFuture<Bundle> addAccount(final String accountType, final String authTokenType, final String[] requiredFeatures, final Bundle addAccountOptions, final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { if (accountType == null) { Log.e(TAG, "the account must not be null"); // to unblock caller waiting on Future.get() set(new Bundle()); return; } mService.addAcount(mResponse, accountType, authTokenType, requiredFeatures, activity != null, addAccountOptions); } }.start(); } public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures( final String type, final String[] features, AccountManagerCallback<Account[]> callback, Handler handler) { return new Future2Task<Account[]>(handler, callback) { public void doWork() throws RemoteException { if (type == null) { Log.e(TAG, "Type is null"); set(new Account[0]); return; } mService.getAccountsByFeatures(mResponse, type, features); } public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException { if (!bundle.containsKey(KEY_ACCOUNTS)) { throw new AuthenticatorException("no result in response"); } final Parcelable[] parcelables = bundle.getParcelableArray(KEY_ACCOUNTS); Account[] descs = new Account[parcelables.length]; for (int i = 0; i < parcelables.length; i++) { descs[i] = (Account) parcelables[i]; } return descs; } }.start(); } /** * Requests that the authenticator checks that the user knows the credentials for the account. * This is typically done by returning an intent to an activity that prompts the user to * enter the credentials. This request * is processed by the authenticator for the account. If no matching authenticator is * registered in the system then {@link AuthenticatorException} is thrown. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param account The account whose credentials are to be checked * @param options authenticator specific options for the request * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then * the intent will be started with this activity. If activity is null then the result will * be returned as-is. * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains either: * <ul> * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials * <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct * credentials * </ul> * If the user presses "back" then the request will be canceled. */ public AccountManagerFuture<Bundle> confirmCredentials(final Account account, final Bundle options, final Activity activity, final AccountManagerCallback<Bundle> callback, final Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.confirmCredentials(mResponse, account, options, activity != null); } }.start(); } /** * Requests that the authenticator update the the credentials for a user. This is typically * done by returning an intent to an activity that will prompt the user to update the stored * credentials for the account. This request * is processed by the authenticator for the account. If no matching authenticator is * registered in the system then {@link AuthenticatorException} is thrown. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param account The account whose credentials are to be updated. * @param authTokenType the auth token to retrieve as part of updating the credentials. * May be null. * @param options authenticator specific options for the request * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then * the intent will be started with this activity. If activity is null then the result will * be returned as-is. * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains either: * <ul> * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials * <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct * credentials, and optionally a {@link #KEY_AUTHTOKEN} if an authTokenType was provided. * </ul> * If the user presses "back" then the request will be canceled. */ public AccountManagerFuture<Bundle> updateCredentials(final Account account, final String authTokenType, final Bundle options, final Activity activity, final AccountManagerCallback<Bundle> callback, final Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.updateCredentials(mResponse, account, authTokenType, activity != null, options); } }.start(); } /** * Request that the properties for an authenticator be updated. This is typically done by * returning an intent to an activity that will allow the user to make changes. This request * is processed by the authenticator for the account. If no matching authenticator is * registered in the system then {@link AuthenticatorException} is thrown. * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param accountType The account type of the authenticator whose properties are to be edited. * @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then * the intent will be started with this activity. If activity is null then the result will * be returned as-is. * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains either: * <ul> * <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials * <li> nothing, returned if the edit completes successfully * </ul> * If the user presses "back" then the request will be canceled. */ public AccountManagerFuture<Bundle> editProperties(final String accountType, final Activity activity, final AccountManagerCallback<Bundle> callback, final Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.editProperties(mResponse, accountType, activity != null); } }.start(); } private void ensureNotOnMainThread() { final Looper looper = Looper.myLooper(); if (looper != null && looper == mContext.getMainLooper()) { // We really want to throw an exception here, but GTalkService exercises this // path quite a bit and needs some serious rewrite in order to work properly. //noinspection ThrowableInstanceNeverThrow // Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs", // new Exception()); // TODO remove the log and throw this exception when the callers are fixed // throw new IllegalStateException( // "calling this from your main thread can lead to deadlock"); } } private void postToHandler(Handler handler, final AccountManagerCallback<Bundle> callback, final AccountManagerFuture<Bundle> future) { handler = handler == null ? mMainHandler : handler; handler.post(new Runnable() { public void run() { callback.run(future); } }); } private void postToHandler(Handler handler, final OnAccountsUpdateListener listener, final Account[] accounts) { final Account[] accountsCopy = new Account[accounts.length]; // send a copy to make sure that one doesn't // change what another sees System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length); handler = (handler == null) ? mMainHandler : handler; handler.post(new Runnable() { public void run() { try { listener.onAccountsUpdated(accountsCopy); } catch (SQLException e) { // Better luck next time. If the problem was disk-full, // the STORAGE_OK intent will re-trigger the update. Log.e(TAG, "Can't update accounts", e); } } }); } private abstract class AmsTask extends FutureTask<Bundle> implements AccountManagerFuture<Bundle> { final IAccountManagerResponse mResponse; final Handler mHandler; final AccountManagerCallback<Bundle> mCallback; final Activity mActivity; public AmsTask(Activity activity, Handler handler, AccountManagerCallback<Bundle> callback) { super(new Callable<Bundle>() { public Bundle call() throws Exception { throw new IllegalStateException("this should never be called"); } }); mHandler = handler; mCallback = callback; mActivity = activity; mResponse = new Response(); } public final AccountManagerFuture<Bundle> start() { try { doWork(); } catch (RemoteException e) { setException(e); } return this; } public abstract void doWork() throws RemoteException; private Bundle internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { ensureNotOnMainThread(); try { if (timeout == null) { return get(); } else { return get(timeout, unit); } } catch (CancellationException e) { throw new OperationCanceledException(); } catch (TimeoutException e) { // fall through and cancel } catch (InterruptedException e) { // fall through and cancel } catch (ExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { throw (IOException) cause; } else if (cause instanceof UnsupportedOperationException) { throw new AuthenticatorException(cause); } else if (cause instanceof AuthenticatorException) { throw (AuthenticatorException) cause; } else if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else if (cause instanceof Error) { throw (Error) cause; } else { throw new IllegalStateException(cause); } } finally { cancel(true /* interrupt if running */); } throw new OperationCanceledException(); } public Bundle getResult() throws OperationCanceledException, IOException, AuthenticatorException { return internalGetResult(null, null); } public Bundle getResult(long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { return internalGetResult(timeout, unit); } protected void done() { if (mCallback != null) { postToHandler(mHandler, mCallback, this); } } /** Handles the responses from the AccountManager */ private class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { Intent intent = bundle.getParcelable("intent"); if (intent != null && mActivity != null) { // since the user provided an Activity we will silently start intents // that we see mActivity.startActivity(intent); // leave the Future running to wait for the real response to this request } else if (bundle.getBoolean("retry")) { try { doWork(); } catch (RemoteException e) { // this will only happen if the system process is dead, which means // we will be dying ourselves } } else { set(bundle); } } public void onError(int code, String message) { if (code == ERROR_CODE_CANCELED) { // the authenticator indicated that this request was canceled, do so now cancel(true /* mayInterruptIfRunning */); return; } setException(convertErrorToException(code, message)); } } } private abstract class BaseFutureTask<T> extends FutureTask<T> { final public IAccountManagerResponse mResponse; final Handler mHandler; public BaseFutureTask(Handler handler) { super(new Callable<T>() { public T call() throws Exception { throw new IllegalStateException("this should never be called"); } }); mHandler = handler; mResponse = new Response(); } public abstract void doWork() throws RemoteException; public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException; protected void postRunnableToHandler(Runnable runnable) { Handler handler = (mHandler == null) ? mMainHandler : mHandler; handler.post(runnable); } protected void startTask() { try { doWork(); } catch (RemoteException e) { setException(e); } } protected class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { try { T result = bundleToResult(bundle); if (result == null) { return; } set(result); return; } catch (ClassCastException e) { // we will set the exception below } catch (AuthenticatorException e) { // we will set the exception below } onError(ERROR_CODE_INVALID_RESPONSE, "no result in response"); } public void onError(int code, String message) { if (code == ERROR_CODE_CANCELED) { cancel(true /* mayInterruptIfRunning */); return; } setException(convertErrorToException(code, message)); } } } private abstract class Future2Task<T> extends BaseFutureTask<T> implements AccountManagerFuture<T> { final AccountManagerCallback<T> mCallback; public Future2Task(Handler handler, AccountManagerCallback<T> callback) { super(handler); mCallback = callback; } protected void done() { if (mCallback != null) { postRunnableToHandler(new Runnable() { public void run() { mCallback.run(Future2Task.this); } }); } } public Future2Task<T> start() { startTask(); return this; } private T internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { ensureNotOnMainThread(); try { if (timeout == null) { return get(); } else { return get(timeout, unit); } } catch (InterruptedException e) { // fall through and cancel } catch (TimeoutException e) { // fall through and cancel } catch (CancellationException e) { // fall through and cancel } catch (ExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { throw (IOException) cause; } else if (cause instanceof UnsupportedOperationException) { throw new AuthenticatorException(cause); } else if (cause instanceof AuthenticatorException) { throw (AuthenticatorException) cause; } else if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else if (cause instanceof Error) { throw (Error) cause; } else { throw new IllegalStateException(cause); } } finally { cancel(true /* interrupt if running */); } throw new OperationCanceledException(); } public T getResult() throws OperationCanceledException, IOException, AuthenticatorException { return internalGetResult(null, null); } public T getResult(long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { return internalGetResult(timeout, unit); } } private Exception convertErrorToException(int code, String message) { if (code == ERROR_CODE_NETWORK_ERROR) { return new IOException(message); } if (code == ERROR_CODE_UNSUPPORTED_OPERATION) { return new UnsupportedOperationException(message); } if (code == ERROR_CODE_INVALID_RESPONSE) { return new AuthenticatorException(message); } if (code == ERROR_CODE_BAD_ARGUMENTS) { return new IllegalArgumentException(message); } return new AuthenticatorException(message); } private class GetAuthTokenByTypeAndFeaturesTask extends AmsTask implements AccountManagerCallback<Bundle> { GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType, final String[] features, Activity activityForPrompting, final Bundle addAccountOptions, final Bundle loginOptions, AccountManagerCallback<Bundle> callback, Handler handler) { super(activityForPrompting, handler, callback); if (accountType == null) throw new IllegalArgumentException("account type is null"); mAccountType = accountType; mAuthTokenType = authTokenType; mFeatures = features; mAddAccountOptions = addAccountOptions; mLoginOptions = loginOptions; mMyCallback = this; } volatile AccountManagerFuture<Bundle> mFuture = null; final String mAccountType; final String mAuthTokenType; final String[] mFeatures; final Bundle mAddAccountOptions; final Bundle mLoginOptions; final AccountManagerCallback<Bundle> mMyCallback; public void doWork() throws RemoteException { getAccountsByTypeAndFeatures(mAccountType, mFeatures, new AccountManagerCallback<Account[]>() { public void run(AccountManagerFuture<Account[]> future) { Account[] accounts; try { accounts = future.getResult(); } catch (OperationCanceledException e) { setException(e); return; } catch (IOException e) { setException(e); return; } catch (AuthenticatorException e) { setException(e); return; } if (accounts.length == 0) { if (mActivity != null) { // no accounts, add one now. pretend that the user directly // made this request mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures, mAddAccountOptions, mActivity, mMyCallback, mHandler); } else { // send result since we can't prompt to add an account Bundle result = new Bundle(); result.putString(KEY_ACCOUNT_NAME, null); result.putString(KEY_ACCOUNT_TYPE, null); result.putString(KEY_AUTHTOKEN, null); try { mResponse.onResult(result); } catch (RemoteException e) { // this will never happen } // we are done } } else if (accounts.length == 1) { // have a single account, return an authtoken for it if (mActivity == null) { mFuture = getAuthToken(accounts[0], mAuthTokenType, false /* notifyAuthFailure */, mMyCallback, mHandler); } else { mFuture = getAuthToken(accounts[0], mAuthTokenType, mLoginOptions, mActivity, mMyCallback, mHandler); } } else { if (mActivity != null) { IAccountManagerResponse chooseResponse = new IAccountManagerResponse.Stub() { public void onResult(Bundle value) throws RemoteException { Account account = new Account( value.getString(KEY_ACCOUNT_NAME), value.getString(KEY_ACCOUNT_TYPE)); mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions, mActivity, mMyCallback, mHandler); } public void onError(int errorCode, String errorMessage) throws RemoteException { mResponse.onError(errorCode, errorMessage); } }; // have many accounts, launch the chooser Intent intent = new Intent(); intent.setClassName("android", "android.accounts.ChooseAccountActivity"); intent.putExtra(KEY_ACCOUNTS, accounts); intent.putExtra(KEY_ACCOUNT_MANAGER_RESPONSE, new AccountManagerResponse(chooseResponse)); mActivity.startActivity(intent); // the result will arrive via the IAccountManagerResponse } else { // send result since we can't prompt to select an account Bundle result = new Bundle(); result.putString(KEY_ACCOUNTS, null); try { mResponse.onResult(result); } catch (RemoteException e) { // this will never happen } // we are done } } }}, mHandler); } public void run(AccountManagerFuture<Bundle> future) { try { set(future.getResult()); } catch (OperationCanceledException e) { cancel(true /* mayInterruptIfRUnning */); } catch (IOException e) { setException(e); } catch (AuthenticatorException e) { setException(e); } } } /** * Convenience method that combines the functionality of {@link #getAccountsByTypeAndFeatures}, * {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)}, * and {@link #addAccount}. It first gets the list of accounts that match accountType and the * feature set. If there are none then {@link #addAccount} is invoked with the authTokenType * feature set, and addAccountOptions. If there is exactly one then * {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)} is * called with that account. If there are more than one then a chooser activity is launched * to prompt the user to select one of them and then the authtoken is retrieved for it, * <p> * This call returns immediately but runs asynchronously and the result is accessed via the * {@link AccountManagerFuture} that is returned. This future is also passed as the sole * parameter to the {@link AccountManagerCallback}. If the caller wished to use this * method asynchronously then they will generally pass in a callback object that will get * invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then * they will generally pass null for the callback and instead call * {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value, * which will then block until the request completes. * <p> * Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}. * * @param accountType the accountType to query; this must be non-null * @param authTokenType the type of authtoken to retrieve; this must be non-null * @param features a filter for the accounts. See {@link #getAccountsByTypeAndFeatures}. * @param activityForPrompting The activity used to start any account management * activities that are required to fulfill this request. This may be null. * @param addAccountOptions authenticator-specific options used if an account needs to be added * @param getAuthTokenOptions authenticator-specific options passed to getAuthToken * @param callback A callback to invoke when the request completes. If null then * no callback is invoked. * @param handler The {@link Handler} to use to invoke the callback. If null then the * main thread's {@link Handler} is used. * @return an {@link AccountManagerFuture} that represents the future result of the call. * The future result is a {@link Bundle} that contains either: * <ul> * <li> {@link #KEY_INTENT}, if no activity is supplied yet an activity needs to launched to * fulfill the request. * <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} if the * request completes successfully. * </ul> * If the user presses "back" then the request will be canceled. */ public AccountManagerFuture<Bundle> getAuthTokenByFeatures( final String accountType, final String authTokenType, final String[] features, final Activity activityForPrompting, final Bundle addAccountOptions, final Bundle getAuthTokenOptions, final AccountManagerCallback<Bundle> callback, final Handler handler) { if (accountType == null) throw new IllegalArgumentException("account type is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); final GetAuthTokenByTypeAndFeaturesTask task = new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features, activityForPrompting, addAccountOptions, getAuthTokenOptions, callback, handler); task.start(); return task; } private final HashMap<OnAccountsUpdateListener, Handler> mAccountsUpdatedListeners = Maps.newHashMap(); /** * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent * so that it can read the updated list of accounts and send them to the listener * in mAccountsUpdatedListeners. */ private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() { public void onReceive(final Context context, final Intent intent) { final Account[] accounts = getAccounts(); // send the result to the listeners synchronized (mAccountsUpdatedListeners) { for (Map.Entry<OnAccountsUpdateListener, Handler> entry : mAccountsUpdatedListeners.entrySet()) { postToHandler(entry.getValue(), entry.getKey(), accounts); } } } }; /** * Add a {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}. * The listener is guaranteed to be invoked on the thread of the Handler that is passed * in or the main thread's Handler if handler is null. * <p> * You must remove this listener before the context that was used to retrieve this * {@link AccountManager} instance goes away. This generally means when the Activity * or Service you are running is stopped. * @param listener the listener to add * @param handler the Handler whose thread will be used to invoke the listener. If null * the AccountManager context's main thread will be used. * @param updateImmediately if true then the listener will be invoked as a result of this * call. * @throws IllegalArgumentException if listener is null * @throws IllegalStateException if listener was already added */ public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener, Handler handler, boolean updateImmediately) { if (listener == null) { throw new IllegalArgumentException("the listener is null"); } synchronized (mAccountsUpdatedListeners) { if (mAccountsUpdatedListeners.containsKey(listener)) { throw new IllegalStateException("this listener is already added"); } final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty(); mAccountsUpdatedListeners.put(listener, handler); if (wasEmpty) { // Register a broadcast receiver to monitor account changes IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION); // To recover from disk-full. intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK); mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter); } } if (updateImmediately) { postToHandler(handler, listener, getAccounts()); } } /** * Remove an {@link OnAccountsUpdateListener} that was previously registered with * {@link #addOnAccountsUpdatedListener}. * @param listener the listener to remove * @throws IllegalArgumentException if listener is null * @throws IllegalStateException if listener was not already added */ public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) { if (listener == null) { Log.e(TAG, "Missing listener"); return; } synchronized (mAccountsUpdatedListeners) { if (!mAccountsUpdatedListeners.containsKey(listener)) { Log.e(TAG, "Listener was not previously added"); return; } mAccountsUpdatedListeners.remove(listener); if (mAccountsUpdatedListeners.isEmpty()) { mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver); } } } }