/* * 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.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.Parcelable; import android.util.Config; 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 AccountManagerService. It provides * methods to allow for account, password, and authtoken management for all accounts on the * device. Some of these calls are implemented with the help of the corresponding * {@link IAccountAuthenticator} services. One accesses the {@link AccountManager} by calling: * AccountManager accountManager = AccountManager.get(context); * *

* TODO(fredq) this interface is still in flux */ public class AccountManager { private static final String TAG = "AccountManager"; private final Context mContext; private final IAccountManager mService; private final Handler mMainHandler; /** * @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; } public static AccountManager get(Context context) { return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); } public String blockingGetPassword(Account account) { ensureNotOnMainThread(); try { return mService.getPassword(account); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Future1 getPassword(final Future1Callback callback, final Account account, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public String call() throws Exception { return blockingGetPassword(account); } }); } public String blockingGetUserData(Account account, String key) { ensureNotOnMainThread(); try { return mService.getUserData(account, key); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Future1 getUserData(Future1Callback callback, final Account account, final String key, Handler handler) { return startAsFuture(callback, handler, new Callable() { public String call() throws Exception { return blockingGetUserData(account, key); } }); } public AuthenticatorDescription[] blockingGetAuthenticatorTypes() { ensureNotOnMainThread(); try { return mService.getAuthenticatorTypes(); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Future1 getAuthenticatorTypes( Future1Callback callback, Handler handler) { return startAsFuture(callback, handler, new Callable() { public AuthenticatorDescription[] call() throws Exception { return blockingGetAuthenticatorTypes(); } }); } public Account[] blockingGetAccounts() { ensureNotOnMainThread(); try { return mService.getAccounts(); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Account[] blockingGetAccountsByType(String accountType) { ensureNotOnMainThread(); try { return mService.getAccountsByType(accountType); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Future1 getAccounts(Future1Callback callback, Handler handler) { return startAsFuture(callback, handler, new Callable() { public Account[] call() throws Exception { return blockingGetAccounts(); } }); } public Future1 getAccountsByType(Future1Callback callback, final String type, Handler handler) { return startAsFuture(callback, handler, new Callable() { public Account[] call() throws Exception { return blockingGetAccountsByType(type); } }); } public boolean blockingAddAccountExplicitly(Account account, String password, Bundle extras) { ensureNotOnMainThread(); try { return mService.addAccount(account, password, extras); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Future1 addAccountExplicitly(final Future1Callback callback, final Account account, final String password, final Bundle extras, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Boolean call() throws Exception { return blockingAddAccountExplicitly(account, password, extras); } }); } public void blockingRemoveAccount(Account account) { ensureNotOnMainThread(); try { mService.removeAccount(account); } catch (RemoteException e) { // if this happens the entire runtime will restart } } public Future1 removeAccount(Future1Callback callback, final Account account, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Void call() throws Exception { blockingRemoveAccount(account); return null; } }); } public void blockingInvalidateAuthToken(String accountType, String authToken) { ensureNotOnMainThread(); try { mService.invalidateAuthToken(accountType, authToken); } catch (RemoteException e) { // if this happens the entire runtime will restart } } public Future1 invalidateAuthToken(Future1Callback callback, final String accountType, final String authToken, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Void call() throws Exception { blockingInvalidateAuthToken(accountType, authToken); return null; } }); } public String blockingPeekAuthToken(Account account, String authTokenType) { ensureNotOnMainThread(); try { return mService.peekAuthToken(account, authTokenType); } catch (RemoteException e) { // if this happens the entire runtime will restart throw new RuntimeException(e); } } public Future1 peekAuthToken(Future1Callback callback, final Account account, final String authTokenType, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public String call() throws Exception { return blockingPeekAuthToken(account, authTokenType); } }); } public void blockingSetPassword(Account account, String password) { ensureNotOnMainThread(); try { mService.setPassword(account, password); } catch (RemoteException e) { // if this happens the entire runtime will restart } } public Future1 setPassword(Future1Callback callback, final Account account, final String password, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Void call() throws Exception { blockingSetPassword(account, password); return null; } }); } public void blockingClearPassword(Account account) { ensureNotOnMainThread(); try { mService.clearPassword(account); } catch (RemoteException e) { // if this happens the entire runtime will restart } } public Future1 clearPassword(final Future1Callback callback, final Account account, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Void call() throws Exception { blockingClearPassword(account); return null; } }); } public void blockingSetUserData(Account account, String key, String value) { ensureNotOnMainThread(); try { mService.setUserData(account, key, value); } catch (RemoteException e) { // if this happens the entire runtime will restart } } public Future1 setUserData(Future1Callback callback, final Account account, final String key, final String value, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Void call() throws Exception { blockingSetUserData(account, key, value); return null; } }); } public void blockingSetAuthToken(Account account, String authTokenType, String authToken) { ensureNotOnMainThread(); try { mService.setAuthToken(account, authTokenType, authToken); } catch (RemoteException e) { // if this happens the entire runtime will restart } } public Future1 setAuthToken(Future1Callback callback, final Account account, final String authTokenType, final String authToken, final Handler handler) { return startAsFuture(callback, handler, new Callable() { public Void call() throws Exception { blockingSetAuthToken(account, authTokenType, authToken); return null; } }); } public String blockingGetAuthToken(Account account, String authTokenType, boolean notifyAuthFailure) throws OperationCanceledException, IOException, AuthenticatorException { ensureNotOnMainThread(); Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */, null /* handler */).getResult(); return bundle.getString(Constants.AUTHTOKEN_KEY); } /** * Request the auth token for this account/authTokenType. If this succeeds then the * auth token will then be passed to the activity. If this results in an authentication * failure then a login intent will be returned that can be invoked to prompt the user to * update their credentials. This login activity will return the auth token to the calling * activity. If activity is null then the login intent will not be invoked. * * @param account the account whose auth token should be retrieved * @param authTokenType the auth token type that should be retrieved * @param loginOptions * @param activity the activity to launch the login intent, if necessary, and to which */ public Future2 getAuthToken( final Account account, final String authTokenType, final Bundle loginOptions, final Activity activity, Future2Callback 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 */, loginOptions); } }.start(); } public Future2 getAuthToken( final Account account, final String authTokenType, final boolean notifyAuthFailure, Future2Callback 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(); } public Future2 addAccount(final String accountType, final String authTokenType, final String[] requiredFeatures, final Bundle addAccountOptions, final Activity activity, Future2Callback callback, Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.addAcount(mResponse, accountType, authTokenType, requiredFeatures, activity != null, addAccountOptions); } }.start(); } /** @deprecated use {@link #confirmCredentials} instead */ public Future1 confirmPassword(final Account account, final String password, Future1Callback callback, Handler handler) { return new AMSTaskBoolean(handler, callback) { public void doWork() throws RemoteException { mService.confirmPassword(response, account, password); } }; } public Account[] blockingGetAccountsWithTypeAndFeatures(String type, String[] features) throws AuthenticatorException, IOException, OperationCanceledException { Future2 future = getAccountsWithTypeAndFeatures(type, features, null /* callback */, null /* handler */); Bundle result = future.getResult(); Parcelable[] accountsTemp = result.getParcelableArray(Constants.ACCOUNTS_KEY); if (accountsTemp == null) { throw new AuthenticatorException("accounts should not be null"); } Account[] accounts = new Account[accountsTemp.length]; for (int i = 0; i < accountsTemp.length; i++) { accounts[i] = (Account) accountsTemp[i]; } return accounts; } public Future2 getAccountsWithTypeAndFeatures( final String type, final String[] features, Future2Callback callback, Handler handler) { if (type == null) throw new IllegalArgumentException("type is null"); return new AmsTask(null /* activity */, handler, callback) { public void doWork() throws RemoteException { mService.getAccountsByTypeAndFeatures(mResponse, type, features); } }.start(); } public Future2 confirmCredentials(final Account account, final Activity activity, final Future2Callback callback, final Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.confirmCredentials(mResponse, account, activity != null); } }.start(); } public Future2 updateCredentials(final Account account, final String authTokenType, final Bundle loginOptions, final Activity activity, final Future2Callback callback, final Handler handler) { return new AmsTask(activity, handler, callback) { public void doWork() throws RemoteException { mService.updateCredentials(mResponse, account, authTokenType, activity != null, loginOptions); } }.start(); } public Future2 editProperties(final String accountType, final Activity activity, final Future2Callback 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(fredq) 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 Future2Callback callback, final Future2 future) { handler = handler == null ? mMainHandler : handler; handler.post(new Runnable() { public void run() { callback.run(future); } }); } private void postToHandler(Handler handler, final OnAccountsUpdatedListener listener, final Account[] accounts) { handler = handler == null ? mMainHandler : handler; handler.post(new Runnable() { public void run() { listener.onAccountsUpdated(accounts); } }); } private void postToHandler(Handler handler, final Future1Callback callback, final Future1 future) { handler = handler == null ? mMainHandler : handler; handler.post(new Runnable() { public void run() { callback.run(future); } }); } private Future1 startAsFuture(Future1Callback callback, Handler handler, Callable callable) { final FutureTaskWithCallback task = new FutureTaskWithCallback(callback, callable, handler); new Thread(task).start(); return task; } private class FutureTaskWithCallback extends FutureTask implements Future1 { final Future1Callback mCallback; final Handler mHandler; public FutureTaskWithCallback(Future1Callback callback, Callable callable, Handler handler) { super(callable); mCallback = callback; mHandler = handler; } protected void done() { if (mCallback != null) { postToHandler(mHandler, mCallback, this); } } public V internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException { try { if (timeout == null) { return get(); } else { return get(timeout, unit); } } catch (InterruptedException e) { // we will cancel the task below } catch (CancellationException e) { // we will cancel the task below } catch (TimeoutException e) { // we will cancel the task below } catch (ExecutionException e) { // this should never happen throw new IllegalStateException(e.getCause()); } finally { cancel(true /* interruptIfRunning */); } throw new OperationCanceledException(); } public V getResult() throws OperationCanceledException { return internalGetResult(null, null); } public V getResult(long timeout, TimeUnit unit) throws OperationCanceledException { return internalGetResult(null, null); } } private abstract class AmsTask extends FutureTask implements Future2 { final IAccountManagerResponse mResponse; final Handler mHandler; final Future2Callback mCallback; final Activity mActivity; final Thread mThread; public AmsTask(Activity activity, Handler handler, Future2Callback callback) { super(new Callable() { public Bundle call() throws Exception { throw new IllegalStateException("this should never be called"); } }); mHandler = handler; mCallback = callback; mActivity = activity; mResponse = new Response(); mThread = new Thread(new Runnable() { public void run() { try { doWork(); } catch (RemoteException e) { // never happens } } }, "AmsTask"); } public final Future2 start() { mThread.start(); return this; } public abstract void doWork() throws RemoteException; private Bundle internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { 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 == Constants.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 AMSTaskBoolean extends FutureTask implements Future1 { final IAccountManagerResponse response; final Handler mHandler; final Future1Callback mCallback; public AMSTaskBoolean(Handler handler, Future1Callback callback) { super(new Callable() { public Boolean call() throws Exception { throw new IllegalStateException("this should never be called"); } }); mHandler = handler; mCallback = callback; response = new Response(); new Thread(new Runnable() { public void run() { try { doWork(); } catch (RemoteException e) { // never happens } } }).start(); } public abstract void doWork() throws RemoteException; protected void done() { if (mCallback != null) { postToHandler(mHandler, mCallback, this); } } private Boolean internalGetResult(Long timeout, TimeUnit unit) { 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) { return false; } catch (ExecutionException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { return false; } else if (cause instanceof UnsupportedOperationException) { return false; } else if (cause instanceof AuthenticatorException) { return false; } 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 */); } return false; } public Boolean getResult() throws OperationCanceledException { return internalGetResult(null, null); } public Boolean getResult(long timeout, TimeUnit unit) throws OperationCanceledException { return internalGetResult(timeout, unit); } private class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { try { if (bundle.containsKey(Constants.BOOLEAN_RESULT_KEY)) { set(bundle.getBoolean(Constants.BOOLEAN_RESULT_KEY)); return; } } catch (ClassCastException e) { // we will set the exception below } onError(Constants.ERROR_CODE_INVALID_RESPONSE, "no result in response"); } public void onError(int code, String message) { if (code == Constants.ERROR_CODE_CANCELED) { cancel(true /* mayInterruptIfRunning */); return; } setException(convertErrorToException(code, message)); } } } private Exception convertErrorToException(int code, String message) { if (code == Constants.ERROR_CODE_NETWORK_ERROR) { return new IOException(message); } if (code == Constants.ERROR_CODE_UNSUPPORTED_OPERATION) { return new UnsupportedOperationException(message); } if (code == Constants.ERROR_CODE_INVALID_RESPONSE) { return new AuthenticatorException(message); } if (code == Constants.ERROR_CODE_BAD_ARGUMENTS) { return new IllegalArgumentException(message); } return new AuthenticatorException(message); } private class GetAuthTokenByTypeAndFeaturesTask extends AmsTask implements Future2Callback { GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType, final String[] features, Activity activityForPrompting, final Bundle addAccountOptions, final Bundle loginOptions, Future2Callback 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 Future2 mFuture = null; final String mAccountType; final String mAuthTokenType; final String[] mFeatures; final Bundle mAddAccountOptions; final Bundle mLoginOptions; final Future2Callback mMyCallback; public void doWork() throws RemoteException { getAccountsWithTypeAndFeatures(mAccountType, mFeatures, new Future2Callback() { public void run(Future2 future) { Bundle getAccountsResult; try { getAccountsResult = future.getResult(); } catch (OperationCanceledException e) { setException(e); return; } catch (IOException e) { setException(e); return; } catch (AuthenticatorException e) { setException(e); return; } Parcelable[] accounts = getAccountsResult.getParcelableArray(Constants.ACCOUNTS_KEY); 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(Constants.ACCOUNT_NAME_KEY, null); result.putString(Constants.ACCOUNT_TYPE_KEY, null); result.putString(Constants.AUTHTOKEN_KEY, 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((Account) accounts[0], mAuthTokenType, false /* notifyAuthFailure */, mMyCallback, mHandler); } else { mFuture = getAuthToken((Account) 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(Constants.ACCOUNT_NAME_KEY), value.getString(Constants.ACCOUNT_TYPE_KEY)); 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(Constants.ACCOUNTS_KEY, accounts); intent.putExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY, 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(Constants.ACCOUNTS_KEY, null); try { mResponse.onResult(result); } catch (RemoteException e) { // this will never happen } // we are done } } }}, mHandler); } // TODO(fredq) pass through the calls to our implemention of Future2 to the underlying // future that we create. We need to do things like have cancel cancel the mFuture, if set // or to cause this to be canceled if mFuture isn't set. // Once this is done then getAuthTokenByFeatures can be changed to return a Future2. public void run(Future2 future) { try { set(future.get()); } catch (InterruptedException e) { cancel(true); } catch (CancellationException e) { cancel(true); } catch (ExecutionException e) { setException(e.getCause()); } } } public void getAuthTokenByFeatures( final String accountType, final String authTokenType, final String[] features, final Activity activityForPrompting, final Bundle addAccountOptions, final Bundle loginOptions, final Future2Callback callback, final Handler handler) { if (accountType == null) throw new IllegalArgumentException("account type is null"); if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null"); new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features, activityForPrompting, addAccountOptions, loginOptions, callback, handler).start(); } private final HashMap mAccountsUpdatedListeners = Maps.newHashMap(); // These variable are only used from the LOGIN_ACCOUNTS_CHANGED_ACTION BroadcastReceiver // and its getAccounts() callback which are both invoked only on the main thread. As a // result we don't need to protect against concurrent accesses and any changes are guaranteed // to be visible when used. Basically, these two variables are thread-confined. private Future1 mAccountsLookupFuture = null; private boolean mAccountLookupPending = false; /** * 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) { if (mAccountsLookupFuture != null) { // an accounts lookup is already in progress, // don't bother starting another request mAccountLookupPending = true; return; } // initiate a read of the accounts mAccountsLookupFuture = getAccounts(new Future1Callback() { public void run(Future1 future) { // clear the future so that future receives will try the lookup again mAccountsLookupFuture = null; // get the accounts array Account[] accounts; try { accounts = future.getResult(); } catch (OperationCanceledException e) { // this should never happen, but if it does pretend we got another // accounts changed broadcast if (Config.LOGD) { Log.d(TAG, "the accounts lookup for listener notifications was " + "canceled, try again by simulating the receipt of " + "a LOGIN_ACCOUNTS_CHANGED_ACTION broadcast"); } onReceive(context, intent); return; } // send the result to the listeners synchronized (mAccountsUpdatedListeners) { for (Map.Entry entry : mAccountsUpdatedListeners.entrySet()) { Account[] accountsCopy = new Account[accounts.length]; // send the listeners a copy to make sure that one doesn't // change what another sees System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length); postToHandler(entry.getValue(), entry.getKey(), accountsCopy); } } // If mAccountLookupPending was set when the account lookup finished it // means that we had previously ignored a LOGIN_ACCOUNTS_CHANGED_ACTION // intent because a lookup was already in progress. Now that we are done // with this lookup and notification pretend that another intent // was received by calling onReceive() directly. if (mAccountLookupPending) { mAccountLookupPending = false; onReceive(context, intent); return; } } }, mMainHandler); } }; /** * Add a {@link OnAccountsUpdatedListener} 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. * @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 OnAccountsUpdatedListener 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(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION); mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter); } } if (updateImmediately) { getAccounts(new Future1Callback() { public void run(Future1 future) { try { listener.onAccountsUpdated(future.getResult()); } catch (OperationCanceledException e) { // ignore } } }, handler); } } /** * Remove an {@link OnAccountsUpdatedListener} 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(OnAccountsUpdatedListener listener) { if (listener == null) { throw new IllegalArgumentException("the listener is null"); } synchronized (mAccountsUpdatedListeners) { if (mAccountsUpdatedListeners.remove(listener) == null) { throw new IllegalStateException("this listener was not previously added"); } if (mAccountsUpdatedListeners.isEmpty()) { mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver); } } } }