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