blob: 153d95f559b44a647a6df2220516070642805ef5 [file] [log] [blame]
/*
* 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.database.SQLException;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.RemoteException;
import android.os.Parcelable;
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 AccountManager Service. It provides
* methods to allow for account, password, and authtoken management for all accounts on the
* device. One accesses the {@link AccountManager} by calling:
* <pre>
* AccountManager accountManager = AccountManager.get(context);
* </pre>
*
* <p>
* The AccountManager Service provides storage for the accounts known to the system,
* provides methods to manage them, and allows the registration of authenticators to
* which operations such as addAccount and getAuthToken are delegated.
* <p>
* Many of the calls take an {@link AccountManagerCallback} and {@link Handler} as parameters.
* These calls return immediately but run asynchronously. If a callback is provided then
* {@link AccountManagerCallback#run} will be invoked wen the request completes, successfully
* or not. An {@link AccountManagerFuture} is returned by these requests and also passed into the
* callback. The result if retrieved by calling {@link AccountManagerFuture#getResult()} which
* either returns the result or throws an exception as appropriate.
* <p>
* The asynchronous request can be made blocking by not providing a callback and instead
* calling {@link AccountManagerFuture#getResult()} on the future that is returned. This will
* cause the running thread to block until the result is returned. Keep in mind that one
* should not block the main thread in this way. Instead one should either use a callback,
* thus making the call asynchronous, or make the blocking call on a separate thread.
* <p>
* If one wants to ensure that the callback is invoked from a specific handler then they should
* pass the handler to the request. This makes it easier to ensure thread-safety by running
* all of one's logic from a single handler.
*/
public class AccountManager {
private static final String TAG = "AccountManager";
public static final int ERROR_CODE_REMOTE_EXCEPTION = 1;
public static final int ERROR_CODE_NETWORK_ERROR = 3;
public static final int ERROR_CODE_CANCELED = 4;
public static final int ERROR_CODE_INVALID_RESPONSE = 5;
public static final int ERROR_CODE_UNSUPPORTED_OPERATION = 6;
public static final int ERROR_CODE_BAD_ARGUMENTS = 7;
public static final int ERROR_CODE_BAD_REQUEST = 8;
public static final String KEY_ACCOUNTS = "accounts";
public static final String KEY_AUTHENTICATOR_TYPES = "authenticator_types";
public static final String KEY_USERDATA = "userdata";
public static final String KEY_AUTHTOKEN = "authtoken";
public static final String KEY_PASSWORD = "password";
public static final String KEY_ACCOUNT_NAME = "authAccount";
public static final String KEY_ACCOUNT_TYPE = "accountType";
public static final String KEY_ERROR_CODE = "errorCode";
public static final String KEY_ERROR_MESSAGE = "errorMessage";
public static final String KEY_INTENT = "intent";
public static final String KEY_BOOLEAN_RESULT = "booleanResult";
public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "accountAuthenticatorResponse";
public static final String KEY_ACCOUNT_MANAGER_RESPONSE = "accountManagerResponse";
public static final String KEY_AUTH_FAILED_MESSAGE = "authFailedMessage";
public static final String KEY_AUTH_TOKEN_LABEL = "authTokenLabelKey";
public static final String ACTION_AUTHENTICATOR_INTENT =
"android.accounts.AccountAuthenticator";
public static final String AUTHENTICATOR_META_DATA_NAME =
"android.accounts.AccountAuthenticator";
public static final String AUTHENTICATOR_ATTRIBUTES_NAME = "account-authenticator";
private final Context mContext;
private final IAccountManager mService;
private final Handler mMainHandler;
/**
* Action sent as a broadcast Intent by the AccountsService
* when accounts are added to and/or removed from the device's
* database.
*/
public static final String LOGIN_ACCOUNTS_CHANGED_ACTION =
"android.accounts.LOGIN_ACCOUNTS_CHANGED";
/**
* @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;
}
/**
* Retrieve an AccountManager instance that is associated with the context that is passed in.
* Certain calls such as {@link #addOnAccountsUpdatedListener} use this context internally,
* so the caller must take care to use a {@link Context} whose lifetime is associated with
* the listener registration.
* @param context The {@link Context} to use when necessary
* @return an {@link AccountManager} instance that is associated with context
*/
public static AccountManager get(Context context) {
return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
}
/**
* Get the password that is associated with the account. Returns null if the account does
* not exist.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
*/
public String getPassword(final Account account) {
try {
return mService.getPassword(account);
} catch (RemoteException e) {
// will never happen
throw new RuntimeException(e);
}
}
/**
* Get the user data named by "key" that is associated with the account.
* Returns null if the account does not exist or if it does not have a value for key.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
*/
public String getUserData(final Account account, final String key) {
try {
return mService.getUserData(account, key);
} catch (RemoteException e) {
// will never happen
throw new RuntimeException(e);
}
}
/**
* Query the AccountManager Service for an array that contains a
* {@link AuthenticatorDescription} for each registered authenticator.
* @return an array that contains all the authenticators known to the AccountManager service.
* This array will be empty if there are no authenticators and will never return null.
* <p>
* No permission is required to make this call.
*/
public AuthenticatorDescription[] getAuthenticatorTypes() {
try {
return mService.getAuthenticatorTypes();
} catch (RemoteException e) {
// will never happen
throw new RuntimeException(e);
}
}
/**
* Query the AccountManager Service for all accounts.
* @return an array that contains all the accounts known to the AccountManager service.
* This array will be empty if there are no accounts and will never return null.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS}
*/
public Account[] getAccounts() {
try {
return mService.getAccounts(null);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Query the AccountManager for the set of accounts that have a given type. If null
* is passed as the type than all accounts are returned.
* @param type the account type by which to filter, or null to get all accounts
* @return an array that contains the accounts that match the specified type. This array
* will be empty if no accounts match. It will never return null.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#GET_ACCOUNTS}
*/
public Account[] getAccountsByType(String type) {
try {
return mService.getAccounts(type);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Add an account to the AccountManager's set of known accounts.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
* @param account The account to add
* @param password The password to associate with the account. May be null.
* @param extras A bundle of key/value pairs to set as the account's userdata. May be null.
* @return true if the account was sucessfully added, false otherwise, for example,
* if the account already exists or if the account is null
*/
public boolean addAccountExplicitly(Account account, String password, Bundle extras) {
try {
return mService.addAccount(account, password, extras);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Removes the given account. If this account does not exist then this call has no effect.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
*
* @param account The {@link Account} to remove
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Boolean} that is true if the account is successfully removed
* or false if the authenticator refuses to remove the account.
*/
public AccountManagerFuture<Boolean> removeAccount(final Account account,
AccountManagerCallback<Boolean> callback, Handler handler) {
return new Future2Task<Boolean>(handler, callback) {
public void doWork() throws RemoteException {
mService.removeAccount(mResponse, account);
}
public Boolean bundleToResult(Bundle bundle) throws AuthenticatorException {
if (!bundle.containsKey(KEY_BOOLEAN_RESULT)) {
throw new AuthenticatorException("no result in response");
}
return bundle.getBoolean(KEY_BOOLEAN_RESULT);
}
}.start();
}
/**
* Removes the given authtoken. If this authtoken does not exist for the given account type
* then this call has no effect.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
* @param accountType the account type of the authtoken to invalidate
* @param authToken the authtoken to invalidate
*/
public void invalidateAuthToken(final String accountType, final String authToken) {
try {
mService.invalidateAuthToken(accountType, authToken);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Gets the authtoken named by "authTokenType" for the specified account if it is cached
* by the AccountManager. If no authtoken is cached then null is returned rather than
* asking the authenticaticor to generate one. If the account or the
* authtoken do not exist then null is returned.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
* @param account the account whose authtoken is to be retrieved, must not be null
* @param authTokenType the type of authtoken to retrieve
* @return an authtoken for the given account and authTokenType, if one is cached by the
* AccountManager, null otherwise.
*/
public String peekAuthToken(final Account account, final String authTokenType) {
try {
return mService.peekAuthToken(account, authTokenType);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Sets the password for the account. The password may be null. If the account does not exist
* then this call has no affect.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
* @param account the account whose password is to be set. Must not be null.
* @param password the password to set for the account. May be null.
*/
public void setPassword(final Account account, final String password) {
try {
mService.setPassword(account, password);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Sets the password for account to null. If the account does not exist then this call
* has no effect.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
* @param account the account whose password is to be cleared. Must not be null.
*/
public void clearPassword(final Account account) {
try {
mService.clearPassword(account);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Sets account's userdata named "key" to the specified value. If the account does not
* exist then this call has no effect.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
* @param account the account whose userdata is to be set. Must not be null.
* @param key the key of the userdata to set. Must not be null.
* @param value the value to set. May be null.
*/
public void setUserData(final Account account, final String key, final String value) {
try {
mService.setUserData(account, key, value);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Sets the authtoken named by "authTokenType" to the value specified by authToken.
* If the account does not exist then this call has no effect.
* <p>
* Requires that the caller has permission
* {@link android.Manifest.permission#AUTHENTICATE_ACCOUNTS} and is running
* with the same UID as the Authenticator for the account.
* @param account the account whose authtoken is to be set. Must not be null.
* @param authTokenType the type of the authtoken to set. Must not be null.
* @param authToken the authToken to set. May be null.
*/
public void setAuthToken(Account account, final String authTokenType, final String authToken) {
try {
mService.setAuthToken(account, authTokenType, authToken);
} catch (RemoteException e) {
// won't ever happen
throw new RuntimeException(e);
}
}
/**
* Convenience method that makes a blocking call to
* {@link #getAuthToken(Account, String, boolean, AccountManagerCallback, Handler)}
* then extracts and returns the value of {@link #KEY_AUTHTOKEN} from its result.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}.
* @param account the account whose authtoken is to be retrieved, must not be null
* @param authTokenType the type of authtoken to retrieve
* @param notifyAuthFailure if true, cause the AccountManager to put up a "sign-on" notification
* for the account if no authtoken is cached by the AccountManager and the the authenticator
* does not have valid credentials to get an authtoken.
* @return an authtoken for the given account and authTokenType, if one is cached by the
* AccountManager, null otherwise.
* @throws AuthenticatorException if the authenticator is not present, unreachable or returns
* an invalid response.
* @throws OperationCanceledException if the request is canceled for any reason
* @throws java.io.IOException if the authenticator experiences an IOException while attempting
* to communicate with its backend server.
*/
public String blockingGetAuthToken(Account account, String authTokenType,
boolean notifyAuthFailure)
throws OperationCanceledException, IOException, AuthenticatorException {
Bundle bundle = getAuthToken(account, authTokenType, notifyAuthFailure, null /* callback */,
null /* handler */).getResult();
return bundle.getString(KEY_AUTHTOKEN);
}
/**
* Request that an authtoken of the specified type be returned for an account.
* If the Account Manager has a cached authtoken of the requested type then it will
* service the request itself. Otherwise it will pass the request on to the authenticator.
* The authenticator can try to service this request with information it already has stored
* in the AccountManager but may need to launch an activity to prompt the
* user to enter credentials. If it is able to retrieve the authtoken it will be returned
* in the result.
* <p>
* If the authenticator needs to prompt the user for credentials it will return an intent to
* the activity that will do the prompting. If an activity is supplied then that activity
* will be used to launch the intent and the result will come from it. Otherwise a result will
* be returned that contains the intent.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}.
*
* @param account The account whose credentials are to be updated.
* @param authTokenType the auth token to retrieve as part of updating the credentials.
* May be null.
* @param loginOptions authenticator specific options for the request
* @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then
* the intent will be started with this activity. If activity is null then the result will
* be returned as-is.
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains:
* <ul>
* <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN}
* </ul>
* If the user presses "back" then the request will be canceled.
*/
public AccountManagerFuture<Bundle> getAuthToken(
final Account account, final String authTokenType, final Bundle loginOptions,
final Activity activity, AccountManagerCallback<Bundle> 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();
}
/**
* Request that an authtoken of the specified type be returned for an account.
* If the Account Manager has a cached authtoken of the requested type then it will
* service the request itself. Otherwise it will pass the request on to the authenticator.
* The authenticator can try to service this request with information it already has stored
* in the AccountManager but may need to launch an activity to prompt the
* user to enter credentials. If it is able to retrieve the authtoken it will be returned
* in the result.
* <p>
* If the authenticator needs to prompt the user for credentials it will return an intent for
* an activity that will do the prompting. If an intent is returned and notifyAuthFailure
* is true then a notification will be created that launches this intent.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#USE_CREDENTIALS}.
*
* @param account The account whose credentials are to be updated.
* @param authTokenType the auth token to retrieve as part of updating the credentials.
* May be null.
* @param notifyAuthFailure if true and the authenticator returns a {@link #KEY_INTENT} in the
* result then a "sign-on needed" notification will be created that will launch this intent.
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains either:
* <ul>
* <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials
* <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN}
* if the authenticator is able to retrieve the auth token
* </ul>
* If the user presses "back" then the request will be canceled.
*/
public AccountManagerFuture<Bundle> getAuthToken(
final Account account, final String authTokenType, final boolean notifyAuthFailure,
AccountManagerCallback<Bundle> 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();
}
/**
* Request that an account be added with the given accountType. This request
* is processed by the authenticator for the account type. If no authenticator is registered
* in the system then {@link AuthenticatorException} is thrown.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
*
* @param accountType The type of account to add. This must not be null.
* @param authTokenType The account that is added should be able to service this auth token
* type. This may be null.
* @param requiredFeatures The account that is added should support these features.
* This array may be null or empty.
* @param addAccountOptions A bundle of authenticator-specific options that is passed on
* to the authenticator. This may be null.
* @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then
* the intent will be started with this activity. If activity is null then the result will
* be returned as-is.
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains either:
* <ul>
* <li> {@link #KEY_INTENT}, or
* <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE}
* and {@link #KEY_AUTHTOKEN} (if an authTokenType was specified).
* </ul>
*/
public AccountManagerFuture<Bundle> addAccount(final String accountType,
final String authTokenType, final String[] requiredFeatures,
final Bundle addAccountOptions,
final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
return new AmsTask(activity, handler, callback) {
public void doWork() throws RemoteException {
mService.addAcount(mResponse, accountType, authTokenType,
requiredFeatures, activity != null, addAccountOptions);
}
}.start();
}
public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
final String type, final String[] features,
AccountManagerCallback<Account[]> callback, Handler handler) {
if (type == null) throw new IllegalArgumentException("type is null");
return new Future2Task<Account[]>(handler, callback) {
public void doWork() throws RemoteException {
mService.getAccountsByFeatures(mResponse, type, features);
}
public Account[] bundleToResult(Bundle bundle) throws AuthenticatorException {
if (!bundle.containsKey(KEY_ACCOUNTS)) {
throw new AuthenticatorException("no result in response");
}
final Parcelable[] parcelables = bundle.getParcelableArray(KEY_ACCOUNTS);
Account[] descs = new Account[parcelables.length];
for (int i = 0; i < parcelables.length; i++) {
descs[i] = (Account) parcelables[i];
}
return descs;
}
}.start();
}
/**
* Requests that the authenticator checks that the user knows the credentials for the account.
* This is typically done by returning an intent to an activity that prompts the user to
* enter the credentials. This request
* is processed by the authenticator for the account. If no matching authenticator is
* registered in the system then {@link AuthenticatorException} is thrown.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
*
* @param account The account whose credentials are to be checked
* @param options authenticator specific options for the request
* @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then
* the intent will be started with this activity. If activity is null then the result will
* be returned as-is.
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains either:
* <ul>
* <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials
* <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct
* credentials
* </ul>
* If the user presses "back" then the request will be canceled.
*/
public AccountManagerFuture<Bundle> confirmCredentials(final Account account,
final Bundle options,
final Activity activity,
final AccountManagerCallback<Bundle> callback,
final Handler handler) {
return new AmsTask(activity, handler, callback) {
public void doWork() throws RemoteException {
mService.confirmCredentials(mResponse, account, options, activity != null);
}
}.start();
}
/**
* Requests that the authenticator update the the credentials for a user. This is typically
* done by returning an intent to an activity that will prompt the user to update the stored
* credentials for the account. This request
* is processed by the authenticator for the account. If no matching authenticator is
* registered in the system then {@link AuthenticatorException} is thrown.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
*
* @param account The account whose credentials are to be updated.
* @param authTokenType the auth token to retrieve as part of updating the credentials.
* May be null.
* @param loginOptions authenticator specific options for the request
* @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then
* the intent will be started with this activity. If activity is null then the result will
* be returned as-is.
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains either:
* <ul>
* <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials
* <li> {@link #KEY_ACCOUNT_NAME} and {@link #KEY_ACCOUNT_TYPE} if the user enters the correct
* credentials, and optionally a {@link #KEY_AUTHTOKEN} if an authTokenType was provided.
* </ul>
* If the user presses "back" then the request will be canceled.
*/
public AccountManagerFuture<Bundle> updateCredentials(final Account account,
final String authTokenType,
final Bundle loginOptions, final Activity activity,
final AccountManagerCallback<Bundle> callback,
final Handler handler) {
return new AmsTask(activity, handler, callback) {
public void doWork() throws RemoteException {
mService.updateCredentials(mResponse, account, authTokenType, activity != null,
loginOptions);
}
}.start();
}
/**
* Request that the properties for an authenticator be updated. This is typically done by
* returning an intent to an activity that will allow the user to make changes. This request
* is processed by the authenticator for the account. If no matching authenticator is
* registered in the system then {@link AuthenticatorException} is thrown.
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
*
* @param accountType The account type of the authenticator whose properties are to be edited.
* @param activity If the authenticator returns a {@link #KEY_INTENT} in the result then
* the intent will be started with this activity. If activity is null then the result will
* be returned as-is.
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains either:
* <ul>
* <li> {@link #KEY_INTENT}, which is to be used to prompt the user for the credentials
* <li> nothing, returned if the edit completes successfully
* </ul>
* If the user presses "back" then the request will be canceled.
*/
public AccountManagerFuture<Bundle> editProperties(final String accountType,
final Activity activity, final AccountManagerCallback<Bundle> 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 AccountManagerCallback<Bundle> callback,
final AccountManagerFuture<Bundle> future) {
handler = handler == null ? mMainHandler : handler;
handler.post(new Runnable() {
public void run() {
callback.run(future);
}
});
}
private void postToHandler(Handler handler, final OnAccountsUpdateListener listener,
final Account[] accounts) {
final Account[] accountsCopy = new Account[accounts.length];
// send a copy to make sure that one doesn't
// change what another sees
System.arraycopy(accounts, 0, accountsCopy, 0, accountsCopy.length);
handler = (handler == null) ? mMainHandler : handler;
handler.post(new Runnable() {
public void run() {
try {
listener.onAccountsUpdated(accountsCopy);
} catch (SQLException e) {
// Better luck next time. If the problem was disk-full,
// the STORAGE_OK intent will re-trigger the update.
Log.e(TAG, "Can't update accounts", e);
}
}
});
}
private abstract class AmsTask extends FutureTask<Bundle> implements AccountManagerFuture<Bundle> {
final IAccountManagerResponse mResponse;
final Handler mHandler;
final AccountManagerCallback<Bundle> mCallback;
final Activity mActivity;
public AmsTask(Activity activity, Handler handler, AccountManagerCallback<Bundle> 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();
}
public final AccountManagerFuture<Bundle> start() {
try {
doWork();
} catch (RemoteException e) {
setException(e);
}
return this;
}
public abstract void doWork() throws RemoteException;
private Bundle internalGetResult(Long timeout, TimeUnit unit)
throws OperationCanceledException, IOException, AuthenticatorException {
ensureNotOnMainThread();
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 if (bundle.getBoolean("retry")) {
try {
doWork();
} catch (RemoteException e) {
// this will only happen if the system process is dead, which means
// we will be dying ourselves
}
} else {
set(bundle);
}
}
public void onError(int code, String message) {
if (code == 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 BaseFutureTask<T> extends FutureTask<T> {
final public IAccountManagerResponse mResponse;
final Handler mHandler;
public BaseFutureTask(Handler handler) {
super(new Callable<T>() {
public T call() throws Exception {
throw new IllegalStateException("this should never be called");
}
});
mHandler = handler;
mResponse = new Response();
}
public abstract void doWork() throws RemoteException;
public abstract T bundleToResult(Bundle bundle) throws AuthenticatorException;
protected void postRunnableToHandler(Runnable runnable) {
Handler handler = (mHandler == null) ? mMainHandler : mHandler;
handler.post(runnable);
}
protected void startTask() {
try {
doWork();
} catch (RemoteException e) {
setException(e);
}
}
protected class Response extends IAccountManagerResponse.Stub {
public void onResult(Bundle bundle) {
try {
T result = bundleToResult(bundle);
if (result == null) {
return;
}
set(result);
return;
} catch (ClassCastException e) {
// we will set the exception below
} catch (AuthenticatorException e) {
// we will set the exception below
}
onError(ERROR_CODE_INVALID_RESPONSE, "no result in response");
}
public void onError(int code, String message) {
if (code == ERROR_CODE_CANCELED) {
cancel(true /* mayInterruptIfRunning */);
return;
}
setException(convertErrorToException(code, message));
}
}
}
private abstract class Future2Task<T>
extends BaseFutureTask<T> implements AccountManagerFuture<T> {
final AccountManagerCallback<T> mCallback;
public Future2Task(Handler handler, AccountManagerCallback<T> callback) {
super(handler);
mCallback = callback;
}
protected void done() {
if (mCallback != null) {
postRunnableToHandler(new Runnable() {
public void run() {
mCallback.run(Future2Task.this);
}
});
}
}
public Future2Task<T> start() {
startTask();
return this;
}
private T internalGetResult(Long timeout, TimeUnit unit)
throws OperationCanceledException, IOException, AuthenticatorException {
ensureNotOnMainThread();
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) {
// 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 T getResult()
throws OperationCanceledException, IOException, AuthenticatorException {
return internalGetResult(null, null);
}
public T getResult(long timeout, TimeUnit unit)
throws OperationCanceledException, IOException, AuthenticatorException {
return internalGetResult(timeout, unit);
}
}
private Exception convertErrorToException(int code, String message) {
if (code == ERROR_CODE_NETWORK_ERROR) {
return new IOException(message);
}
if (code == ERROR_CODE_UNSUPPORTED_OPERATION) {
return new UnsupportedOperationException(message);
}
if (code == ERROR_CODE_INVALID_RESPONSE) {
return new AuthenticatorException(message);
}
if (code == ERROR_CODE_BAD_ARGUMENTS) {
return new IllegalArgumentException(message);
}
return new AuthenticatorException(message);
}
private class GetAuthTokenByTypeAndFeaturesTask
extends AmsTask implements AccountManagerCallback<Bundle> {
GetAuthTokenByTypeAndFeaturesTask(final String accountType, final String authTokenType,
final String[] features, Activity activityForPrompting,
final Bundle addAccountOptions, final Bundle loginOptions,
AccountManagerCallback<Bundle> 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 AccountManagerFuture<Bundle> mFuture = null;
final String mAccountType;
final String mAuthTokenType;
final String[] mFeatures;
final Bundle mAddAccountOptions;
final Bundle mLoginOptions;
final AccountManagerCallback<Bundle> mMyCallback;
public void doWork() throws RemoteException {
getAccountsByTypeAndFeatures(mAccountType, mFeatures,
new AccountManagerCallback<Account[]>() {
public void run(AccountManagerFuture<Account[]> future) {
Account[] accounts;
try {
accounts = future.getResult();
} catch (OperationCanceledException e) {
setException(e);
return;
} catch (IOException e) {
setException(e);
return;
} catch (AuthenticatorException e) {
setException(e);
return;
}
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(KEY_ACCOUNT_NAME, null);
result.putString(KEY_ACCOUNT_TYPE, null);
result.putString(KEY_AUTHTOKEN, 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(accounts[0], mAuthTokenType,
false /* notifyAuthFailure */, mMyCallback, mHandler);
} else {
mFuture = getAuthToken(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(KEY_ACCOUNT_NAME),
value.getString(KEY_ACCOUNT_TYPE));
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(KEY_ACCOUNTS, accounts);
intent.putExtra(KEY_ACCOUNT_MANAGER_RESPONSE,
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(KEY_ACCOUNTS, null);
try {
mResponse.onResult(result);
} catch (RemoteException e) {
// this will never happen
}
// we are done
}
}
}}, mHandler);
}
public void run(AccountManagerFuture<Bundle> future) {
try {
set(future.getResult());
} catch (OperationCanceledException e) {
cancel(true /* mayInterruptIfRUnning */);
} catch (IOException e) {
setException(e);
} catch (AuthenticatorException e) {
setException(e);
}
}
}
/**
* Convenience method that combines the functionality of {@link #getAccountsByTypeAndFeatures},
* {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)},
* and {@link #addAccount}. It first gets the list of accounts that match accountType and the
* feature set. If there are none then {@link #addAccount} is invoked with the authTokenType
* feature set, and addAccountOptions. If there is exactly one then
* {@link #getAuthToken(Account, String, Bundle, Activity, AccountManagerCallback, Handler)} is
* called with that account. If there are more than one then a chooser activity is launched
* to prompt the user to select one of them and then the authtoken is retrieved for it,
* <p>
* This call returns immediately but runs asynchronously and the result is accessed via the
* {@link AccountManagerFuture} that is returned. This future is also passed as the sole
* parameter to the {@link AccountManagerCallback}. If the caller wished to use this
* method asynchronously then they will generally pass in a callback object that will get
* invoked with the {@link AccountManagerFuture}. If they wish to use it synchronously then
* they will generally pass null for the callback and instead call
* {@link android.accounts.AccountManagerFuture#getResult()} on this method's return value,
* which will then block until the request completes.
* <p>
* Requires that the caller has permission {@link android.Manifest.permission#MANAGE_ACCOUNTS}.
*
* @param accountType the accountType to query; this must be non-null
* @param authTokenType the type of authtoken to retrieve; this must be non-null
* @param features a filter for the accounts. See {@link #getAccountsByTypeAndFeatures}.
* @param activityForPrompting The activity used to start any account management
* activities that are required to fulfill this request. This may be null.
* @param addAccountOptions authenticator-specific options used if an account needs to be added
* @param loginOptions authenticator-specific options passed to getAuthToken
* @param callback A callback to invoke when the request completes. If null then
* no callback is invoked.
* @param handler The {@link Handler} to use to invoke the callback. If null then the
* main thread's {@link Handler} is used.
* @return an {@link AccountManagerFuture} that represents the future result of the call.
* The future result is a {@link Bundle} that contains either:
* <ul>
* <li> {@link #KEY_INTENT}, if no activity is supplied yet an activity needs to launched to
* fulfill the request.
* <li> {@link #KEY_ACCOUNT_NAME}, {@link #KEY_ACCOUNT_TYPE} and {@link #KEY_AUTHTOKEN} if the
* request completes successfully.
* </ul>
* If the user presses "back" then the request will be canceled.
*/
public AccountManagerFuture<Bundle> getAuthTokenByFeatures(
final String accountType, final String authTokenType, final String[] features,
final Activity activityForPrompting, final Bundle addAccountOptions,
final Bundle loginOptions,
final AccountManagerCallback<Bundle> callback, final Handler handler) {
if (accountType == null) throw new IllegalArgumentException("account type is null");
if (authTokenType == null) throw new IllegalArgumentException("authTokenType is null");
final GetAuthTokenByTypeAndFeaturesTask task =
new GetAuthTokenByTypeAndFeaturesTask(accountType, authTokenType, features,
activityForPrompting, addAccountOptions, loginOptions, callback, handler);
task.start();
return task;
}
private final HashMap<OnAccountsUpdateListener, Handler> mAccountsUpdatedListeners =
Maps.newHashMap();
/**
* 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) {
final Account[] accounts = getAccounts();
// send the result to the listeners
synchronized (mAccountsUpdatedListeners) {
for (Map.Entry<OnAccountsUpdateListener, Handler> entry :
mAccountsUpdatedListeners.entrySet()) {
postToHandler(entry.getValue(), entry.getKey(), accounts);
}
}
}
};
/**
* Add a {@link OnAccountsUpdateListener} 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.
* <p>
* You must remove this listener before the context that was used to retrieve this
* {@link AccountManager} instance goes away. This generally means when the Activity
* or Service you are running is stopped.
* @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 OnAccountsUpdateListener 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(LOGIN_ACCOUNTS_CHANGED_ACTION);
// To recover from disk-full.
intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK);
mContext.registerReceiver(mAccountsChangedBroadcastReceiver, intentFilter);
}
}
if (updateImmediately) {
postToHandler(handler, listener, getAccounts());
}
}
/**
* Remove an {@link OnAccountsUpdateListener} 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(OnAccountsUpdateListener listener) {
if (listener == null) {
throw new IllegalArgumentException("the listener is null");
}
synchronized (mAccountsUpdatedListeners) {
if (!mAccountsUpdatedListeners.containsKey(listener)) {
throw new IllegalStateException("this listener was not previously added");
}
mAccountsUpdatedListeners.remove(listener);
if (mAccountsUpdatedListeners.isEmpty()) {
mContext.unregisterReceiver(mAccountsChangedBroadcastReceiver);
}
}
}
}