diff options
Diffstat (limited to 'core/java/android/accounts/AccountManager.java')
-rw-r--r-- | core/java/android/accounts/AccountManager.java | 1367 |
1 files changed, 1367 insertions, 0 deletions
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java new file mode 100644 index 0000000..9765496 --- /dev/null +++ b/core/java/android/accounts/AccountManager.java @@ -0,0 +1,1367 @@ +/* + * 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); + } + } + } +} |