AccountManager.java revision 4db3a5b327038e4dc1bc58e3213bb9ad0719bcc1
1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.accounts;
18
19import android.app.Activity;
20import android.content.Intent;
21import android.content.Context;
22import android.content.IntentFilter;
23import android.content.BroadcastReceiver;
24import android.database.SQLException;
25import android.os.Bundle;
26import android.os.Handler;
27import android.os.Looper;
28import android.os.RemoteException;
29import android.os.Parcelable;
30import android.util.Log;
31
32import java.io.IOException;
33import java.util.concurrent.Callable;
34import java.util.concurrent.CancellationException;
35import java.util.concurrent.ExecutionException;
36import java.util.concurrent.FutureTask;
37import java.util.concurrent.TimeoutException;
38import java.util.concurrent.TimeUnit;
39import java.util.HashMap;
40import java.util.Map;
41
42import com.google.android.collect.Maps;
43
44/**
45 * A class that helps with interactions with the AccountManagerService. It provides
46 * methods to allow for account, password, and authtoken management for all accounts on the
47 * device. One accesses the {@link AccountManager} by calling:
48 *    AccountManager accountManager = AccountManager.get(context);
49 *
50 * <p>
51 * TODO(fredq) this interface is still in flux
52 */
53public class AccountManager {
54    private static final String TAG = "AccountManager";
55
56    public static final int ERROR_CODE_REMOTE_EXCEPTION = 1;
57    public static final int ERROR_CODE_NETWORK_ERROR = 3;
58    public static final int ERROR_CODE_CANCELED = 4;
59    public static final int ERROR_CODE_INVALID_RESPONSE = 5;
60    public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6;
61    public static final int ERROR_CODE_BAD_ARGUMENTS = 7;
62    public static final int ERROR_CODE_BAD_REQUEST = 8;
63    public static final String KEY_ACCOUNTS = "accounts";
64    public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types";
65    public static final String KEY_USERDATA = "userdata";
66    public static final String KEY_AUTHTOKEN = "authtoken";
67    public static final String KEY_PASSWORD = "password";
68    public static final String KEY_ACCOUNT_NAME = "authAccount";
69    public static final String KEY_ACCOUNT_TYPE = "accountType";
70    public static final String KEY_ERROR_CODE = "errorCode";
71    public static final String KEY_ERROR_MESSAGE = "errorMessage";
72    public static final String KEY_INTENT = "intent";
73    public static final String KEY_BOOLEAN_RESULT = "booleanResult";
74    public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse";
75    public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse";
76    public static final String KEY_AUTH_FAILED_MESSAGE = "authFailedMessage";
77    public static final String KEY_AUTH_TOKEN_LABEL = "authTokenLabelKey";
78    public static final String ACTION_AUTHENTICATOR_INTENT =
79            "android.accounts.AccountAuthenticator";
80    public static final String AUTHENTICATOR_META_DATA_NAME =
81                    "android.accounts.AccountAuthenticator";
82    public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
83
84    private final Context mContext;
85    private final IAccountManager mService;
86    private final Handler mMainHandler;
87    /**
88     * Action sent as a broadcast Intent by the AccountsService
89     * when accounts are added to and/or removed from the device's
90     * database.
91     */
92    public static final String LOGIN_ACCOUNTS_CHANGED_ACTION =
93        "android.accounts.LOGIN_ACCOUNTS_CHANGED";
94
95    /**
96     * @hide
97     */
98    public AccountManager(Context context, IAccountManager service) {
99        mContext = context;
100        mService = service;
101        mMainHandler = new Handler(mContext.getMainLooper());
102    }
103
104    /**
105     * @hide used for testing only
106     */
107    public AccountManager(Context context, IAccountManager service, Handler handler) {
108        mContext = context;
109        mService = service;
110        mMainHandler = handler;
111    }
112
113    public static AccountManager get(Context context) {
114        return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
115    }
116
117    public String getPassword(final Account account) {
118        try {
119            return mService.getPassword(account);
120        } catch (RemoteException e) {
121            // will never happen
122            throw new RuntimeException(e);
123        }
124    }
125
126    public String getUserData(final Account account, final String key) {
127        try {
128            return mService.getUserData(account, key);
129        } catch (RemoteException e) {
130            // will never happen
131            throw new RuntimeException(e);
132        }
133    }
134
135    public AuthenticatorDescription[] getAuthenticatorTypes() {
136        try {
137            return mService.getAuthenticatorTypes();
138        } catch (RemoteException e) {
139            // will never happen
140            throw new RuntimeException(e);
141        }
142    }
143
144    public Account[] getAccounts() {
145        try {
146            return mService.getAccounts(null);
147        } catch (RemoteException e) {
148            // won't ever happen
149            throw new RuntimeException(e);
150        }
151    }
152
153    public Account[] getAccountsByType(String type) {
154        try {
155            return mService.getAccounts(type);
156        } catch (RemoteException e) {
157            // won't ever happen
158            throw new RuntimeException(e);
159        }
160    }
161
162    public boolean addAccountExplicitly(Account account, String password, Bundle extras) {
163        try {
164            return mService.addAccount(account, password, extras);
165        } catch (RemoteException e) {
166            // won't ever happen
167            throw new RuntimeException(e);
168        }
169    }
170
171    public AccountManagerFuture<Boolean> removeAccount(final Account account,
172            AccountManagerCallback<Boolean> callback, Handler handler) {
173        return new Future2Task<Boolean>(handler, callback) {
174            public void doWork() throws RemoteException {
175                mService.removeAccount(mResponse, account);
176            }
177            public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
178                if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) {
179                    throw new AuthenticatorException("no result in response");
180                }
181                return bundle.getBoolean(KEY_BOOLEAN_RESULT);
182            }
183        }.start();
184    }
185
186    public void invalidateAuthToken(final String accountType, final String authToken) {
187        try {
188            mService.invalidateAuthToken(accountType, authToken);
189        } catch (RemoteException e) {
190            // won't ever happen
191            throw new RuntimeException(e);
192        }
193    }
194
195    public String peekAuthToken(final Account account, final String authTokenType) {
196        try {
197            return mService.peekAuthToken(account, authTokenType);
198        } catch (RemoteException e) {
199            // won't ever happen
200            throw new RuntimeException(e);
201        }
202    }
203
204    public void setPassword(final Account account, final String password) {
205        try {
206            mService.setPassword(account, password);
207        } catch (RemoteException e) {
208            // won't ever happen
209            throw new RuntimeException(e);
210        }
211    }
212
213    public void clearPassword(final Account account) {
214        try {
215            mService.clearPassword(account);
216        } catch (RemoteException e) {
217            // won't ever happen
218            throw new RuntimeException(e);
219        }
220    }
221
222    public void setUserData(final Account account, final String key, final String value) {
223        try {
224            mService.setUserData(account, key, value);
225        } catch (RemoteException e) {
226            // won't ever happen
227            throw new RuntimeException(e);
228        }
229    }
230
231    public void setAuthToken(Account account, final String authTokenType, final String authToken) {
232        try {
233            mService.setAuthToken(account, authTokenType, authToken);
234        } catch (RemoteException e) {
235            // won't ever happen
236            throw new RuntimeException(e);
237        }
238    }
239
240    public String blockingGetAuthToken(Account account, String authTokenType,
241            boolean notifyAuthFailure)
242            throws OperationCanceledException, IOException, AuthenticatorException {
243        Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */,
244                null /* handler */).getResult();
245        return bundle.getString(KEY_AUTHTOKEN);
246    }
247
248    /**
249     * Request the auth token for this account/authTokenType. If this succeeds then the
250     * auth token will then be passed to the activity. If this results in an authentication
251     * failure then a login intent will be returned that can be invoked to prompt the user to
252     * update their credentials. This login activity will return the auth token to the calling
253     * activity. If activity is null then the login intent will not be invoked.
254     *
255     * @param account the account whose auth token should be retrieved
256     * @param authTokenType the auth token type that should be retrieved
257     * @param loginOptions
258     * @param activity the activity to launch the login intent, if necessary, and to which
259     */
260    public AccountManagerFuture<Bundle> getAuthToken(
261            final Account account, final String authTokenType, final Bundle loginOptions,
262            final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
263        if (activity == null) throw new IllegalArgumentException("activity is null");
264        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
265        return new AmsTask(activity, handler, callback) {
266            public void doWork() throws RemoteException {
267                mService.getAuthToken(mResponse, account, authTokenType,
268                        false /* notifyOnAuthFailure */, true /* expectActivityLaunch */,
269                        loginOptions);
270            }
271        }.start();
272    }
273
274    public AccountManagerFuture<Bundle> getAuthToken(
275            final Account account, final String authTokenType, final boolean notifyAuthFailure,
276            AccountManagerCallback<Bundle> callback, Handler handler) {
277        if (account == null) throw new IllegalArgumentException("account is null");
278        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
279        return new AmsTask(null, handler, callback) {
280            public void doWork() throws RemoteException {
281                mService.getAuthToken(mResponse, account, authTokenType,
282                        notifyAuthFailure, false /* expectActivityLaunch */, null /* options */);
283            }
284        }.start();
285    }
286
287    public AccountManagerFuture<Bundle> addAccount(final String accountType,
288            final String authTokenType, final String[] requiredFeatures,
289            final Bundle addAccountOptions,
290            final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
291        return new AmsTask(activity, handler, callback) {
292            public void doWork() throws RemoteException {
293                mService.addAcount(mResponse, accountType, authTokenType,
294                        requiredFeatures, activity != null, addAccountOptions);
295            }
296        }.start();
297    }
298
299    public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
300            final String type, final String[] features,
301            AccountManagerCallback<Account[]> callback, Handler handler) {
302        if (type == null) throw new IllegalArgumentException("type is null");
303        return new Future2Task<Account[]>(handler, callback) {
304            public void doWork() throws RemoteException {
305                mService.getAccountsByFeatures(mResponse, type, features);
306            }
307            public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException {
308                if (!bundle.containsKey(KEY_ACCOUNTS)) {
309                    throw new AuthenticatorException("no result in response");
310                }
311                final Parcelable[] parcelables = bundle.getParcelableArray(KEY_ACCOUNTS);
312                Account[] descs = new Account[parcelables.length];
313                for (int i = 0; i < parcelables.length; i++) {
314                    descs[i] = (Account) parcelables[i];
315                }
316                return descs;
317            }
318        }.start();
319    }
320
321    public AccountManagerFuture<Bundle> confirmCredentials(final Account account,
322            final Bundle options,
323            final Activity activity,
324            final AccountManagerCallback<Bundle> callback,
325            final Handler handler) {
326        return new AmsTask(activity, handler, callback) {
327            public void doWork() throws RemoteException {
328                mService.confirmCredentials(mResponse, account, options, activity != null);
329            }
330        }.start();
331    }
332
333    public AccountManagerFuture<Bundle> updateCredentials(final Account account, final String authTokenType,
334            final Bundle loginOptions, final Activity activity,
335            final AccountManagerCallback<Bundle> callback,
336            final Handler handler) {
337        return new AmsTask(activity, handler, callback) {
338            public void doWork() throws RemoteException {
339                mService.updateCredentials(mResponse, account, authTokenType, activity != null,
340                        loginOptions);
341            }
342        }.start();
343    }
344
345    public AccountManagerFuture<Bundle> editProperties(final String accountType, final Activity activity,
346            final AccountManagerCallback<Bundle> callback,
347            final Handler handler) {
348        return new AmsTask(activity, handler, callback) {
349            public void doWork() throws RemoteException {
350                mService.editProperties(mResponse, accountType, activity != null);
351            }
352        }.start();
353    }
354
355    private void ensureNotOnMainThread() {
356        final Looper looper = Looper.myLooper();
357        if (looper != null && looper == mContext.getMainLooper()) {
358            // We really want to throw an exception here, but GTalkService exercises this
359            // path quite a bit and needs some serious rewrite in order to work properly.
360            //noinspection ThrowableInstanceNeverThrow
361//            Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs",
362//                    new Exception());
363            // TODO(fredq) remove the log and throw this exception when the callers are fixed
364//            throw new IllegalStateException(
365//                    "calling this from your main thread can lead to deadlock");
366        }
367    }
368
369    private void postToHandler(Handler handler, final AccountManagerCallback<Bundle> callback,
370            final AccountManagerFuture<Bundle> future) {
371        handler = handler == null ? mMainHandler : handler;
372        handler.post(new Runnable() {
373            public void run() {
374                callback.run(future);
375            }
376        });
377    }
378
379    private void postToHandler(Handler handler, final OnAccountsUpdateListener listener,
380            final Account[] accounts) {
381        final Account[] accountsCopy = new Account[accounts.length];
382        // send a copy to make sure that one doesn't
383        // change what another sees
384        System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
385        handler = (handler == null) ? mMainHandler : handler;
386        handler.post(new Runnable() {
387            public void run() {
388                try {
389                    listener.onAccountsUpdated(accountsCopy);
390                } catch (SQLException e) {
391                    // Better luck next time.  If the problem was disk-full,
392                    // the STORAGE_OK intent will re-trigger the update.
393                    Log.e(TAG, "Can't update accounts", e);
394                }
395            }
396        });
397    }
398
399    private abstract class AmsTask extends FutureTask<Bundle> implements AccountManagerFuture<Bundle> {
400        final IAccountManagerResponse mResponse;
401        final Handler mHandler;
402        final AccountManagerCallback<Bundle> mCallback;
403        final Activity mActivity;
404        public AmsTask(Activity activity, Handler handler, AccountManagerCallback<Bundle> callback) {
405            super(new Callable<Bundle>() {
406                public Bundle call() throws Exception {
407                    throw new IllegalStateException("this should never be called");
408                }
409            });
410
411            mHandler = handler;
412            mCallback = callback;
413            mActivity = activity;
414            mResponse = new Response();
415        }
416
417        public final AccountManagerFuture<Bundle> start() {
418            try {
419                doWork();
420            } catch (RemoteException e) {
421                setException(e);
422            }
423            return this;
424        }
425
426        public abstract void doWork() throws RemoteException;
427
428        private Bundle internalGetResult(Long timeout, TimeUnit unit)
429                throws OperationCanceledException, IOException, AuthenticatorException {
430            ensureNotOnMainThread();
431            try {
432                if (timeout == null) {
433                    return get();
434                } else {
435                    return get(timeout, unit);
436                }
437            } catch (CancellationException e) {
438                throw new OperationCanceledException();
439            } catch (TimeoutException e) {
440                // fall through and cancel
441            } catch (InterruptedException e) {
442                // fall through and cancel
443            } catch (ExecutionException e) {
444                final Throwable cause = e.getCause();
445                if (cause instanceof IOException) {
446                    throw (IOException) cause;
447                } else if (cause instanceof UnsupportedOperationException) {
448                    throw new AuthenticatorException(cause);
449                } else if (cause instanceof AuthenticatorException) {
450                    throw (AuthenticatorException) cause;
451                } else if (cause instanceof RuntimeException) {
452                    throw (RuntimeException) cause;
453                } else if (cause instanceof Error) {
454                    throw (Error) cause;
455                } else {
456                    throw new IllegalStateException(cause);
457                }
458            } finally {
459                cancel(true /* interrupt if running */);
460            }
461            throw new OperationCanceledException();
462        }
463
464        public Bundle getResult()
465                throws OperationCanceledException, IOException, AuthenticatorException {
466            return internalGetResult(null, null);
467        }
468
469        public Bundle getResult(long timeout, TimeUnit unit)
470                throws OperationCanceledException, IOException, AuthenticatorException {
471            return internalGetResult(timeout, unit);
472        }
473
474        protected void done() {
475            if (mCallback != null) {
476                postToHandler(mHandler, mCallback, this);
477            }
478        }
479
480        /** Handles the responses from the AccountManager */
481        private class Response extends IAccountManagerResponse.Stub {
482            public void onResult(Bundle bundle) {
483                Intent intent = bundle.getParcelable("intent");
484                if (intent != null && mActivity != null) {
485                    // since the user provided an Activity we will silently start intents
486                    // that we see
487                    mActivity.startActivity(intent);
488                    // leave the Future running to wait for the real response to this request
489                } else if (bundle.getBoolean("retry")) {
490                    try {
491                        doWork();
492                    } catch (RemoteException e) {
493                        // this will only happen if the system process is dead, which means
494                        // we will be dying ourselves
495                    }
496                } else {
497                    set(bundle);
498                }
499            }
500
501            public void onError(int code, String message) {
502                if (code == ERROR_CODE_CANCELED) {
503                    // the authenticator indicated that this request was canceled, do so now
504                    cancel(true /* mayInterruptIfRunning */);
505                    return;
506                }
507                setException(convertErrorToException(code, message));
508            }
509        }
510
511    }
512
513    private abstract class BaseFutureTask<T> extends FutureTask<T> {
514        final public IAccountManagerResponse mResponse;
515        final Handler mHandler;
516
517        public BaseFutureTask(Handler handler) {
518            super(new Callable<T>() {
519                public T call() throws Exception {
520                    throw new IllegalStateException("this should never be called");
521                }
522            });
523            mHandler = handler;
524            mResponse = new Response();
525        }
526
527        public abstract void doWork() throws RemoteException;
528
529        public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException;
530
531        protected void postRunnableToHandler(Runnable runnable) {
532            Handler handler = (mHandler == null) ? mMainHandler : mHandler;
533            handler.post(runnable);
534        }
535
536        protected void startTask() {
537            try {
538                doWork();
539            } catch (RemoteException e) {
540                setException(e);
541            }
542        }
543
544        protected class Response extends IAccountManagerResponse.Stub {
545            public void onResult(Bundle bundle) {
546                try {
547                    T result = bundleToResult(bundle);
548                    if (result == null) {
549                        return;
550                    }
551                    set(result);
552                    return;
553                } catch (ClassCastException e) {
554                    // we will set the exception below
555                } catch (AuthenticatorException e) {
556                    // we will set the exception below
557                }
558                onError(ERROR_CODE_INVALID_RESPONSE, "no result in response");
559            }
560
561            public void onError(int code, String message) {
562                if (code == ERROR_CODE_CANCELED) {
563                    cancel(true /* mayInterruptIfRunning */);
564                    return;
565                }
566                setException(convertErrorToException(code, message));
567            }
568        }
569    }
570
571    private abstract class Future2Task<T>
572            extends BaseFutureTask<T> implements AccountManagerFuture<T> {
573        final AccountManagerCallback<T> mCallback;
574        public Future2Task(Handler handler, AccountManagerCallback<T> callback) {
575            super(handler);
576            mCallback = callback;
577        }
578
579        protected void done() {
580            if (mCallback != null) {
581                postRunnableToHandler(new Runnable() {
582                    public void run() {
583                        mCallback.run(Future2Task.this);
584                    }
585                });
586            }
587        }
588
589        public Future2Task<T> start() {
590            startTask();
591            return this;
592        }
593
594        private T internalGetResult(Long timeout, TimeUnit unit)
595                throws OperationCanceledException, IOException, AuthenticatorException {
596            ensureNotOnMainThread();
597            try {
598                if (timeout == null) {
599                    return get();
600                } else {
601                    return get(timeout, unit);
602                }
603            } catch (InterruptedException e) {
604                // fall through and cancel
605            } catch (TimeoutException e) {
606                // fall through and cancel
607            } catch (CancellationException e) {
608                // fall through and cancel
609            } catch (ExecutionException e) {
610                final Throwable cause = e.getCause();
611                if (cause instanceof IOException) {
612                    throw (IOException) cause;
613                } else if (cause instanceof UnsupportedOperationException) {
614                    throw new AuthenticatorException(cause);
615                } else if (cause instanceof AuthenticatorException) {
616                    throw (AuthenticatorException) cause;
617                } else if (cause instanceof RuntimeException) {
618                    throw (RuntimeException) cause;
619                } else if (cause instanceof Error) {
620                    throw (Error) cause;
621                } else {
622                    throw new IllegalStateException(cause);
623                }
624            } finally {
625                cancel(true /* interrupt if running */);
626            }
627            throw new OperationCanceledException();
628        }
629
630        public T getResult()
631                throws OperationCanceledException, IOException, AuthenticatorException {
632            return internalGetResult(null, null);
633        }
634
635        public T getResult(long timeout, TimeUnit unit)
636                throws OperationCanceledException, IOException, AuthenticatorException {
637            return internalGetResult(timeout, unit);
638        }
639
640    }
641
642    private Exception convertErrorToException(int code, String message) {
643        if (code == ERROR_CODE_NETWORK_ERROR) {
644            return new IOException(message);
645        }
646
647        if (code == ERROR_CODE_UNSUPPORTED_OPERATION) {
648            return new UnsupportedOperationException(message);
649        }
650
651        if (code == ERROR_CODE_INVALID_RESPONSE) {
652            return new AuthenticatorException(message);
653        }
654
655        if (code == ERROR_CODE_BAD_ARGUMENTS) {
656            return new IllegalArgumentException(message);
657        }
658
659        return new AuthenticatorException(message);
660    }
661
662    private class GetAuthTokenByTypeAndFeaturesTask
663            extends AmsTask implements AccountManagerCallback<Bundle> {
664        GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
665                final String[] features, Activity activityForPrompting,
666                final Bundle addAccountOptions, final Bundle loginOptions,
667                AccountManagerCallback<Bundle> callback, Handler handler) {
668            super(activityForPrompting, handler, callback);
669            if (accountType == null) throw new IllegalArgumentException("account type is null");
670            mAccountType = accountType;
671            mAuthTokenType = authTokenType;
672            mFeatures = features;
673            mAddAccountOptions = addAccountOptions;
674            mLoginOptions = loginOptions;
675            mMyCallback = this;
676        }
677        volatile AccountManagerFuture<Bundle> mFuture = null;
678        final String mAccountType;
679        final String mAuthTokenType;
680        final String[] mFeatures;
681        final Bundle mAddAccountOptions;
682        final Bundle mLoginOptions;
683        final AccountManagerCallback<Bundle> mMyCallback;
684
685        public void doWork() throws RemoteException {
686            getAccountsByTypeAndFeatures(mAccountType, mFeatures,
687                    new AccountManagerCallback<Account[]>() {
688                        public void run(AccountManagerFuture<Account[]> future) {
689                            Account[] accounts;
690                            try {
691                                accounts = future.getResult();
692                            } catch (OperationCanceledException e) {
693                                setException(e);
694                                return;
695                            } catch (IOException e) {
696                                setException(e);
697                                return;
698                            } catch (AuthenticatorException e) {
699                                setException(e);
700                                return;
701                            }
702
703                            if (accounts.length == 0) {
704                                if (mActivity != null) {
705                                    // no accounts, add one now. pretend that the user directly
706                                    // made this request
707                                    mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures,
708                                            mAddAccountOptions, mActivity, mMyCallback, mHandler);
709                                } else {
710                                    // send result since we can't prompt to add an account
711                                    Bundle result = new Bundle();
712                                    result.putString(KEY_ACCOUNT_NAME, null);
713                                    result.putString(KEY_ACCOUNT_TYPE, null);
714                                    result.putString(KEY_AUTHTOKEN, null);
715                                    try {
716                                        mResponse.onResult(result);
717                                    } catch (RemoteException e) {
718                                        // this will never happen
719                                    }
720                                    // we are done
721                                }
722                            } else if (accounts.length == 1) {
723                                // have a single account, return an authtoken for it
724                                if (mActivity == null) {
725                                    mFuture = getAuthToken(accounts[0], mAuthTokenType,
726                                            false /* notifyAuthFailure */, mMyCallback, mHandler);
727                                } else {
728                                    mFuture = getAuthToken(accounts[0],
729                                            mAuthTokenType, mLoginOptions,
730                                            mActivity, mMyCallback, mHandler);
731                                }
732                            } else {
733                                if (mActivity != null) {
734                                    IAccountManagerResponse chooseResponse =
735                                            new IAccountManagerResponse.Stub() {
736                                        public void onResult(Bundle value) throws RemoteException {
737                                            Account account = new Account(
738                                                    value.getString(KEY_ACCOUNT_NAME),
739                                                    value.getString(KEY_ACCOUNT_TYPE));
740                                            mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
741                                                    mActivity, mMyCallback, mHandler);
742                                        }
743
744                                        public void onError(int errorCode, String errorMessage)
745                                                throws RemoteException {
746                                            mResponse.onError(errorCode, errorMessage);
747                                        }
748                                    };
749                                    // have many accounts, launch the chooser
750                                    Intent intent = new Intent();
751                                    intent.setClassName("android",
752                                            "android.accounts.ChooseAccountActivity");
753                                    intent.putExtra(KEY_ACCOUNTS, accounts);
754                                    intent.putExtra(KEY_ACCOUNT_MANAGER_RESPONSE,
755                                            new AccountManagerResponse(chooseResponse));
756                                    mActivity.startActivity(intent);
757                                    // the result will arrive via the IAccountManagerResponse
758                                } else {
759                                    // send result since we can't prompt to select an account
760                                    Bundle result = new Bundle();
761                                    result.putString(KEY_ACCOUNTS, null);
762                                    try {
763                                        mResponse.onResult(result);
764                                    } catch (RemoteException e) {
765                                        // this will never happen
766                                    }
767                                    // we are done
768                                }
769                            }
770                        }}, mHandler);
771        }
772
773        public void run(AccountManagerFuture<Bundle> future) {
774            try {
775                set(future.getResult());
776            } catch (OperationCanceledException e) {
777                cancel(true /* mayInterruptIfRUnning */);
778            } catch (IOException e) {
779                setException(e);
780            } catch (AuthenticatorException e) {
781                setException(e);
782            }
783        }
784    }
785
786    public AccountManagerFuture<Bundle> getAuthTokenByFeatures(
787            final String accountType, final String authTokenType, final String[] features,
788            final Activity activityForPrompting, final Bundle addAccountOptions,
789            final Bundle loginOptions,
790            final AccountManagerCallback<Bundle> callback, final Handler handler) {
791        if (accountType == null) throw new IllegalArgumentException("account type is null");
792        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
793        final GetAuthTokenByTypeAndFeaturesTask task =
794                new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features,
795                activityForPrompting, addAccountOptions, loginOptions, callback, handler);
796        task.start();
797        return task;
798    }
799
800    private final HashMap<OnAccountsUpdateListener, Handler> mAccountsUpdatedListeners =
801            Maps.newHashMap();
802
803    /**
804     * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent
805     * so that it can read the updated list of accounts and send them to the listener
806     * in mAccountsUpdatedListeners.
807     */
808    private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() {
809        public void onReceive(final Context context, final Intent intent) {
810            final Account[] accounts = getAccounts();
811            // send the result to the listeners
812            synchronized (mAccountsUpdatedListeners) {
813                for (Map.Entry<OnAccountsUpdateListener, Handler> entry :
814                        mAccountsUpdatedListeners.entrySet()) {
815                    postToHandler(entry.getValue(), entry.getKey(), accounts);
816                }
817            }
818        }
819    };
820
821    /**
822     * Add a {@link OnAccountsUpdateListener} to this instance of the {@link AccountManager}.
823     * The listener is guaranteed to be invoked on the thread of the Handler that is passed
824     * in or the main thread's Handler if handler is null.
825     * <p>
826     * You must remove this listener before the context that was used to retrieve this
827     * {@link AccountManager} instance goes away. This generally means when the Activity
828     * or Service you are running is stopped.
829     * @param listener the listener to add
830     * @param handler the Handler whose thread will be used to invoke the listener. If null
831     * the AccountManager context's main thread will be used.
832     * @param updateImmediately if true then the listener will be invoked as a result of this
833     * call.
834     * @throws IllegalArgumentException if listener is null
835     * @throws IllegalStateException if listener was already added
836     */
837    public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener,
838            Handler handler, boolean updateImmediately) {
839        if (listener == null) {
840            throw new IllegalArgumentException("the listener is null");
841        }
842        synchronized (mAccountsUpdatedListeners) {
843            if (mAccountsUpdatedListeners.containsKey(listener)) {
844                throw new IllegalStateException("this listener is already added");
845            }
846            final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty();
847
848            mAccountsUpdatedListeners.put(listener, handler);
849
850            if (wasEmpty) {
851                // Register a broadcast receiver to monitor account changes
852                IntentFilter intentFilter = new IntentFilter();
853                intentFilter.addAction(LOGIN_ACCOUNTS_CHANGED_ACTION);
854                // To recover from disk-full.
855                intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
856                mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
857            }
858        }
859
860        if (updateImmediately) {
861            postToHandler(handler, listener, getAccounts());
862        }
863    }
864
865    /**
866     * Remove an {@link OnAccountsUpdateListener} that was previously registered with
867     * {@link #addOnAccountsUpdatedListener}.
868     * @param listener the listener to remove
869     * @throws IllegalArgumentException if listener is null
870     * @throws IllegalStateException if listener was not already added
871     */
872    public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
873        if (listener == null) {
874            throw new IllegalArgumentException("the listener is null");
875        }
876        synchronized (mAccountsUpdatedListeners) {
877            if (!mAccountsUpdatedListeners.containsKey(listener)) {
878                throw new IllegalStateException("this listener was not previously added");
879            }
880            mAccountsUpdatedListeners.remove(listener);
881            if (mAccountsUpdatedListeners.isEmpty()) {
882                mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
883            }
884        }
885    }
886}
887