diff options
Diffstat (limited to 'core/java/android/accounts/AccountManager.java')
-rw-r--r-- | core/java/android/accounts/AccountManager.java | 1073 |
1 files changed, 1073 insertions, 0 deletions
diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java new file mode 100644 index 0000000..4fcaa88 --- /dev/null +++ b/core/java/android/accounts/AccountManager.java @@ -0,0 +1,1073 @@ +/* + * 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); + } + } + } +} |