AccountManager.java revision ffd0cb04f97e62d286d185c520580d81a9c328b1
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    public AccountManagerFuture<Boolean> confirmPassword(final Account account, final String password,
265            AccountManagerCallback<Boolean> callback, Handler handler) {
266        return new Future2Task<Boolean>(handler, callback) {
267            public void doWork() throws RemoteException {
268                mService.confirmPassword(mResponse, account, password);
269            }
270            public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
271                if (!bundle.containsKey(Constants.BOOLEAN_RESULT_KEY)) {
272                    throw new AuthenticatorException("no result in response");
273                }
274                return bundle.getBoolean(Constants.BOOLEAN_RESULT_KEY);
275            }
276        }.start();
277    }
278
279    public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
280            final String type, final String[] features,
281            AccountManagerCallback<Account[]> callback, Handler handler) {
282        if (type == null) throw new IllegalArgumentException("type is null");
283        return new Future2Task<Account[]>(handler, callback) {
284            public void doWork() throws RemoteException {
285                mService.getAccountsByFeatures(mResponse, type, features);
286            }
287            public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException {
288                if (!bundle.containsKey(Constants.ACCOUNTS_KEY)) {
289                    throw new AuthenticatorException("no result in response");
290                }
291                final Parcelable[] parcelables = bundle.getParcelableArray(Constants.ACCOUNTS_KEY);
292                Account[] descs = new Account[parcelables.length];
293                for (int i = 0; i < parcelables.length; i++) {
294                    descs[i] = (Account) parcelables[i];
295                }
296                return descs;
297            }
298        }.start();
299    }
300
301    public AccountManagerFuture<Bundle> confirmCredentials(final Account account, final Activity activity,
302            final AccountManagerCallback<Bundle> callback,
303            final Handler handler) {
304        return new AmsTask(activity, handler, callback) {
305            public void doWork() throws RemoteException {
306                mService.confirmCredentials(mResponse, account, activity != null);
307            }
308        }.start();
309    }
310
311    public AccountManagerFuture<Bundle> updateCredentials(final Account account, final String authTokenType,
312            final Bundle loginOptions, final Activity activity,
313            final AccountManagerCallback<Bundle> callback,
314            final Handler handler) {
315        return new AmsTask(activity, handler, callback) {
316            public void doWork() throws RemoteException {
317                mService.updateCredentials(mResponse, account, authTokenType, activity != null,
318                        loginOptions);
319            }
320        }.start();
321    }
322
323    public AccountManagerFuture<Bundle> editProperties(final String accountType, 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.editProperties(mResponse, accountType, activity != null);
329            }
330        }.start();
331    }
332
333    private void ensureNotOnMainThread() {
334        final Looper looper = Looper.myLooper();
335        if (looper != null && looper == mContext.getMainLooper()) {
336            // We really want to throw an exception here, but GTalkService exercises this
337            // path quite a bit and needs some serious rewrite in order to work properly.
338            //noinspection ThrowableInstanceNeverThrow
339//            Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs",
340//                    new Exception());
341            // TODO(fredq) remove the log and throw this exception when the callers are fixed
342//            throw new IllegalStateException(
343//                    "calling this from your main thread can lead to deadlock");
344        }
345    }
346
347    private void postToHandler(Handler handler, final AccountManagerCallback<Bundle> callback,
348            final AccountManagerFuture<Bundle> future) {
349        handler = handler == null ? mMainHandler : handler;
350        handler.post(new Runnable() {
351            public void run() {
352                callback.run(future);
353            }
354        });
355    }
356
357    private void postToHandler(Handler handler, final OnAccountsUpdatedListener listener,
358            final Account[] accounts) {
359        final Account[] accountsCopy = new Account[accounts.length];
360        // send a copy to make sure that one doesn't
361        // change what another sees
362        System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
363        handler = (handler == null) ? mMainHandler : handler;
364        handler.post(new Runnable() {
365            public void run() {
366                listener.onAccountsUpdated(accountsCopy);
367            }
368        });
369    }
370
371    private abstract class AmsTask extends FutureTask<Bundle> implements AccountManagerFuture<Bundle> {
372        final IAccountManagerResponse mResponse;
373        final Handler mHandler;
374        final AccountManagerCallback<Bundle> mCallback;
375        final Activity mActivity;
376        public AmsTask(Activity activity, Handler handler, AccountManagerCallback<Bundle> callback) {
377            super(new Callable<Bundle>() {
378                public Bundle call() throws Exception {
379                    throw new IllegalStateException("this should never be called");
380                }
381            });
382
383            mHandler = handler;
384            mCallback = callback;
385            mActivity = activity;
386            mResponse = new Response();
387        }
388
389        public final AccountManagerFuture<Bundle> start() {
390            try {
391                doWork();
392            } catch (RemoteException e) {
393                setException(e);
394            }
395            return this;
396        }
397
398        public abstract void doWork() throws RemoteException;
399
400        private Bundle internalGetResult(Long timeout, TimeUnit unit)
401                throws OperationCanceledException, IOException, AuthenticatorException {
402            ensureNotOnMainThread();
403            try {
404                if (timeout == null) {
405                    return get();
406                } else {
407                    return get(timeout, unit);
408                }
409            } catch (CancellationException e) {
410                throw new OperationCanceledException();
411            } catch (TimeoutException e) {
412                // fall through and cancel
413            } catch (InterruptedException e) {
414                // fall through and cancel
415            } catch (ExecutionException e) {
416                final Throwable cause = e.getCause();
417                if (cause instanceof IOException) {
418                    throw (IOException) cause;
419                } else if (cause instanceof UnsupportedOperationException) {
420                    throw new AuthenticatorException(cause);
421                } else if (cause instanceof AuthenticatorException) {
422                    throw (AuthenticatorException) cause;
423                } else if (cause instanceof RuntimeException) {
424                    throw (RuntimeException) cause;
425                } else if (cause instanceof Error) {
426                    throw (Error) cause;
427                } else {
428                    throw new IllegalStateException(cause);
429                }
430            } finally {
431                cancel(true /* interrupt if running */);
432            }
433            throw new OperationCanceledException();
434        }
435
436        public Bundle getResult()
437                throws OperationCanceledException, IOException, AuthenticatorException {
438            return internalGetResult(null, null);
439        }
440
441        public Bundle getResult(long timeout, TimeUnit unit)
442                throws OperationCanceledException, IOException, AuthenticatorException {
443            return internalGetResult(timeout, unit);
444        }
445
446        protected void done() {
447            if (mCallback != null) {
448                postToHandler(mHandler, mCallback, this);
449            }
450        }
451
452        /** Handles the responses from the AccountManager */
453        private class Response extends IAccountManagerResponse.Stub {
454            public void onResult(Bundle bundle) {
455                Intent intent = bundle.getParcelable("intent");
456                if (intent != null && mActivity != null) {
457                    // since the user provided an Activity we will silently start intents
458                    // that we see
459                    mActivity.startActivity(intent);
460                    // leave the Future running to wait for the real response to this request
461                } else if (bundle.getBoolean("retry")) {
462                    try {
463                        doWork();
464                    } catch (RemoteException e) {
465                        // this will only happen if the system process is dead, which means
466                        // we will be dying ourselves
467                    }
468                } else {
469                    set(bundle);
470                }
471            }
472
473            public void onError(int code, String message) {
474                if (code == Constants.ERROR_CODE_CANCELED) {
475                    // the authenticator indicated that this request was canceled, do so now
476                    cancel(true /* mayInterruptIfRunning */);
477                    return;
478                }
479                setException(convertErrorToException(code, message));
480            }
481        }
482
483    }
484
485    private abstract class BaseFutureTask<T> extends FutureTask<T> {
486        final public IAccountManagerResponse mResponse;
487        final Handler mHandler;
488
489        public BaseFutureTask(Handler handler) {
490            super(new Callable<T>() {
491                public T call() throws Exception {
492                    throw new IllegalStateException("this should never be called");
493                }
494            });
495            mHandler = handler;
496            mResponse = new Response();
497        }
498
499        public abstract void doWork() throws RemoteException;
500
501        public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException;
502
503        protected void postRunnableToHandler(Runnable runnable) {
504            Handler handler = (mHandler == null) ? mMainHandler : mHandler;
505            handler.post(runnable);
506        }
507
508        protected void startTask() {
509            try {
510                doWork();
511            } catch (RemoteException e) {
512                setException(e);
513            }
514        }
515
516        protected class Response extends IAccountManagerResponse.Stub {
517            public void onResult(Bundle bundle) {
518                try {
519                    T result = bundleToResult(bundle);
520                    if (result == null) {
521                        return;
522                    }
523                    set(result);
524                    return;
525                } catch (ClassCastException e) {
526                    // we will set the exception below
527                } catch (AuthenticatorException e) {
528                    // we will set the exception below
529                }
530                onError(Constants.ERROR_CODE_INVALID_RESPONSE, "no result in response");
531            }
532
533            public void onError(int code, String message) {
534                if (code == Constants.ERROR_CODE_CANCELED) {
535                    cancel(true /* mayInterruptIfRunning */);
536                    return;
537                }
538                setException(convertErrorToException(code, message));
539            }
540        }
541    }
542
543    private abstract class Future2Task<T>
544            extends BaseFutureTask<T> implements AccountManagerFuture<T> {
545        final AccountManagerCallback<T> mCallback;
546        public Future2Task(Handler handler, AccountManagerCallback<T> callback) {
547            super(handler);
548            mCallback = callback;
549        }
550
551        protected void done() {
552            if (mCallback != null) {
553                postRunnableToHandler(new Runnable() {
554                    public void run() {
555                        mCallback.run(Future2Task.this);
556                    }
557                });
558            }
559        }
560
561        public Future2Task<T> start() {
562            startTask();
563            return this;
564        }
565
566        private T internalGetResult(Long timeout, TimeUnit unit)
567                throws OperationCanceledException, IOException, AuthenticatorException {
568            ensureNotOnMainThread();
569            try {
570                if (timeout == null) {
571                    return get();
572                } else {
573                    return get(timeout, unit);
574                }
575            } catch (InterruptedException e) {
576                // fall through and cancel
577            } catch (TimeoutException e) {
578                // fall through and cancel
579            } catch (CancellationException e) {
580                // fall through and cancel
581            } catch (ExecutionException e) {
582                final Throwable cause = e.getCause();
583                if (cause instanceof IOException) {
584                    throw (IOException) cause;
585                } else if (cause instanceof UnsupportedOperationException) {
586                    throw new AuthenticatorException(cause);
587                } else if (cause instanceof AuthenticatorException) {
588                    throw (AuthenticatorException) cause;
589                } else if (cause instanceof RuntimeException) {
590                    throw (RuntimeException) cause;
591                } else if (cause instanceof Error) {
592                    throw (Error) cause;
593                } else {
594                    throw new IllegalStateException(cause);
595                }
596            } finally {
597                cancel(true /* interrupt if running */);
598            }
599            throw new OperationCanceledException();
600        }
601
602        public T getResult()
603                throws OperationCanceledException, IOException, AuthenticatorException {
604            return internalGetResult(null, null);
605        }
606
607        public T getResult(long timeout, TimeUnit unit)
608                throws OperationCanceledException, IOException, AuthenticatorException {
609            return internalGetResult(timeout, unit);
610        }
611
612    }
613
614    private Exception convertErrorToException(int code, String message) {
615        if (code == Constants.ERROR_CODE_NETWORK_ERROR) {
616            return new IOException(message);
617        }
618
619        if (code == Constants.ERROR_CODE_UNSUPPORTED_OPERATION) {
620            return new UnsupportedOperationException(message);
621        }
622
623        if (code == Constants.ERROR_CODE_INVALID_RESPONSE) {
624            return new AuthenticatorException(message);
625        }
626
627        if (code == Constants.ERROR_CODE_BAD_ARGUMENTS) {
628            return new IllegalArgumentException(message);
629        }
630
631        return new AuthenticatorException(message);
632    }
633
634    private class GetAuthTokenByTypeAndFeaturesTask
635            extends AmsTask implements AccountManagerCallback<Bundle> {
636        GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
637                final String[] features, Activity activityForPrompting,
638                final Bundle addAccountOptions, final Bundle loginOptions,
639                AccountManagerCallback<Bundle> callback, Handler handler) {
640            super(activityForPrompting, handler, callback);
641            if (accountType == null) throw new IllegalArgumentException("account type is null");
642            mAccountType = accountType;
643            mAuthTokenType = authTokenType;
644            mFeatures = features;
645            mAddAccountOptions = addAccountOptions;
646            mLoginOptions = loginOptions;
647            mMyCallback = this;
648        }
649        volatile AccountManagerFuture<Bundle> mFuture = null;
650        final String mAccountType;
651        final String mAuthTokenType;
652        final String[] mFeatures;
653        final Bundle mAddAccountOptions;
654        final Bundle mLoginOptions;
655        final AccountManagerCallback<Bundle> mMyCallback;
656
657        public void doWork() throws RemoteException {
658            getAccountsByTypeAndFeatures(mAccountType, mFeatures,
659                    new AccountManagerCallback<Account[]>() {
660                        public void run(AccountManagerFuture<Account[]> future) {
661                            Account[] accounts;
662                            try {
663                                accounts = future.getResult();
664                            } catch (OperationCanceledException e) {
665                                setException(e);
666                                return;
667                            } catch (IOException e) {
668                                setException(e);
669                                return;
670                            } catch (AuthenticatorException e) {
671                                setException(e);
672                                return;
673                            }
674
675                            if (accounts.length == 0) {
676                                if (mActivity != null) {
677                                    // no accounts, add one now. pretend that the user directly
678                                    // made this request
679                                    mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures,
680                                            mAddAccountOptions, mActivity, mMyCallback, mHandler);
681                                } else {
682                                    // send result since we can't prompt to add an account
683                                    Bundle result = new Bundle();
684                                    result.putString(Constants.ACCOUNT_NAME_KEY, null);
685                                    result.putString(Constants.ACCOUNT_TYPE_KEY, null);
686                                    result.putString(Constants.AUTHTOKEN_KEY, null);
687                                    try {
688                                        mResponse.onResult(result);
689                                    } catch (RemoteException e) {
690                                        // this will never happen
691                                    }
692                                    // we are done
693                                }
694                            } else if (accounts.length == 1) {
695                                // have a single account, return an authtoken for it
696                                if (mActivity == null) {
697                                    mFuture = getAuthToken(accounts[0], mAuthTokenType,
698                                            false /* notifyAuthFailure */, mMyCallback, mHandler);
699                                } else {
700                                    mFuture = getAuthToken(accounts[0],
701                                            mAuthTokenType, mLoginOptions,
702                                            mActivity, mMyCallback, mHandler);
703                                }
704                            } else {
705                                if (mActivity != null) {
706                                    IAccountManagerResponse chooseResponse =
707                                            new IAccountManagerResponse.Stub() {
708                                        public void onResult(Bundle value) throws RemoteException {
709                                            Account account = new Account(
710                                                    value.getString(Constants.ACCOUNT_NAME_KEY),
711                                                    value.getString(Constants.ACCOUNT_TYPE_KEY));
712                                            mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
713                                                    mActivity, mMyCallback, mHandler);
714                                        }
715
716                                        public void onError(int errorCode, String errorMessage)
717                                                throws RemoteException {
718                                            mResponse.onError(errorCode, errorMessage);
719                                        }
720                                    };
721                                    // have many accounts, launch the chooser
722                                    Intent intent = new Intent();
723                                    intent.setClassName("android",
724                                            "android.accounts.ChooseAccountActivity");
725                                    intent.putExtra(Constants.ACCOUNTS_KEY, accounts);
726                                    intent.putExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY,
727                                            new AccountManagerResponse(chooseResponse));
728                                    mActivity.startActivity(intent);
729                                    // the result will arrive via the IAccountManagerResponse
730                                } else {
731                                    // send result since we can't prompt to select an account
732                                    Bundle result = new Bundle();
733                                    result.putString(Constants.ACCOUNTS_KEY, null);
734                                    try {
735                                        mResponse.onResult(result);
736                                    } catch (RemoteException e) {
737                                        // this will never happen
738                                    }
739                                    // we are done
740                                }
741                            }
742                        }}, mHandler);
743        }
744
745
746
747        // TODO(fredq) pass through the calls to our implemention of Future2 to the underlying
748        // future that we create. We need to do things like have cancel cancel the mFuture, if set
749        // or to cause this to be canceled if mFuture isn't set.
750        // Once this is done then getAuthTokenByFeatures can be changed to return a Future2.
751
752        public void run(AccountManagerFuture<Bundle> future) {
753            try {
754                set(future.get());
755            } catch (InterruptedException e) {
756                cancel(true);
757            } catch (CancellationException e) {
758                cancel(true);
759            } catch (ExecutionException e) {
760                setException(e.getCause());
761            }
762        }
763    }
764
765    public void getAuthTokenByFeatures(
766            final String accountType, final String authTokenType, final String[] features,
767            final Activity activityForPrompting, final Bundle addAccountOptions,
768            final Bundle loginOptions,
769            final AccountManagerCallback<Bundle> callback, final Handler handler) {
770        if (accountType == null) throw new IllegalArgumentException("account type is null");
771        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
772        new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType,  features,
773                activityForPrompting, addAccountOptions, loginOptions, callback, handler).start();
774    }
775
776    private final HashMap<OnAccountsUpdatedListener, Handler> mAccountsUpdatedListeners =
777            Maps.newHashMap();
778
779    /**
780     * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent
781     * so that it can read the updated list of accounts and send them to the listener
782     * in mAccountsUpdatedListeners.
783     */
784    private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() {
785        public void onReceive(final Context context, final Intent intent) {
786            final Account[] accounts = getAccounts();
787            // send the result to the listeners
788            synchronized (mAccountsUpdatedListeners) {
789                for (Map.Entry<OnAccountsUpdatedListener, Handler> entry :
790                        mAccountsUpdatedListeners.entrySet()) {
791                    postToHandler(entry.getValue(), entry.getKey(), accounts);
792                }
793            }
794        }
795    };
796
797    /**
798     * Add a {@link OnAccountsUpdatedListener} to this instance of the {@link AccountManager}.
799     * The listener is guaranteed to be invoked on the thread of the Handler that is passed
800     * in or the main thread's Handler if handler is null.
801     * @param listener the listener to add
802     * @param handler the Handler whose thread will be used to invoke the listener. If null
803     * the AccountManager context's main thread will be used.
804     * @param updateImmediately if true then the listener will be invoked as a result of this
805     * call.
806     * @throws IllegalArgumentException if listener is null
807     * @throws IllegalStateException if listener was already added
808     */
809    public void addOnAccountsUpdatedListener(final OnAccountsUpdatedListener listener,
810            Handler handler, boolean updateImmediately) {
811        if (listener == null) {
812            throw new IllegalArgumentException("the listener is null");
813        }
814        synchronized (mAccountsUpdatedListeners) {
815            if (mAccountsUpdatedListeners.containsKey(listener)) {
816                throw new IllegalStateException("this listener is already added");
817            }
818            final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty();
819
820            mAccountsUpdatedListeners.put(listener, handler);
821
822            if (wasEmpty) {
823                // Register a broadcast receiver to monitor account changes
824                IntentFilter intentFilter = new IntentFilter();
825                intentFilter.addAction(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION);
826                mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
827            }
828        }
829
830        if (updateImmediately) {
831            postToHandler(handler, listener, getAccounts());
832        }
833    }
834
835    /**
836     * Remove an {@link OnAccountsUpdatedListener} that was previously registered with
837     * {@link #addOnAccountsUpdatedListener}.
838     * @param listener the listener to remove
839     * @throws IllegalArgumentException if listener is null
840     * @throws IllegalStateException if listener was not already added
841     */
842    public void removeOnAccountsUpdatedListener(OnAccountsUpdatedListener listener) {
843        if (listener == null) {
844            throw new IllegalArgumentException("the listener is null");
845        }
846        synchronized (mAccountsUpdatedListeners) {
847            if (mAccountsUpdatedListeners.remove(listener) == null) {
848                throw new IllegalStateException("this listener was not previously added");
849            }
850            if (mAccountsUpdatedListeners.isEmpty()) {
851                mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
852            }
853        }
854    }
855}
856