AccountManager.java revision d4a1d2e14297a3387fdb5761090961e714370492
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 if (bundle.getBoolean("retry")) {
656                    try {
657                        doWork();
658                    } catch (RemoteException e) {
659                        // this will only happen if the system process is dead, which means
660                        // we will be dying ourselves
661                    }
662                } else {
663                    set(bundle);
664                }
665            }
666
667            public void onError(int code, String message) {
668                if (code == Constants.ERROR_CODE_CANCELED) {
669                    // the authenticator indicated that this request was canceled, do so now
670                    cancel(true /* mayInterruptIfRunning */);
671                    return;
672                }
673                setException(convertErrorToException(code, message));
674            }
675        }
676
677    }
678
679    private abstract class AMSTaskBoolean extends FutureTask<Boolean> implements Future1<Boolean> {
680        final IAccountManagerResponse response;
681        final Handler mHandler;
682        final Future1Callback<Boolean> mCallback;
683        public AMSTaskBoolean(Handler handler, Future1Callback<Boolean> callback) {
684            super(new Callable<Boolean>() {
685                public Boolean call() throws Exception {
686                    throw new IllegalStateException("this should never be called");
687                }
688            });
689
690            mHandler = handler;
691            mCallback = callback;
692            response = new Response();
693
694            new Thread(new Runnable() {
695                public void run() {
696                    try {
697                        doWork();
698                    } catch (RemoteException e) {
699                        // never happens
700                    }
701                }
702            }).start();
703        }
704
705        public abstract void doWork() throws RemoteException;
706
707
708        protected void done() {
709            if (mCallback != null) {
710                postToHandler(mHandler, mCallback, this);
711            }
712        }
713
714        private Boolean internalGetResult(Long timeout, TimeUnit unit) {
715            try {
716                if (timeout == null) {
717                    return get();
718                } else {
719                    return get(timeout, unit);
720                }
721            } catch (InterruptedException e) {
722                // fall through and cancel
723            } catch (TimeoutException e) {
724                // fall through and cancel
725            } catch (CancellationException e) {
726                return false;
727            } catch (ExecutionException e) {
728                final Throwable cause = e.getCause();
729                if (cause instanceof IOException) {
730                    return false;
731                } else if (cause instanceof UnsupportedOperationException) {
732                    return false;
733                } else if (cause instanceof AuthenticatorException) {
734                    return false;
735                } else if (cause instanceof RuntimeException) {
736                    throw (RuntimeException) cause;
737                } else if (cause instanceof Error) {
738                    throw (Error) cause;
739                } else {
740                    throw new IllegalStateException(cause);
741                }
742            } finally {
743                cancel(true /* interrupt if running */);
744            }
745            return false;
746        }
747
748        public Boolean getResult() throws OperationCanceledException {
749            return internalGetResult(null, null);
750        }
751
752        public Boolean getResult(long timeout, TimeUnit unit) throws OperationCanceledException {
753            return internalGetResult(timeout, unit);
754        }
755
756        private class Response extends IAccountManagerResponse.Stub {
757            public void onResult(Bundle bundle) {
758                try {
759                    if (bundle.containsKey(Constants.BOOLEAN_RESULT_KEY)) {
760                        set(bundle.getBoolean(Constants.BOOLEAN_RESULT_KEY));
761                        return;
762                    }
763                } catch (ClassCastException e) {
764                    // we will set the exception below
765                }
766                onError(Constants.ERROR_CODE_INVALID_RESPONSE, "no result in response");
767            }
768
769            public void onError(int code, String message) {
770                if (code == Constants.ERROR_CODE_CANCELED) {
771                    cancel(true /* mayInterruptIfRunning */);
772                    return;
773                }
774                setException(convertErrorToException(code, message));
775            }
776        }
777
778    }
779
780    private Exception convertErrorToException(int code, String message) {
781        if (code == Constants.ERROR_CODE_NETWORK_ERROR) {
782            return new IOException(message);
783        }
784
785        if (code == Constants.ERROR_CODE_UNSUPPORTED_OPERATION) {
786            return new UnsupportedOperationException(message);
787        }
788
789        if (code == Constants.ERROR_CODE_INVALID_RESPONSE) {
790            return new AuthenticatorException(message);
791        }
792
793        if (code == Constants.ERROR_CODE_BAD_ARGUMENTS) {
794            return new IllegalArgumentException(message);
795        }
796
797        return new AuthenticatorException(message);
798    }
799
800    private class GetAuthTokenByTypeAndFeaturesTask extends AmsTask implements Future2Callback {
801        GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
802                final String[] features, Activity activityForPrompting,
803                final Bundle addAccountOptions, final Bundle loginOptions,
804                Future2Callback callback, Handler handler) {
805            super(activityForPrompting, handler, callback);
806            if (accountType == null) throw new IllegalArgumentException("account type is null");
807            mAccountType = accountType;
808            mAuthTokenType = authTokenType;
809            mFeatures = features;
810            mAddAccountOptions = addAccountOptions;
811            mLoginOptions = loginOptions;
812            mMyCallback = this;
813        }
814        volatile Future2 mFuture = null;
815        final String mAccountType;
816        final String mAuthTokenType;
817        final String[] mFeatures;
818        final Bundle mAddAccountOptions;
819        final Bundle mLoginOptions;
820        final Future2Callback mMyCallback;
821
822        public void doWork() throws RemoteException {
823            getAccountsWithTypeAndFeatures(mAccountType, mFeatures, new Future2Callback() {
824                public void run(Future2 future) {
825                    Bundle getAccountsResult;
826                    try {
827                        getAccountsResult = future.getResult();
828                    } catch (OperationCanceledException e) {
829                        setException(e);
830                        return;
831                    } catch (IOException e) {
832                        setException(e);
833                        return;
834                    } catch (AuthenticatorException e) {
835                        setException(e);
836                        return;
837                    }
838
839                    Parcelable[] accounts =
840                            getAccountsResult.getParcelableArray(Constants.ACCOUNTS_KEY);
841                    if (accounts.length == 0) {
842                        if (mActivity != null) {
843                            // no accounts, add one now. pretend that the user directly
844                            // made this request
845                            mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures,
846                                    mAddAccountOptions, mActivity, mMyCallback, mHandler);
847                        } else {
848                            // send result since we can't prompt to add an account
849                            Bundle result = new Bundle();
850                            result.putString(Constants.ACCOUNT_NAME_KEY, null);
851                            result.putString(Constants.ACCOUNT_TYPE_KEY, null);
852                            result.putString(Constants.AUTHTOKEN_KEY, null);
853                            try {
854                                mResponse.onResult(result);
855                            } catch (RemoteException e) {
856                                // this will never happen
857                            }
858                            // we are done
859                        }
860                    } else if (accounts.length == 1) {
861                        // have a single account, return an authtoken for it
862                        if (mActivity == null) {
863                            mFuture = getAuthToken((Account) accounts[0], mAuthTokenType,
864                                    false /* notifyAuthFailure */, mMyCallback, mHandler);
865                        } else {
866                            mFuture = getAuthToken((Account) accounts[0],
867                                    mAuthTokenType, mLoginOptions,
868                                    mActivity, mMyCallback, mHandler);
869                        }
870                    } else {
871                        if (mActivity != null) {
872                            IAccountManagerResponse chooseResponse =
873                                    new IAccountManagerResponse.Stub() {
874                                public void onResult(Bundle value) throws RemoteException {
875                                    Account account = new Account(
876                                            value.getString(Constants.ACCOUNT_NAME_KEY),
877                                            value.getString(Constants.ACCOUNT_TYPE_KEY));
878                                    mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
879                                            mActivity, mMyCallback, mHandler);
880                                }
881
882                                public void onError(int errorCode, String errorMessage)
883                                        throws RemoteException {
884                                    mResponse.onError(errorCode, errorMessage);
885                                }
886                            };
887                            // have many accounts, launch the chooser
888                            Intent intent = new Intent();
889                            intent.setClassName("android",
890                                    "android.accounts.ChooseAccountActivity");
891                            intent.putExtra(Constants.ACCOUNTS_KEY, accounts);
892                            intent.putExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY,
893                                    new AccountManagerResponse(chooseResponse));
894                            mActivity.startActivity(intent);
895                            // the result will arrive via the IAccountManagerResponse
896                        } else {
897                            // send result since we can't prompt to select an account
898                            Bundle result = new Bundle();
899                            result.putString(Constants.ACCOUNTS_KEY, null);
900                            try {
901                                mResponse.onResult(result);
902                            } catch (RemoteException e) {
903                                // this will never happen
904                            }
905                            // we are done
906                        }
907                    }
908                }}, mHandler);
909        }
910
911
912
913        // TODO(fredq) pass through the calls to our implemention of Future2 to the underlying
914        // future that we create. We need to do things like have cancel cancel the mFuture, if set
915        // or to cause this to be canceled if mFuture isn't set.
916        // Once this is done then getAuthTokenByFeatures can be changed to return a Future2.
917
918        public void run(Future2 future) {
919            try {
920                set(future.get());
921            } catch (InterruptedException e) {
922                cancel(true);
923            } catch (CancellationException e) {
924                cancel(true);
925            } catch (ExecutionException e) {
926                setException(e.getCause());
927            }
928        }
929    }
930
931    public void getAuthTokenByFeatures(
932            final String accountType, final String authTokenType, final String[] features,
933            final Activity activityForPrompting, final Bundle addAccountOptions,
934            final Bundle loginOptions,
935            final Future2Callback callback, final Handler handler) {
936        if (accountType == null) throw new IllegalArgumentException("account type is null");
937        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
938        new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType,  features,
939                activityForPrompting, addAccountOptions, loginOptions, callback, handler).start();
940    }
941
942    private final HashMap<OnAccountsUpdatedListener, Handler> mAccountsUpdatedListeners =
943            Maps.newHashMap();
944
945    // These variable are only used from the LOGIN_ACCOUNTS_CHANGED_ACTION BroadcastReceiver
946    // and its getAccounts() callback which are both invoked only on the main thread. As a
947    // result we don't need to protect against concurrent accesses and any changes are guaranteed
948    // to be visible when used. Basically, these two variables are thread-confined.
949    private Future1<Account[]> mAccountsLookupFuture = null;
950    private boolean mAccountLookupPending = false;
951
952    /**
953     * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent
954     * so that it can read the updated list of accounts and send them to the listener
955     * in mAccountsUpdatedListeners.
956     */
957    private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() {
958        public void onReceive(final Context context, final Intent intent) {
959            if (mAccountsLookupFuture != null) {
960                // an accounts lookup is already in progress,
961                // don't bother starting another request
962                mAccountLookupPending = true;
963                return;
964            }
965            // initiate a read of the accounts
966            mAccountsLookupFuture = getAccounts(new Future1Callback<Account[]>() {
967                public void run(Future1<Account[]> future) {
968                    // clear the future so that future receives will try the lookup again
969                    mAccountsLookupFuture = null;
970
971                    // get the accounts array
972                    Account[] accounts;
973                    try {
974                        accounts = future.getResult();
975                    } catch (OperationCanceledException e) {
976                        // this should never happen, but if it does pretend we got another
977                        // accounts changed broadcast
978                        if (Config.LOGD) {
979                            Log.d(TAG, "the accounts lookup for listener notifications was "
980                                    + "canceled, try again by simulating the receipt of "
981                                    + "a LOGIN_ACCOUNTS_CHANGED_ACTION broadcast");
982                        }
983                        onReceive(context, intent);
984                        return;
985                    }
986
987                    // send the result to the listeners
988                    synchronized (mAccountsUpdatedListeners) {
989                        for (Map.Entry<OnAccountsUpdatedListener, Handler> entry :
990                                mAccountsUpdatedListeners.entrySet()) {
991                            Account[] accountsCopy = new Account[accounts.length];
992                            // send the listeners a copy to make sure that one doesn't
993                            // change what another sees
994                            System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
995                            postToHandler(entry.getValue(), entry.getKey(), accountsCopy);
996                        }
997                    }
998
999                    // If mAccountLookupPending was set when the account lookup finished it
1000                    // means that we had previously ignored a LOGIN_ACCOUNTS_CHANGED_ACTION
1001                    // intent because a lookup was already in progress. Now that we are done
1002                    // with this lookup and notification pretend that another intent
1003                    // was received by calling onReceive() directly.
1004                    if (mAccountLookupPending) {
1005                        mAccountLookupPending = false;
1006                        onReceive(context, intent);
1007                        return;
1008                    }
1009                }
1010            }, mMainHandler);
1011        }
1012    };
1013
1014    /**
1015     * Add a {@link OnAccountsUpdatedListener} to this instance of the {@link AccountManager}.
1016     * The listener is guaranteed to be invoked on the thread of the Handler that is passed
1017     * in or the main thread's Handler if handler is null.
1018     * @param listener the listener to add
1019     * @param handler the Handler whose thread will be used to invoke the listener. If null
1020     * the AccountManager context's main thread will be used.
1021     * @param updateImmediately if true then the listener will be invoked as a result of this
1022     * call.
1023     * @throws IllegalArgumentException if listener is null
1024     * @throws IllegalStateException if listener was already added
1025     */
1026    public void addOnAccountsUpdatedListener(final OnAccountsUpdatedListener listener,
1027            Handler handler, boolean updateImmediately) {
1028        if (listener == null) {
1029            throw new IllegalArgumentException("the listener is null");
1030        }
1031        synchronized (mAccountsUpdatedListeners) {
1032            if (mAccountsUpdatedListeners.containsKey(listener)) {
1033                throw new IllegalStateException("this listener is already added");
1034            }
1035            final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty();
1036
1037            mAccountsUpdatedListeners.put(listener, handler);
1038
1039            if (wasEmpty) {
1040                // Register a broadcast receiver to monitor account changes
1041                IntentFilter intentFilter = new IntentFilter();
1042                intentFilter.addAction(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION);
1043                mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
1044            }
1045        }
1046
1047        if (updateImmediately) {
1048            getAccounts(new Future1Callback<Account[]>() {
1049                public void run(Future1<Account[]> future) {
1050                    try {
1051                        listener.onAccountsUpdated(future.getResult());
1052                    } catch (OperationCanceledException e) {
1053                        // ignore
1054                    }
1055                }
1056            }, handler);
1057        }
1058    }
1059
1060    /**
1061     * Remove an {@link OnAccountsUpdatedListener} that was previously registered with
1062     * {@link #addOnAccountsUpdatedListener}.
1063     * @param listener the listener to remove
1064     * @throws IllegalArgumentException if listener is null
1065     * @throws IllegalStateException if listener was not already added
1066     */
1067    public void removeOnAccountsUpdatedListener(OnAccountsUpdatedListener listener) {
1068        if (listener == null) {
1069            throw new IllegalArgumentException("the listener is null");
1070        }
1071        synchronized (mAccountsUpdatedListeners) {
1072            if (mAccountsUpdatedListeners.remove(listener) == null) {
1073                throw new IllegalStateException("this listener was not previously added");
1074            }
1075            if (mAccountsUpdatedListeners.isEmpty()) {
1076                mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
1077            }
1078        }
1079    }
1080}
1081