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