/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.accounts;

import android.app.Activity;
import android.content.Intent;
import android.content.Context;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.Parcelable;
import android.util.Config;
import android.util.Log;

import java.io.IOException;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.Map;

import com.google.android.collect.Maps;

/**
 * A class that helps with interactions with the AccountManagerService. It provides
 * methods to allow for account, password, and authtoken management for all accounts on the
 * device. Some of these calls are implemented with the help of the corresponding
 * {@link IAccountAuthenticator} services. One accesses the {@link AccountManager} by calling:
 *    AccountManager accountManager = AccountManager.get(context);
 *
 * <p>
 * TODO(fredq) this interface is still in flux
 */
public class AccountManager {
    private static final String TAG = "AccountManager";

    private final Context mContext;
    private final IAccountManager mService;
    private final Handler mMainHandler;

    /**
     * @hide
     */
    public AccountManager(Context context, IAccountManager service) {
        mContext = context;
        mService = service;
        mMainHandler = new Handler(mContext.getMainLooper());
    }

    /**
     * @hide used for testing only
     */
    public AccountManager(Context context, IAccountManager service, Handler handler) {
        mContext = context;
        mService = service;
        mMainHandler = handler;
    }

    public static AccountManager get(Context context) {
        return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
    }

    public String blockingGetPassword(Account account) {
        ensureNotOnMainThread();
        try {
            return mService.getPassword(account);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Future1<String> getPassword(final Future1Callback<String> callback,
            final Account account, final Handler handler) {
        return startAsFuture(callback, handler, new Callable<String>() {
            public String call() throws Exception {
                return blockingGetPassword(account);
            }
        });
    }

    public String blockingGetUserData(Account account, String key) {
        ensureNotOnMainThread();
        try {
            return mService.getUserData(account, key);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Future1<String> getUserData(Future1Callback<String> callback,
            final Account account, final String key, Handler handler) {
        return startAsFuture(callback, handler, new Callable<String>() {
            public String call() throws Exception {
                return blockingGetUserData(account, key);
            }
        });
    }

    public String[] blockingGetAuthenticatorTypes() {
        ensureNotOnMainThread();
        try {
            return mService.getAuthenticatorTypes();
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Future1<String[]> getAuthenticatorTypes(Future1Callback<String[]> callback,
            Handler handler) {
        return startAsFuture(callback, handler, new Callable<String[]>() {
            public String[] call() throws Exception {
                return blockingGetAuthenticatorTypes();
            }
        });
    }

    public Account[] blockingGetAccounts() {
        ensureNotOnMainThread();
        try {
            return mService.getAccounts();
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Account[] blockingGetAccountsByType(String accountType) {
        ensureNotOnMainThread();
        try {
            return mService.getAccountsByType(accountType);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Future1<Account[]> getAccounts(Future1Callback<Account[]> callback, Handler handler) {
        return startAsFuture(callback, handler, new Callable<Account[]>() {
            public Account[] call() throws Exception {
                return blockingGetAccounts();
            }
        });
    }

    public Future1<Account[]> getAccountsByType(Future1Callback<Account[]> callback,
            final String type, Handler handler) {
        return startAsFuture(callback, handler, new Callable<Account[]>() {
            public Account[] call() throws Exception {
                return blockingGetAccountsByType(type);
            }
        });
    }

    public boolean blockingAddAccountExplicitly(Account account, String password, Bundle extras) {
        ensureNotOnMainThread();
        try {
            return mService.addAccount(account, password, extras);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Future1<Boolean> addAccountExplicitly(final Future1Callback<Boolean> callback,
            final Account account, final String password, final Bundle extras,
            final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Boolean>() {
            public Boolean call() throws Exception {
                return blockingAddAccountExplicitly(account, password, extras);
            }
        });
    }

    public void blockingRemoveAccount(Account account) {
        ensureNotOnMainThread();
        try {
            mService.removeAccount(account);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
        }
    }

    public Future1<Void> removeAccount(Future1Callback<Void> callback, final Account account,
            final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Void>() {
            public Void call() throws Exception {
                blockingRemoveAccount(account);
                return null;
            }
        });
    }

    public void blockingInvalidateAuthToken(String accountType, String authToken) {
        ensureNotOnMainThread();
        try {
            mService.invalidateAuthToken(accountType, authToken);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
        }
    }

    public Future1<Void> invalidateAuthToken(Future1Callback<Void> callback,
            final String accountType, final String authToken, final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Void>() {
            public Void call() throws Exception {
                blockingInvalidateAuthToken(accountType, authToken);
                return null;
            }
        });
    }

    public String blockingPeekAuthToken(Account account, String authTokenType) {
        ensureNotOnMainThread();
        try {
            return mService.peekAuthToken(account, authTokenType);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
            throw new RuntimeException(e);
        }
    }

    public Future1<String> peekAuthToken(Future1Callback<String> callback,
            final Account account, final String authTokenType, final Handler handler) {
        return startAsFuture(callback, handler, new Callable<String>() {
            public String call() throws Exception {
                return blockingPeekAuthToken(account, authTokenType);
            }
        });
    }

    public void blockingSetPassword(Account account, String password) {
        ensureNotOnMainThread();
        try {
            mService.setPassword(account, password);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
        }
    }

    public Future1<Void> setPassword(Future1Callback<Void> callback,
            final Account account, final String password, final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Void>() {
            public Void call() throws Exception {
                blockingSetPassword(account, password);
                return null;
            }
        });
    }

    public void blockingClearPassword(Account account) {
        ensureNotOnMainThread();
        try {
            mService.clearPassword(account);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
        }
    }

    public Future1<Void> clearPassword(final Future1Callback<Void> callback, final Account account,
            final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Void>() {
            public Void call() throws Exception {
                blockingClearPassword(account);
                return null;
            }
        });
    }

    public void blockingSetUserData(Account account, String key, String value) {
        ensureNotOnMainThread();
        try {
            mService.setUserData(account, key, value);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
        }
    }

    public Future1<Void> setUserData(Future1Callback<Void> callback,
            final Account account, final String key, final String value, final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Void>() {
            public Void call() throws Exception {
                blockingSetUserData(account, key, value);
                return null;
            }
        });
    }

    public void blockingSetAuthToken(Account account, String authTokenType, String authToken) {
        ensureNotOnMainThread();
        try {
            mService.setAuthToken(account, authTokenType, authToken);
        } catch (RemoteException e) {
            // if this happens the entire runtime will restart
        }
    }

    public Future1<Void> setAuthToken(Future1Callback<Void> callback,
            final Account account, final String authTokenType, final String authToken,
            final Handler handler) {
        return startAsFuture(callback, handler, new Callable<Void>() {
            public Void call() throws Exception {
                blockingSetAuthToken(account, authTokenType, authToken);
                return null;
            }
        });
    }

    public String blockingGetAuthToken(Account account, String authTokenType,
            boolean notifyAuthFailure)
            throws OperationCanceledException, IOException, AuthenticatorException {
        ensureNotOnMainThread();
        Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */,
                null /* handler */).getResult();
        return bundle.getString(Constants.AUTHTOKEN_KEY);
    }

    /**
     * Request the auth token for this account/authTokenType. If this succeeds then the
     * auth token will then be passed to the activity. If this results in an authentication
     * failure then a login intent will be returned that can be invoked to prompt the user to
     * update their credentials. This login activity will return the auth token to the calling
     * activity. If activity is null then the login intent will not be invoked.
     *
     * @param account the account whose auth token should be retrieved
     * @param authTokenType the auth token type that should be retrieved
     * @param loginOptions
     * @param activity the activity to launch the login intent, if necessary, and to which
     */
    public Future2 getAuthToken(
            final Account account, final String authTokenType, final Bundle loginOptions,
            final Activity activity, Future2Callback callback, Handler handler) {
        if (activity == null) throw new IllegalArgumentException("activity is null");
        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
        return new AmsTask(activity, handler, callback) {
            public void doWork() throws RemoteException {
                mService.getAuthToken(mResponse, account, authTokenType,
                        false /* notifyOnAuthFailure */, true /* expectActivityLaunch */,
                        loginOptions);
            }
        }.start();
    }

    public Future2 getAuthToken(
            final Account account, final String authTokenType, final boolean notifyAuthFailure,
            Future2Callback callback, Handler handler) {
        if (account == null) throw new IllegalArgumentException("account is null");
        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
        return new AmsTask(null, handler, callback) {
            public void doWork() throws RemoteException {
                mService.getAuthToken(mResponse, account, authTokenType,
                        notifyAuthFailure, false /* expectActivityLaunch */, null /* options */);
            }
        }.start();
    }

    public Future2 addAccount(final String accountType,
            final String authTokenType, final String[] requiredFeatures,
            final Bundle addAccountOptions,
            final Activity activity, Future2Callback callback, Handler handler) {
        return new AmsTask(activity, handler, callback) {
            public void doWork() throws RemoteException {
                mService.addAcount(mResponse, accountType, authTokenType,
                        requiredFeatures, activity != null, addAccountOptions);
            }
        }.start();
    }

    /** @deprecated use {@link #confirmCredentials} instead */
    public Future1<Boolean> confirmPassword(final Account account, final String password,
            Future1Callback<Boolean> callback, Handler handler) {
        return new AMSTaskBoolean(handler, callback) {
            public void doWork() throws RemoteException {
                mService.confirmPassword(response, account, password);
            }
        };
    }

    public Account[] blockingGetAccountsWithTypeAndFeatures(String type, String[] features)
            throws AuthenticatorException, IOException, OperationCanceledException {
        Future2 future = getAccountsWithTypeAndFeatures(type, features,
                null /* callback */, null /* handler */);
        Bundle result = future.getResult();
        Parcelable[] accountsTemp = result.getParcelableArray(Constants.ACCOUNTS_KEY);
        if (accountsTemp == null) {
            throw new AuthenticatorException("accounts should not be null");
        }
        Account[] accounts = new Account[accountsTemp.length];
        for (int i = 0; i < accountsTemp.length; i++) {
            accounts[i] = (Account) accountsTemp[i];
        }
        return accounts;
    }

    public Future2 getAccountsWithTypeAndFeatures(
            final String type, final String[] features,
            Future2Callback callback, Handler handler) {
        if (type == null) throw new IllegalArgumentException("type is null");
        return new AmsTask(null /* activity */, handler, callback) {
            public void doWork() throws RemoteException {
                mService.getAccountsByTypeAndFeatures(mResponse, type, features);
            }
        }.start();
    }

    public Future2 confirmCredentials(final Account account, final Activity activity,
            final Future2Callback callback,
            final Handler handler) {
        return new AmsTask(activity, handler, callback) {
            public void doWork() throws RemoteException {
                mService.confirmCredentials(mResponse, account, activity != null);
            }
        }.start();
    }

    public Future2 updateCredentials(final Account account, final String authTokenType,
            final Bundle loginOptions, final Activity activity,
            final Future2Callback callback,
            final Handler handler) {
        return new AmsTask(activity, handler, callback) {
            public void doWork() throws RemoteException {
                mService.updateCredentials(mResponse, account, authTokenType, activity != null,
                        loginOptions);
            }
        }.start();
    }

    public Future2 editProperties(final String accountType, final Activity activity,
            final Future2Callback callback,
            final Handler handler) {
        return new AmsTask(activity, handler, callback) {
            public void doWork() throws RemoteException {
                mService.editProperties(mResponse, accountType, activity != null);
            }
        }.start();
    }

    private void ensureNotOnMainThread() {
        final Looper looper = Looper.myLooper();
        if (looper != null && looper == mContext.getMainLooper()) {
            // We really want to throw an exception here, but GTalkService exercises this
            // path quite a bit and needs some serious rewrite in order to work properly.
            //noinspection ThrowableInstanceNeverThrow
//            Log.e(TAG, "calling this from your main thread can lead to deadlock and/or ANRs",
//                    new Exception());
            // TODO(fredq) remove the log and throw this exception when the callers are fixed
//            throw new IllegalStateException(
//                    "calling this from your main thread can lead to deadlock");
        }
    }

    private void postToHandler(Handler handler, final Future2Callback callback,
            final Future2 future) {
        handler = handler == null ? mMainHandler : handler;
        handler.post(new Runnable() {
            public void run() {
                callback.run(future);
            }
        });
    }

    private void postToHandler(Handler handler, final OnAccountsUpdatedListener listener,
            final Account[] accounts) {
        handler = handler == null ? mMainHandler : handler;
        handler.post(new Runnable() {
            public void run() {
                listener.onAccountsUpdated(accounts);
            }
        });
    }

    private <V> void postToHandler(Handler handler, final Future1Callback<V> callback,
            final Future1<V> future) {
        handler = handler == null ? mMainHandler : handler;
        handler.post(new Runnable() {
            public void run() {
                callback.run(future);
            }
        });
    }

    private <V> Future1<V> startAsFuture(Future1Callback<V> callback, Handler handler,
            Callable<V> callable) {
        final FutureTaskWithCallback<V> task =
                new FutureTaskWithCallback<V>(callback, callable, handler);
        new Thread(task).start();
        return task;
    }

    private class FutureTaskWithCallback<V> extends FutureTask<V> implements Future1<V> {
        final Future1Callback<V> mCallback;
        final Handler mHandler;

        public FutureTaskWithCallback(Future1Callback<V> callback, Callable<V> callable,
                Handler handler) {
            super(callable);
            mCallback = callback;
            mHandler = handler;
        }

        protected void done() {
            if (mCallback != null) {
                postToHandler(mHandler, mCallback, this);
            }
        }

        public V internalGetResult(Long timeout, TimeUnit unit) throws OperationCanceledException {
            try {
                if (timeout == null) {
                    return get();
                } else {
                    return get(timeout, unit);
                }
            } catch (InterruptedException e) {
                // we will cancel the task below
            } catch (CancellationException e) {
                // we will cancel the task below
            } catch (TimeoutException e) {
                // we will cancel the task below
            } catch (ExecutionException e) {
                // this should never happen
                throw new IllegalStateException(e.getCause());
            } finally {
                cancel(true /* interruptIfRunning */);
            }
            throw new OperationCanceledException();
        }

        public V getResult() throws OperationCanceledException {
            return internalGetResult(null, null);
        }

        public V getResult(long timeout, TimeUnit unit) throws OperationCanceledException {
            return internalGetResult(null, null);
        }
    }

    private abstract class AmsTask extends FutureTask<Bundle> implements Future2 {
        final IAccountManagerResponse mResponse;
        final Handler mHandler;
        final Future2Callback mCallback;
        final Activity mActivity;
        final Thread mThread;
        public AmsTask(Activity activity, Handler handler, Future2Callback callback) {
            super(new Callable<Bundle>() {
                public Bundle call() throws Exception {
                    throw new IllegalStateException("this should never be called");
                }
            });

            mHandler = handler;
            mCallback = callback;
            mActivity = activity;
            mResponse = new Response();
            mThread = new Thread(new Runnable() {
                public void run() {
                    try {
                        doWork();
                    } catch (RemoteException e) {
                        // never happens
                    }
                }
            }, "AmsTask");
        }

        public final Future2 start() {
            mThread.start();
            return this;
        }

        public abstract void doWork() throws RemoteException;

        private Bundle internalGetResult(Long timeout, TimeUnit unit)
                throws OperationCanceledException, IOException, AuthenticatorException {
            try {
                if (timeout == null) {
                    return get();
                } else {
                    return get(timeout, unit);
                }
            } catch (CancellationException e) {
                throw new OperationCanceledException();
            } catch (TimeoutException e) {
                // fall through and cancel
            } catch (InterruptedException e) {
                // fall through and cancel
            } catch (ExecutionException e) {
                final Throwable cause = e.getCause();
                if (cause instanceof IOException) {
                    throw (IOException) cause;
                } else if (cause instanceof UnsupportedOperationException) {
                    throw new AuthenticatorException(cause);
                } else if (cause instanceof AuthenticatorException) {
                    throw (AuthenticatorException) cause;
                } else if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                } else {
                    throw new IllegalStateException(cause);
                }
            } finally {
                cancel(true /* interrupt if running */);
            }
            throw new OperationCanceledException();
        }

        public Bundle getResult()
                throws OperationCanceledException, IOException, AuthenticatorException {
            return internalGetResult(null, null);
        }

        public Bundle getResult(long timeout, TimeUnit unit)
                throws OperationCanceledException, IOException, AuthenticatorException {
            return internalGetResult(timeout, unit);
        }

        protected void done() {
            if (mCallback != null) {
                postToHandler(mHandler, mCallback, this);
            }
        }

        /** Handles the responses from the AccountManager */
        private class Response extends IAccountManagerResponse.Stub {
            public void onResult(Bundle bundle) {
                Intent intent = bundle.getParcelable("intent");
                if (intent != null && mActivity != null) {
                    // since the user provided an Activity we will silently start intents
                    // that we see
                    mActivity.startActivity(intent);
                    // leave the Future running to wait for the real response to this request
                } else {
                    set(bundle);
                }
            }

            public void onError(int code, String message) {
                if (code == Constants.ERROR_CODE_CANCELED) {
                    // the authenticator indicated that this request was canceled, do so now
                    cancel(true /* mayInterruptIfRunning */);
                    return;
                }
                setException(convertErrorToException(code, message));
            }
        }

    }

    private abstract class AMSTaskBoolean extends FutureTask<Boolean> implements Future1<Boolean> {
        final IAccountManagerResponse response;
        final Handler mHandler;
        final Future1Callback<Boolean> mCallback;
        public AMSTaskBoolean(Handler handler, Future1Callback<Boolean> callback) {
            super(new Callable<Boolean>() {
                public Boolean call() throws Exception {
                    throw new IllegalStateException("this should never be called");
                }
            });

            mHandler = handler;
            mCallback = callback;
            response = new Response();

            new Thread(new Runnable() {
                public void run() {
                    try {
                        doWork();
                    } catch (RemoteException e) {
                        // never happens
                    }
                }
            }).start();
        }

        public abstract void doWork() throws RemoteException;


        protected void done() {
            if (mCallback != null) {
                postToHandler(mHandler, mCallback, this);
            }
        }

        private Boolean internalGetResult(Long timeout, TimeUnit unit) {
            try {
                if (timeout == null) {
                    return get();
                } else {
                    return get(timeout, unit);
                }
            } catch (InterruptedException e) {
                // fall through and cancel
            } catch (TimeoutException e) {
                // fall through and cancel
            } catch (CancellationException e) {
                return false;
            } catch (ExecutionException e) {
                final Throwable cause = e.getCause();
                if (cause instanceof IOException) {
                    return false;
                } else if (cause instanceof UnsupportedOperationException) {
                    return false;
                } else if (cause instanceof AuthenticatorException) {
                    return false;
                } else if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                } else {
                    throw new IllegalStateException(cause);
                }
            } finally {
                cancel(true /* interrupt if running */);
            }
            return false;
        }

        public Boolean getResult() throws OperationCanceledException {
            return internalGetResult(null, null);
        }

        public Boolean getResult(long timeout, TimeUnit unit) throws OperationCanceledException {
            return internalGetResult(timeout, unit);
        }

        private class Response extends IAccountManagerResponse.Stub {
            public void onResult(Bundle bundle) {
                try {
                    if (bundle.containsKey(Constants.BOOLEAN_RESULT_KEY)) {
                        set(bundle.getBoolean(Constants.BOOLEAN_RESULT_KEY));
                        return;
                    }
                } catch (ClassCastException e) {
                    // we will set the exception below
                }
                onError(Constants.ERROR_CODE_INVALID_RESPONSE, "no result in response");
            }

            public void onError(int code, String message) {
                if (code == Constants.ERROR_CODE_CANCELED) {
                    cancel(true /* mayInterruptIfRunning */);
                    return;
                }
                setException(convertErrorToException(code, message));
            }
        }

    }

    private Exception convertErrorToException(int code, String message) {
        if (code == Constants.ERROR_CODE_NETWORK_ERROR) {
            return new IOException(message);
        }

        if (code == Constants.ERROR_CODE_UNSUPPORTED_OPERATION) {
            return new UnsupportedOperationException(message);
        }

        if (code == Constants.ERROR_CODE_INVALID_RESPONSE) {
            return new AuthenticatorException(message);
        }

        if (code == Constants.ERROR_CODE_BAD_ARGUMENTS) {
            return new IllegalArgumentException(message);
        }

        return new AuthenticatorException(message);
    }

    private class GetAuthTokenByTypeAndFeaturesTask extends AmsTask implements Future2Callback {
        GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
                final String[] features, Activity activityForPrompting,
                final Bundle addAccountOptions, final Bundle loginOptions,
                Future2Callback callback, Handler handler) {
            super(activityForPrompting, handler, callback);
            if (accountType == null) throw new IllegalArgumentException("account type is null");
            mAccountType = accountType;
            mAuthTokenType = authTokenType;
            mFeatures = features;
            mAddAccountOptions = addAccountOptions;
            mLoginOptions = loginOptions;
            mMyCallback = this;
        }
        volatile Future2 mFuture = null;
        final String mAccountType;
        final String mAuthTokenType;
        final String[] mFeatures;
        final Bundle mAddAccountOptions;
        final Bundle mLoginOptions;
        final Future2Callback mMyCallback;

        public void doWork() throws RemoteException {
            getAccountsWithTypeAndFeatures(mAccountType, mFeatures, new Future2Callback() {
                public void run(Future2 future) {
                    Bundle getAccountsResult;
                    try {
                        getAccountsResult = future.getResult();
                    } catch (OperationCanceledException e) {
                        setException(e);
                        return;
                    } catch (IOException e) {
                        setException(e);
                        return;
                    } catch (AuthenticatorException e) {
                        setException(e);
                        return;
                    }

                    Parcelable[] accounts =
                            getAccountsResult.getParcelableArray(Constants.ACCOUNTS_KEY);
                    if (accounts.length == 0) {
                        if (mActivity != null) {
                            // no accounts, add one now. pretend that the user directly
                            // made this request
                            mFuture = addAccount(mAccountType, mAuthTokenType, mFeatures,
                                    mAddAccountOptions, mActivity, mMyCallback, mHandler);
                        } else {
                            // send result since we can't prompt to add an account
                            Bundle result = new Bundle();
                            result.putString(Constants.ACCOUNT_NAME_KEY, null);
                            result.putString(Constants.ACCOUNT_TYPE_KEY, null);
                            result.putString(Constants.AUTHTOKEN_KEY, null);
                            try {
                                mResponse.onResult(result);
                            } catch (RemoteException e) {
                                // this will never happen
                            }
                            // we are done
                        }
                    } else if (accounts.length == 1) {
                        // have a single account, return an authtoken for it
                        if (mActivity == null) {
                            mFuture = getAuthToken((Account) accounts[0], mAuthTokenType,
                                    false /* notifyAuthFailure */, mMyCallback, mHandler);
                        } else {
                            mFuture = getAuthToken((Account) accounts[0],
                                    mAuthTokenType, mLoginOptions,
                                    mActivity, mMyCallback, mHandler);
                        }
                    } else {
                        if (mActivity != null) {
                            IAccountManagerResponse chooseResponse =
                                    new IAccountManagerResponse.Stub() {
                                public void onResult(Bundle value) throws RemoteException {
                                    Account account = new Account(
                                            value.getString(Constants.ACCOUNT_NAME_KEY),
                                            value.getString(Constants.ACCOUNT_TYPE_KEY));
                                    mFuture = getAuthToken(account, mAuthTokenType, mLoginOptions,
                                            mActivity, mMyCallback, mHandler);
                                }

                                public void onError(int errorCode, String errorMessage)
                                        throws RemoteException {
                                    mResponse.onError(errorCode, errorMessage);
                                }
                            };
                            // have many accounts, launch the chooser
                            Intent intent = new Intent();
                            intent.setClassName("android",
                                    "android.accounts.ChooseAccountActivity");
                            intent.putExtra(Constants.ACCOUNTS_KEY, accounts);
                            intent.putExtra(Constants.ACCOUNT_MANAGER_RESPONSE_KEY,
                                    new AccountManagerResponse(chooseResponse));
                            mActivity.startActivity(intent);
                            // the result will arrive via the IAccountManagerResponse
                        } else {
                            // send result since we can't prompt to select an account
                            Bundle result = new Bundle();
                            result.putString(Constants.ACCOUNTS_KEY, null);
                            try {
                                mResponse.onResult(result);
                            } catch (RemoteException e) {
                                // this will never happen
                            }
                            // we are done
                        }
                    }
                }}, mHandler);
        }



        // TODO(fredq) pass through the calls to our implemention of Future2 to the underlying
        // future that we create. We need to do things like have cancel cancel the mFuture, if set
        // or to cause this to be canceled if mFuture isn't set.
        // Once this is done then getAuthTokenByFeatures can be changed to return a Future2.

        public void run(Future2 future) {
            try {
                set(future.get());
            } catch (InterruptedException e) {
                cancel(true);
            } catch (CancellationException e) {
                cancel(true);
            } catch (ExecutionException e) {
                setException(e.getCause());
            }
        }
    }

    public void getAuthTokenByFeatures(
            final String accountType, final String authTokenType, final String[] features,
            final Activity activityForPrompting, final Bundle addAccountOptions,
            final Bundle loginOptions,
            final Future2Callback callback, final Handler handler) {
        if (accountType == null) throw new IllegalArgumentException("account type is null");
        if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
        new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType,  features,
                activityForPrompting, addAccountOptions, loginOptions, callback, handler).start();
    }

    private final HashMap<OnAccountsUpdatedListener, Handler> mAccountsUpdatedListeners =
            Maps.newHashMap();

    // These variable are only used from the LOGIN_ACCOUNTS_CHANGED_ACTION BroadcastReceiver
    // and its getAccounts() callback which are both invoked only on the main thread. As a
    // result we don't need to protect against concurrent accesses and any changes are guaranteed
    // to be visible when used. Basically, these two variables are thread-confined.
    private Future1<Account[]> mAccountsLookupFuture = null;
    private boolean mAccountLookupPending = false;

    /**
     * BroadcastReceiver that listens for the LOGIN_ACCOUNTS_CHANGED_ACTION intent
     * so that it can read the updated list of accounts and send them to the listener
     * in mAccountsUpdatedListeners.
     */
    private final BroadcastReceiver mAccountsChangedBroadcastReceiver = new BroadcastReceiver() {
        public void onReceive(final Context context, final Intent intent) {
            if (mAccountsLookupFuture != null) {
                // an accounts lookup is already in progress,
                // don't bother starting another request
                mAccountLookupPending = true;
                return;
            }
            // initiate a read of the accounts
            mAccountsLookupFuture = getAccounts(new Future1Callback<Account[]>() {
                public void run(Future1<Account[]> future) {
                    // clear the future so that future receives will try the lookup again
                    mAccountsLookupFuture = null;

                    // get the accounts array
                    Account[] accounts;
                    try {
                        accounts = future.getResult();
                    } catch (OperationCanceledException e) {
                        // this should never happen, but if it does pretend we got another
                        // accounts changed broadcast
                        if (Config.LOGD) {
                            Log.d(TAG, "the accounts lookup for listener notifications was "
                                    + "canceled, try again by simulating the receipt of "
                                    + "a LOGIN_ACCOUNTS_CHANGED_ACTION broadcast");
                        }
                        onReceive(context, intent);
                        return;
                    }

                    // send the result to the listeners
                    synchronized (mAccountsUpdatedListeners) {
                        for (Map.Entry<OnAccountsUpdatedListener, Handler> entry :
                                mAccountsUpdatedListeners.entrySet()) {
                            Account[] accountsCopy = new Account[accounts.length];
                            // send the listeners a copy to make sure that one doesn't
                            // change what another sees
                            System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
                            postToHandler(entry.getValue(), entry.getKey(), accountsCopy);
                        }
                    }

                    // If mAccountLookupPending was set when the account lookup finished it
                    // means that we had previously ignored a LOGIN_ACCOUNTS_CHANGED_ACTION
                    // intent because a lookup was already in progress. Now that we are done
                    // with this lookup and notification pretend that another intent
                    // was received by calling onReceive() directly.
                    if (mAccountLookupPending) {
                        mAccountLookupPending = false;
                        onReceive(context, intent);
                        return;
                    }
                }
            }, mMainHandler);
        }
    };

    /**
     * Add a {@link OnAccountsUpdatedListener} to this instance of the {@link AccountManager}.
     * The listener is guaranteed to be invoked on the thread of the Handler that is passed
     * in or the main thread's Handler if handler is null.
     * @param listener the listener to add
     * @param handler the Handler whose thread will be used to invoke the listener. If null
     * the AccountManager context's main thread will be used.
     * @param updateImmediately if true then the listener will be invoked as a result of this
     * call.
     * @throws IllegalArgumentException if listener is null
     * @throws IllegalStateException if listener was already added
     */
    public void addOnAccountsUpdatedListener(final OnAccountsUpdatedListener listener,
            Handler handler, boolean updateImmediately) {
        if (listener == null) {
            throw new IllegalArgumentException("the listener is null");
        }
        synchronized (mAccountsUpdatedListeners) {
            if (mAccountsUpdatedListeners.containsKey(listener)) {
                throw new IllegalStateException("this listener is already added");
            }
            final boolean wasEmpty = mAccountsUpdatedListeners.isEmpty();

            mAccountsUpdatedListeners.put(listener, handler);

            if (wasEmpty) {
                // Register a broadcast receiver to monitor account changes
                IntentFilter intentFilter = new IntentFilter();
                intentFilter.addAction(Constants.LOGIN_ACCOUNTS_CHANGED_ACTION);
                mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
            }
        }

        if (updateImmediately) {
            getAccounts(new Future1Callback<Account[]>() {
                public void run(Future1<Account[]> future) {
                    try {
                        listener.onAccountsUpdated(future.getResult());
                    } catch (OperationCanceledException e) {
                        // ignore
                    }
                }
            }, handler);
        }
    }

    /**
     * Remove an {@link OnAccountsUpdatedListener} that was previously registered with
     * {@link #addOnAccountsUpdatedListener}.
     * @param listener the listener to remove
     * @throws IllegalArgumentException if listener is null
     * @throws IllegalStateException if listener was not already added
     */
    public void removeOnAccountsUpdatedListener(OnAccountsUpdatedListener listener) {
        if (listener == null) {
            throw new IllegalArgumentException("the listener is null");
        }
        synchronized (mAccountsUpdatedListeners) {
            if (mAccountsUpdatedListeners.remove(listener) == null) {
                throw new IllegalStateException("this listener was not previously added");
            }
            if (mAccountsUpdatedListeners.isEmpty()) {
                mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
            }
        }
    }
}
