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