AccountManager.java revision d9d2f1140b52fd0c014e9deac59f6000564b7e84
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;
29import android.util.Config;
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    public static AccountManager get(Context context) {
71        return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
72    }
73
74    public String blockingGetPassword(Account account) {
75        ensureNotOnMainThread();
76        try {
77            return mService.getPassword(account);
78        } catch (RemoteException e) {
79            // if this happens the entire runtime will restart
80            throw new RuntimeException(e);
81        }
82    }
83
84    public Future1<String> getPassword(final Future1Callback<String> callback,
85            final Account account, final Handler handler) {
86        return startAsFuture(callback, handler, new Callable<String>() {
87            public String call() throws Exception {
88                return blockingGetPassword(account);
89            }
90        });
91    }
92
93    public String blockingGetUserData(Account account, String key) {
94        ensureNotOnMainThread();
95        try {
96            return mService.getUserData(account, key);
97        } catch (RemoteException e) {
98            // if this happens the entire runtime will restart
99            throw new RuntimeException(e);
100        }
101    }
102
103    public Future1<String> getUserData(Future1Callback<String> callback,
104            final Account account, final String key, Handler handler) {
105        return startAsFuture(callback, handler, new Callable<String>() {
106            public String call() throws Exception {
107                return blockingGetUserData(account, key);
108            }
109        });
110    }
111
112    public String[] blockingGetAuthenticatorTypes() {
113        ensureNotOnMainThread();
114        try {
115            return mService.getAuthenticatorTypes();
116        } catch (RemoteException e) {
117            // if this happens the entire runtime will restart
118            throw new RuntimeException(e);
119        }
120    }
121
122    public Future1<String[]> getAuthenticatorTypes(Future1Callback<String[]> callback,
123            Handler handler) {
124        return startAsFuture(callback, handler, new Callable<String[]>() {
125            public String[] call() throws Exception {
126                return blockingGetAuthenticatorTypes();
127            }
128        });
129    }
130
131    public Account[] blockingGetAccounts() {
132        ensureNotOnMainThread();
133        try {
134            return mService.getAccounts();
135        } catch (RemoteException e) {
136            // if this happens the entire runtime will restart
137            throw new RuntimeException(e);
138        }
139    }
140
141    public Account[] blockingGetAccountsByType(String accountType) {
142        ensureNotOnMainThread();
143        try {
144            return mService.getAccountsByType(accountType);
145        } catch (RemoteException e) {
146            // if this happens the entire runtime will restart
147            throw new RuntimeException(e);
148        }
149    }
150
151    public Future1<Account[]> getAccounts(Future1Callback<Account[]> callback, Handler handler) {
152        return startAsFuture(callback, handler, new Callable<Account[]>() {
153            public Account[] call() throws Exception {
154                return blockingGetAccounts();
155            }
156        });
157    }
158
159    public Future1<Account[]> getAccountsByType(Future1Callback<Account[]> callback,
160            final String type, Handler handler) {
161        return startAsFuture(callback, handler, new Callable<Account[]>() {
162            public Account[] call() throws Exception {
163                return blockingGetAccountsByType(type);
164            }
165        });
166    }
167
168    public boolean blockingAddAccountExplicitly(Account account, String password, Bundle extras) {
169        ensureNotOnMainThread();
170        try {
171            return mService.addAccount(account, password, extras);
172        } catch (RemoteException e) {
173            // if this happens the entire runtime will restart
174            throw new RuntimeException(e);
175        }
176    }
177
178    public Future1<Boolean> addAccountExplicitly(final Future1Callback<Boolean> callback,
179            final Account account, final String password, final Bundle extras,
180            final Handler handler) {
181        return startAsFuture(callback, handler, new Callable<Boolean>() {
182            public Boolean call() throws Exception {
183                return blockingAddAccountExplicitly(account, password, extras);
184            }
185        });
186    }
187
188    public void blockingRemoveAccount(Account account) {
189        ensureNotOnMainThread();
190        try {
191            mService.removeAccount(account);
192        } catch (RemoteException e) {
193            // if this happens the entire runtime will restart
194        }
195    }
196
197    public Future1<Void> removeAccount(Future1Callback<Void> callback, final Account account,
198            final Handler handler) {
199        return startAsFuture(callback, handler, new Callable<Void>() {
200            public Void call() throws Exception {
201                blockingRemoveAccount(account);
202                return null;
203            }
204        });
205    }
206
207    public void blockingInvalidateAuthToken(String accountType, String authToken) {
208        ensureNotOnMainThread();
209        try {
210            mService.invalidateAuthToken(accountType, authToken);
211        } catch (RemoteException e) {
212            // if this happens the entire runtime will restart
213        }
214    }
215
216    public Future1<Void> invalidateAuthToken(Future1Callback<Void> callback,
217            final String accountType, final String authToken, final Handler handler) {
218        return startAsFuture(callback, handler, new Callable<Void>() {
219            public Void call() throws Exception {
220                blockingInvalidateAuthToken(accountType, authToken);
221                return null;
222            }
223        });
224    }
225
226    public String blockingPeekAuthToken(Account account, String authTokenType) {
227        ensureNotOnMainThread();
228        try {
229            return mService.peekAuthToken(account, authTokenType);
230        } catch (RemoteException e) {
231            // if this happens the entire runtime will restart
232            throw new RuntimeException(e);
233        }
234    }
235
236    public Future1<String> peekAuthToken(Future1Callback<String> callback,
237            final Account account, final String authTokenType, final Handler handler) {
238        return startAsFuture(callback, handler, new Callable<String>() {
239            public String call() throws Exception {
240                return blockingPeekAuthToken(account, authTokenType);
241            }
242        });
243    }
244
245    public void blockingSetPassword(Account account, String password) {
246        ensureNotOnMainThread();
247        try {
248            mService.setPassword(account, password);
249        } catch (RemoteException e) {
250            // if this happens the entire runtime will restart
251        }
252    }
253
254    public Future1<Void> setPassword(Future1Callback<Void> callback,
255            final Account account, final String password, final Handler handler) {
256        return startAsFuture(callback, handler, new Callable<Void>() {
257            public Void call() throws Exception {
258                blockingSetPassword(account, password);
259                return null;
260            }
261        });
262    }
263
264    public void blockingClearPassword(Account account) {
265        ensureNotOnMainThread();
266        try {
267            mService.clearPassword(account);
268        } catch (RemoteException e) {
269            // if this happens the entire runtime will restart
270        }
271    }
272
273    public Future1<Void> clearPassword(final Future1Callback<Void> callback, final Account account,
274            final Handler handler) {
275        return startAsFuture(callback, handler, new Callable<Void>() {
276            public Void call() throws Exception {
277                blockingClearPassword(account);
278                return null;
279            }
280        });
281    }
282
283    public void blockingSetUserData(Account account, String key, String value) {
284        ensureNotOnMainThread();
285        try {
286            mService.setUserData(account, key, value);
287        } catch (RemoteException e) {
288            // if this happens the entire runtime will restart
289        }
290    }
291
292    public Future1<Void> setUserData(Future1Callback<Void> callback,
293            final Account account, final String key, final String value, final Handler handler) {
294        return startAsFuture(callback, handler, new Callable<Void>() {
295            public Void call() throws Exception {
296                blockingSetUserData(account, key, value);
297                return null;
298            }
299        });
300    }
301
302    public void blockingSetAuthToken(Account account, String authTokenType, String authToken) {
303        ensureNotOnMainThread();
304        try {
305            mService.setAuthToken(account, authTokenType, authToken);
306        } catch (RemoteException e) {
307            // if this happens the entire runtime will restart
308        }
309    }
310
311    public Future1<Void> setAuthToken(Future1Callback<Void> callback,
312            final Account account, final String authTokenType, final String authToken,
313            final Handler handler) {
314        return startAsFuture(callback, handler, new Callable<Void>() {
315            public Void call() throws Exception {
316                blockingSetAuthToken(account, authTokenType, authToken);
317                return null;
318            }
319        });
320    }
321
322    public String blockingGetAuthToken(Account account, String authTokenType,
323            boolean notifyAuthFailure)
324            throws OperationCanceledException, IOException, AuthenticatorException {
325        ensureNotOnMainThread();
326        Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */,
327                null /* handler */).getResult();
328        return bundle.getString(Constants.AUTHTOKEN_KEY);
329    }
330
331    /**
332     * Request the auth token for this account/authTokenType. If this succeeds then the
333     * auth token will then be passed to the activity. If this results in an authentication
334     * failure then a login intent will be returned that can be invoked to prompt the user to
335     * update their credentials. This login activity will return the auth token to the calling
336     * activity. If activity is null then the login intent will not be invoked.
337     *
338     * @param account the account whose auth token should be retrieved
339     * @param authTokenType the auth token type that should be retrieved
340     * @param loginOptions
341     * @param activity the activity to launch the login intent, if necessary, and to which
342     */
343    public Future2 getAuthToken(
344            final Account account, final String authTokenType, final Bundle loginOptions,
345            final Activity activity, Future2Callback callback, Handler handler) {
346        if (activity == null) throw new IllegalArgumentException("activity is null");
347        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
348        return new AmsTask(activity, handler, callback) {
349            public void doWork() throws RemoteException {
350                mService.getAuthToken(mResponse, account, authTokenType,
351                        false /* notifyOnAuthFailure */, true /* expectActivityLaunch */,
352                        loginOptions);
353            }
354        }.start();
355    }
356
357    public Future2 getAuthToken(
358            final Account account, final String authTokenType, final boolean notifyAuthFailure,
359            Future2Callback callback, Handler handler) {
360        if (account == null) throw new IllegalArgumentException("account is null");
361        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
362        return new AmsTask(null, handler, callback) {
363            public void doWork() throws RemoteException {
364                mService.getAuthToken(mResponse, account, authTokenType,
365                        notifyAuthFailure, false /* expectActivityLaunch */, null /* options */);
366            }
367        }.start();
368    }
369
370    public Future2 addAccount(final String accountType,
371            final String authTokenType, final String[] requiredFeatures,
372            final Bundle addAccountOptions,
373            final Activity activity, Future2Callback callback, Handler handler) {
374        return new AmsTask(activity, handler, callback) {
375            public void doWork() throws RemoteException {
376                mService.addAcount(mResponse, accountType, authTokenType,
377                        requiredFeatures, activity != null, addAccountOptions);
378            }
379        }.start();
380    }
381
382    /** @deprecated use {@link #confirmCredentials} instead */
383    public Future1<Boolean> confirmPassword(final Account account, final String password,
384            Future1Callback<Boolean> callback, Handler handler) {
385        return new AMSTaskBoolean(handler, callback) {
386            public void doWork() throws RemoteException {
387                mService.confirmPassword(response, account, password);
388            }
389        };
390    }
391
392    public Account[] blockingGetAccountsWithTypeAndFeatures(String type, String[] features)
393            throws AuthenticatorException, IOException, OperationCanceledException {
394        Future2 future = getAccountsWithTypeAndFeatures(type, features,
395                null /* callback */, null /* handler */);
396        Bundle result = future.getResult();
397        Parcelable[] accountsTemp = result.getParcelableArray(Constants.ACCOUNTS_KEY);
398        if (accountsTemp == null) {
399            throw new AuthenticatorException("accounts should not be null");
400        }
401        Account[] accounts = new Account[accountsTemp.length];
402        for (int i = 0; i < accountsTemp.length; i++) {
403            accounts[i] = (Account) accountsTemp[i];
404        }
405        return accounts;
406    }
407
408    public Future2 getAccountsWithTypeAndFeatures(
409            final String type, final String[] features,
410            Future2Callback callback, Handler handler) {
411        if (type == null) throw new IllegalArgumentException("type is null");
412        return new AmsTask(null /* activity */, handler, callback) {
413            public void doWork() throws RemoteException {
414                mService.getAccountsByTypeAndFeatures(mResponse, type, features);
415            }
416        }.start();
417    }
418
419    public Future2 confirmCredentials(final Account account, final Activity activity,
420            final Future2Callback callback,
421            final Handler handler) {
422        return new AmsTask(activity, handler, callback) {
423            public void doWork() throws RemoteException {
424                mService.confirmCredentials(mResponse, account, activity != null);
425            }
426        }.start();
427    }
428
429    public Future2 updateCredentials(final Account account, final String authTokenType,
430            final Bundle loginOptions, final Activity activity,
431            final Future2Callback callback,
432            final Handler handler) {
433        return new AmsTask(activity, handler, callback) {
434            public void doWork() throws RemoteException {
435                mService.updateCredentials(mResponse, account, authTokenType, activity != null,
436                        loginOptions);
437            }
438        }.start();
439    }
440
441    public Future2 editProperties(final String accountType, final Activity activity,
442            final Future2Callback callback,
443            final Handler handler) {
444        return new AmsTask(activity, handler, callback) {
445            public void doWork() throws RemoteException {
446                mService.editProperties(mResponse, accountType, activity != null);
447            }
448        }.start();
449    }
450
451    private void ensureNotOnMainThread() {
452        final Looper looper = Looper.myLooper();
453        if (looper != null && looper == mContext.getMainLooper()) {
454            // We really want to throw an exception here, but GTalkService exercises this
455            // path quite a bit and needs some serious rewrite in order to work properly.
456            //noinspection ThrowableInstanceNeverThrow
457//            Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs",
458//                    new Exception());
459            // TODO(fredq) remove the log and throw this exception when the callers are fixed
460//            throw new IllegalStateException(
461//                    "calling this from your main thread can lead to deadlock");
462        }
463    }
464
465    private void postToHandler(Handler handler, final Future2Callback callback,
466            final Future2 future) {
467        handler = handler == null ? mMainHandler : handler;
468        handler.post(new Runnable() {
469            public void run() {
470                callback.run(future);
471            }
472        });
473    }
474
475    private void postToHandler(Handler handler, final OnAccountsUpdatedListener listener,
476            final Account[] accounts) {
477        handler = handler == null ? mMainHandler : handler;
478        handler.post(new Runnable() {
479            public void run() {
480                listener.onAccountsUpdated(accounts);
481            }
482        });
483    }
484
485    private <V> void postToHandler(Handler handler, final Future1Callback<V> callback,
486            final Future1<V> future) {
487        handler = handler == null ? mMainHandler : handler;
488        handler.post(new Runnable() {
489            public void run() {
490                callback.run(future);
491            }
492        });
493    }
494
495    private <V> Future1<V> startAsFuture(Future1Callback<V> callback, Handler handler,
496            Callable<V> callable) {
497        final FutureTaskWithCallback<V> task =
498                new FutureTaskWithCallback<V>(callback, callable, handler);
499        new Thread(task).start();
500        return task;
501    }
502
503    private class FutureTaskWithCallback<V> extends FutureTask<V> implements Future1<V> {
504        final Future1Callback<V> mCallback;
505        final Handler mHandler;
506
507        public FutureTaskWithCallback(Future1Callback<V> callback, Callable<V> callable,
508                Handler handler) {
509            super(callable);
510            mCallback = callback;
511            mHandler = handler;
512        }
513
514        protected void done() {
515            if (mCallback != null) {
516                postToHandler(mHandler, mCallback, this);
517            }
518        }
519
520        public V internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException {
521            try {
522                if (timeout == null) {
523                    return get();
524                } else {
525                    return get(timeout, unit);
526                }
527            } catch (InterruptedException e) {
528                // we will cancel the task below
529            } catch (CancellationException e) {
530                // we will cancel the task below
531            } catch (TimeoutException e) {
532                // we will cancel the task below
533            } catch (ExecutionException e) {
534                // this should never happen
535                throw new IllegalStateException(e.getCause());
536            } finally {
537                cancel(true /* interruptIfRunning */);
538            }
539            throw new OperationCanceledException();
540        }
541
542        public V getResult() throws OperationCanceledException {
543            return internalGetResult(null, null);
544        }
545
546        public V getResult(long timeout, TimeUnit unit) throws OperationCanceledException {
547            return internalGetResult(null, null);
548        }
549    }
550
551    private abstract class AmsTask extends FutureTask<Bundle> implements Future2 {
552        final IAccountManagerResponse mResponse;
553        final Handler mHandler;
554        final Future2Callback mCallback;
555        final Activity mActivity;
556        final Thread mThread;
557        public AmsTask(Activity activity, Handler handler, Future2Callback callback) {
558            super(new Callable<Bundle>() {
559                public Bundle call() throws Exception {
560                    throw new IllegalStateException("this should never be called");
561                }
562            });
563
564            mHandler = handler;
565            mCallback = callback;
566            mActivity = activity;
567            mResponse = new Response();
568            mThread = new Thread(new Runnable() {
569                public void run() {
570                    try {
571                        doWork();
572                    } catch (RemoteException e) {
573                        // never happens
574                    }
575                }
576            }, "AmsTask");
577        }
578
579        public final Future2 start() {
580            mThread.start();
581            return this;
582        }
583
584        public abstract void doWork() throws RemoteException;
585
586        private Bundle internalGetResult(Long timeout, TimeUnit unit)
587                throws OperationCanceledException, IOException, AuthenticatorException {
588            try {
589                if (timeout == null) {
590                    return get();
591                } else {
592                    return get(timeout, unit);
593                }
594            } catch (CancellationException e) {
595                throw new OperationCanceledException();
596            } catch (TimeoutException e) {
597                // fall through and cancel
598            } catch (InterruptedException e) {
599                // fall through and cancel
600            } catch (ExecutionException e) {
601                final Throwable cause = e.getCause();
602                if (cause instanceof IOException) {
603                    throw (IOException) cause;
604                } else if (cause instanceof UnsupportedOperationException) {
605                    throw new AuthenticatorException(cause);
606                } else if (cause instanceof AuthenticatorException) {
607                    throw (AuthenticatorException) cause;
608                } else if (cause instanceof RuntimeException) {
609                    throw (RuntimeException) cause;
610                } else if (cause instanceof Error) {
611                    throw (Error) cause;
612                } else {
613                    throw new IllegalStateException(cause);
614                }
615            } finally {
616                cancel(true /* interrupt if running */);
617            }
618            throw new OperationCanceledException();
619        }
620
621        public Bundle getResult()
622                throws OperationCanceledException, IOException, AuthenticatorException {
623            return internalGetResult(null, null);
624        }
625
626        public Bundle getResult(long timeout, TimeUnit unit)
627                throws OperationCanceledException, IOException, AuthenticatorException {
628            return internalGetResult(timeout, unit);
629        }
630
631        protected void done() {
632            if (mCallback != null) {
633                postToHandler(mHandler, mCallback, this);
634            }
635        }
636
637        /** Handles the responses from the AccountManager */
638        private class Response extends IAccountManagerResponse.Stub {
639            public void onResult(Bundle bundle) {
640                Intent intent = bundle.getParcelable("intent");
641                if (intent != null && mActivity != null) {
642                    // since the user provided an Activity we will silently start intents
643                    // that we see
644                    mActivity.startActivity(intent);
645                    // leave the Future running to wait for the real response to this request
646                } else {
647                    set(bundle);
648                }
649            }
650
651            public void onError(int code, String message) {
652                if (code == Constants.ERROR_CODE_CANCELED) {
653                    // the authenticator indicated that this request was canceled, do so now
654                    cancel(true /* mayInterruptIfRunning */);
655                    return;
656                }
657                setException(convertErrorToException(code, message));
658            }
659        }
660
661    }
662
663    private abstract class AMSTaskBoolean extends FutureTask<Boolean> implements Future1<Boolean> {
664        final IAccountManagerResponse response;
665        final Handler mHandler;
666        final Future1Callback<Boolean> mCallback;
667        public AMSTaskBoolean(Handler handler, Future1Callback<Boolean> callback) {
668            super(new Callable<Boolean>() {
669                public Boolean call() throws Exception {
670                    throw new IllegalStateException("this should never be called");
671                }
672            });
673
674            mHandler = handler;
675            mCallback = callback;
676            response = new Response();
677
678            new Thread(new Runnable() {
679                public void run() {
680                    try {
681                        doWork();
682                    } catch (RemoteException e) {
683                        // never happens
684                    }
685                }
686            }).start();
687        }
688
689        public abstract void doWork() throws RemoteException;
690
691
692        protected void done() {
693            if (mCallback != null) {
694                postToHandler(mHandler, mCallback, this);
695            }
696        }
697
698        private Boolean internalGetResult(Long timeout, TimeUnit unit) {
699            try {
700                if (timeout == null) {
701                    return get();
702                } else {
703                    return get(timeout, unit);
704                }
705            } catch (InterruptedException e) {
706                // fall through and cancel
707            } catch (TimeoutException e) {
708                // fall through and cancel
709            } catch (CancellationException e) {
710                return false;
711            } catch (ExecutionException e) {
712                final Throwable cause = e.getCause();
713                if (cause instanceof IOException) {
714                    return false;
715                } else if (cause instanceof UnsupportedOperationException) {
716                    return false;
717                } else if (cause instanceof AuthenticatorException) {
718                    return false;
719                } else if (cause instanceof RuntimeException) {
720                    throw (RuntimeException) cause;
721                } else if (cause instanceof Error) {
722                    throw (Error) cause;
723                } else {
724                    throw new IllegalStateException(cause);
725                }
726            } finally {
727                cancel(true /* interrupt if running */);
728            }
729            return false;
730        }
731
732        public Boolean getResult() throws OperationCanceledException {
733            return internalGetResult(null, null);
734        }
735
736        public Boolean getResult(long timeout, TimeUnit unit) throws OperationCanceledException {
737            return internalGetResult(timeout, unit);
738        }
739
740        private class Response extends IAccountManagerResponse.Stub {
741            public void onResult(Bundle bundle) {
742                try {
743                    if (bundle.containsKey(Constants.BOOLEAN_RESULT_KEY)) {
744                        set(bundle.getBoolean(Constants.BOOLEAN_RESULT_KEY));
745                        return;
746                    }
747                } catch (ClassCastException e) {
748                    // we will set the exception below
749                }
750                onError(Constants.ERROR_CODE_INVALID_RESPONSE, "no result in response");
751            }
752
753            public void onError(int code, String message) {
754                if (code == Constants.ERROR_CODE_CANCELED) {
755                    cancel(true /* mayInterruptIfRunning */);
756                    return;
757                }
758                setException(convertErrorToException(code, message));
759            }
760        }
761
762    }
763
764    private Exception convertErrorToException(int code, String message) {
765        if (code == Constants.ERROR_CODE_NETWORK_ERROR) {
766            return new IOException(message);
767        }
768
769        if (code == Constants.ERROR_CODE_UNSUPPORTED_OPERATION) {
770            return new UnsupportedOperationException(message);
771        }
772
773        if (code == Constants.ERROR_CODE_INVALID_RESPONSE) {
774            return new AuthenticatorException(message);
775        }
776
777        if (code == Constants.ERROR_CODE_BAD_ARGUMENTS) {
778            return new IllegalArgumentException(message);
779        }
780
781        return new AuthenticatorException(message);
782    }
783
784    private class GetAuthTokenByTypeAndFeaturesTask extends AmsTask implements Future2Callback {
785        GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
786                final String[] features, Activity activityForPrompting,
787                final Bundle addAccountOptions, final Bundle loginOptions,
788                Future2Callback callback, Handler handler) {
789            super(activityForPrompting, handler, callback);
790            if (accountType == null) throw new IllegalArgumentException("account type is null");
791            mAccountType = accountType;
792            mAuthTokenType = authTokenType;
793            mFeatures = features;
794            mAddAccountOptions = addAccountOptions;
795            mLoginOptions = loginOptions;
796            mMyCallback = this;
797        }
798        volatile Future2 mFuture = null;
799        final String mAccountType;
800        final String mAuthTokenType;
801        final String[] mFeatures;
802        final Bundle mAddAccountOptions;
803        final Bundle mLoginOptions;
804        final Future2Callback mMyCallback;
805
806        public void doWork() throws RemoteException {
807            getAccountsWithTypeAndFeatures(mAccountType, mFeatures, new Future2Callback() {
808                public void run(Future2 future) {
809                    Bundle getAccountsResult;
810                    try {
811                        getAccountsResult = future.getResult();
812                    } catch (OperationCanceledException e) {
813                        setException(e);
814                        return;
815                    } catch (IOException e) {
816                        setException(e);
817                        return;
818                    } catch (AuthenticatorException e) {
819                        setException(e);
820                        return;
821                    }
822
823                    Parcelable[] accounts =
824                            getAccountsResult.getParcelableArray(Constants.ACCOUNTS_KEY);
825                    if (accounts.length == 0) {
826                        if (mActivity != null) {
827                            // no accounts, add one now. pretend that the user directly
828                            // made this request
829                            mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures,
830                                    mAddAccountOptions, mActivity, mMyCallback, mHandler);
831                        } else {
832                            // send result since we can't prompt to add an account
833                            Bundle result = new Bundle();
834                            result.putString(Constants.ACCOUNT_NAME_KEY, null);
835                            result.putString(Constants.ACCOUNT_TYPE_KEY, null);
836                            result.putString(Constants.AUTHTOKEN_KEY, null);
837                            try {
838                                mResponse.onResult(result);
839                            } catch (RemoteException e) {
840                                // this will never happen
841                            }
842                            // we are done
843                        }
844                    } else if (accounts.length == 1) {
845                        // have a single account, return an authtoken for it
846                        if (mActivity == null) {
847                            mFuture = getAuthToken((Account) accounts[0], mAuthTokenType,
848                                    false /* notifyAuthFailure */, mMyCallback, mHandler);
849                        } else {
850                            mFuture = getAuthToken((Account) accounts[0],
851                                    mAuthTokenType, mLoginOptions,
852                                    mActivity, mMyCallback, mHandler);
853                        }
854                    } else {
855                        if (mActivity != null) {
856                            IAccountManagerResponse chooseResponse =
857                                    new IAccountManagerResponse.Stub() {
858                                public void onResult(Bundle value) throws RemoteException {
859                                    Account account = new Account(
860                                            value.getString(Constants.ACCOUNT_NAME_KEY),
861                                            value.getString(Constants.ACCOUNT_TYPE_KEY));
862                                    mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
863                                            mActivity, mMyCallback, mHandler);
864                                }
865
866                                public void onError(int errorCode, String errorMessage)
867                                        throws RemoteException {
868                                    mResponse.onError(errorCode, errorMessage);
869                                }
870                            };
871                            // have many accounts, launch the chooser
872                            Intent intent = new Intent();
873                            intent.setClassName("android",
874                                    "android.accounts.ChooseAccountActivity");
875                            intent.putExtra(Constants.ACCOUNTS_KEY, accounts);
876                            intent.putExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY,
877                                    new AccountManagerResponse(chooseResponse));
878                            mActivity.startActivity(intent);
879                            // the result will arrive via the IAccountManagerResponse
880                        } else {
881                            // send result since we can't prompt to select an account
882                            Bundle result = new Bundle();
883                            result.putString(Constants.ACCOUNTS_KEY, null);
884                            try {
885                                mResponse.onResult(result);
886                            } catch (RemoteException e) {
887                                // this will never happen
888                            }
889                            // we are done
890                        }
891                    }
892                }}, mHandler);
893        }
894
895
896
897        // TODO(fredq) pass through the calls to our implemention of Future2 to the underlying
898        // future that we create. We need to do things like have cancel cancel the mFuture, if set
899        // or to cause this to be canceled if mFuture isn't set.
900        // Once this is done then getAuthTokenByFeatures can be changed to return a Future2.
901
902        public void run(Future2 future) {
903            try {
904                set(future.get());
905            } catch (InterruptedException e) {
906                cancel(true);
907            } catch (CancellationException e) {
908                cancel(true);
909            } catch (ExecutionException e) {
910                setException(e.getCause());
911            }
912        }
913    }
914
915    public void getAuthTokenByFeatures(
916            final String accountType, final String authTokenType, final String[] features,
917            final Activity activityForPrompting, final Bundle addAccountOptions,
918            final Bundle loginOptions,
919            final Future2Callback callback, final Handler handler) {
920        if (accountType == null) throw new IllegalArgumentException("account type is null");
921        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
922        new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType,  features,
923                activityForPrompting, addAccountOptions, loginOptions, callback, handler).start();
924    }
925
926    private final HashMap<OnAccountsUpdatedListener, Handler> mAccountsUpdatedListeners =
927            Maps.newHashMap();
928
929    // These variable are only used from the LOGIN_ACCOUNTS_CHANGED_ACTION BroadcastReceiver
930    // and its getAccounts() callback which are both invoked only on the main thread. As a
931    // result we don't need to protect against concurrent accesses and any changes are guaranteed
932    // to be visible when used. Basically, these two variables are thread-confined.
933    private Future1<Account[]> mAccountsLookupFuture = null;
934    private boolean mAccountLookupPending = false;
935
936    /**
937     * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent
938     * so that it can read the updated list of accounts and send them to the listener
939     * in mAccountsUpdatedListeners.
940     */
941    private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() {
942        public void onReceive(final Context context, final Intent intent) {
943            if (mAccountsLookupFuture != null) {
944                // an accounts lookup is already in progress,
945                // don't bother starting another request
946                mAccountLookupPending = true;
947                return;
948            }
949            // initiate a read of the accounts
950            mAccountsLookupFuture = getAccounts(new Future1Callback<Account[]>() {
951                public void run(Future1<Account[]> future) {
952                    // clear the future so that future receives will try the lookup again
953                    mAccountsLookupFuture = null;
954
955                    // get the accounts array
956                    Account[] accounts;
957                    try {
958                        accounts = future.getResult();
959                    } catch (OperationCanceledException e) {
960                        // this should never happen, but if it does pretend we got another
961                        // accounts changed broadcast
962                        if (Config.LOGD) {
963                            Log.d(TAG, "the accounts lookup for listener notifications was "
964                                    + "canceled, try again by simulating the receipt of "
965                                    + "a LOGIN_ACCOUNTS_CHANGED_ACTION broadcast");
966                        }
967                        onReceive(context, intent);
968                        return;
969                    }
970
971                    // send the result to the listeners
972                    synchronized (mAccountsUpdatedListeners) {
973                        for (Map.Entry<OnAccountsUpdatedListener, Handler> entry :
974                                mAccountsUpdatedListeners.entrySet()) {
975                            Account[] accountsCopy = new Account[accounts.length];
976                            // send the listeners a copy to make sure that one doesn't
977                            // change what another sees
978                            System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
979                            postToHandler(entry.getValue(), entry.getKey(), accountsCopy);
980                        }
981                    }
982
983                    // If mAccountLookupPending was set when the account lookup finished it
984                    // means that we had previously ignored a LOGIN_ACCOUNTS_CHANGED_ACTION
985                    // intent because a lookup was already in progress. Now that we are done
986                    // with this lookup and notification pretend that another intent
987                    // was received by calling onReceive() directly.
988                    if (mAccountLookupPending) {
989                        mAccountLookupPending = false;
990                        onReceive(context, intent);
991                        return;
992                    }
993                }
994            }, mMainHandler);
995        }
996    };
997
998    /**
999     * Add a {@link OnAccountsUpdatedListener} to this instance of the {@link AccountManager}.
1000     * The listener is guaranteed to be invoked on the thread of the Handler that is passed
1001     * in or the main thread's Handler if handler is null.
1002     * @param listener the listener to add
1003     * @param handler the Handler whose thread will be used to invoke the listener. If null
1004     * the AccountManager context's main thread will be used.
1005     * @param updateImmediately if true then the listener will be invoked as a result of this
1006     * call.
1007     * @throws IllegalArgumentException if listener is null
1008     * @throws IllegalStateException if listener was already added
1009     */
1010    public void addOnAccountsUpdatedListener(final OnAccountsUpdatedListener listener,
1011            Handler handler, boolean updateImmediately) {
1012        if (listener == null) {
1013            throw new IllegalArgumentException("the listener is null");
1014        }
1015        synchronized (mAccountsUpdatedListeners) {
1016            if (mAccountsUpdatedListeners.containsKey(listener)) {
1017                throw new IllegalStateException("this listener is already added");
1018            }
1019            final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty();
1020
1021            mAccountsUpdatedListeners.put(listener, handler);
1022
1023            if (wasEmpty) {
1024                // Register a broadcast receiver to monitor account changes
1025                IntentFilter intentFilter = new IntentFilter();
1026                intentFilter.addAction(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION);
1027                mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
1028            }
1029        }
1030
1031        if (updateImmediately) {
1032            getAccounts(new Future1Callback<Account[]>() {
1033                public void run(Future1<Account[]> future) {
1034                    try {
1035                        listener.onAccountsUpdated(future.getResult());
1036                    } catch (OperationCanceledException e) {
1037                        // ignore
1038                    }
1039                }
1040            }, handler);
1041        }
1042    }
1043
1044    /**
1045     * Remove an {@link OnAccountsUpdatedListener} that was previously registered with
1046     * {@link #addOnAccountsUpdatedListener}.
1047     * @param listener the listener to remove
1048     * @throws IllegalArgumentException if listener is null
1049     * @throws IllegalStateException if listener was not already added
1050     */
1051    public void removeOnAccountsUpdatedListener(OnAccountsUpdatedListener listener) {
1052        if (listener == null) {
1053            throw new IllegalArgumentException("the listener is null");
1054        }
1055        synchronized (mAccountsUpdatedListeners) {
1056            if (mAccountsUpdatedListeners.remove(listener) == null) {
1057                throw new IllegalStateException("this listener was not previously added");
1058            }
1059            if (mAccountsUpdatedListeners.isEmpty()) {
1060                mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
1061            }
1062        }
1063    }
1064}
1065