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