blob: 8e52099cf1fa2a8df43c9c92590fe312ffe98aa0 [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2;
import static android.os.Build.VERSION_CODES.LOLLIPOP;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorDescription;
import android.accounts.AuthenticatorException;
import android.accounts.IAccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.util.Scheduler.IdleState;
@Implements(AccountManager.class)
public class ShadowAccountManager {
private List<Account> accounts = new ArrayList<>();
private Map<Account, Map<String, String>> authTokens = new HashMap<>();
private Map<String, AuthenticatorDescription> authenticators = new LinkedHashMap<>();
private List<OnAccountsUpdateListener> listeners = new ArrayList<>();
private Map<Account, Map<String, String>> userData = new HashMap<>();
private Map<Account, String> passwords = new HashMap<>();
private Map<Account, Set<String>> accountFeatures = new HashMap<>();
private Map<Account, Set<String>> packageVisibileAccounts = new HashMap<>();
private List<Bundle> addAccountOptionsList = new ArrayList<>();
private Handler mainHandler;
private RoboAccountManagerFuture pendingAddFuture;
@Implementation
public void __constructor__(Context context, IAccountManager service) {
mainHandler = new Handler(context.getMainLooper());
}
/**
* @deprecated This method will be removed in Robolectric 3.4 Use {@link AccountManager#get(Context)} instead.
*/
@Deprecated
@Implementation
public static AccountManager get(Context context) {
return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
}
@Implementation
public Account[] getAccounts() {
return accounts.toArray(new Account[accounts.size()]);
}
@Implementation
public Account[] getAccountsByType(String type) {
if (type == null) {
return getAccounts();
}
List<Account> accountsByType = new ArrayList<>();
for (Account a : accounts) {
if (type.equals(a.type)) {
accountsByType.add(a);
}
}
return accountsByType.toArray(new Account[accountsByType.size()]);
}
@Implementation
public synchronized void setAuthToken(Account account, String tokenType, String authToken) {
if(accounts.contains(account)) {
Map<String, String> tokenMap = authTokens.get(account);
if(tokenMap == null) {
tokenMap = new HashMap<>();
authTokens.put(account, tokenMap);
}
tokenMap.put(tokenType, authToken);
}
}
@Implementation
public String peekAuthToken(Account account, String tokenType) {
Map<String, String> tokenMap = authTokens.get(account);
if(tokenMap != null) {
return tokenMap.get(tokenType);
}
return null;
}
@Implementation
public boolean addAccountExplicitly(Account account, String password, Bundle userdata) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
for (Account a: getAccountsByType(account.type)) {
if (a.name.equals(account.name)) {
return false;
}
}
if (!accounts.add(account)) {
return false;
}
setPassword(account, password);
if(userdata != null) {
for (String key : userdata.keySet()) {
setUserData(account, key, userdata.get(key).toString());
}
}
return true;
}
@Implementation
public String blockingGetAuthToken(Account account, String authTokenType,
boolean notifyAuthFailure) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
if (authTokenType == null) {
throw new IllegalArgumentException("authTokenType is null");
}
Map<String, String> tokensForAccount = authTokens.get(account);
if (tokensForAccount == null) {
return null;
}
return tokensForAccount.get(authTokenType);
}
/**
* The remove operation is posted to the given {@code handler}, and will be
* executed according to the {@link IdleState} of the corresponding {@link org.robolectric.util.Scheduler}.
*/
@Implementation
public AccountManagerFuture<Boolean> removeAccount(final Account account,
AccountManagerCallback<Boolean> callback,
Handler handler) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
return start(
new BaseRoboAccountManagerFuture<Boolean>(callback, handler) {
@Override
public Boolean doWork()
throws OperationCanceledException, IOException, AuthenticatorException {
return removeAccountExplicitly(account);
}
});
}
@Implementation(minSdk = LOLLIPOP_MR1)
public boolean removeAccountExplicitly(Account account) {
passwords.remove(account);
userData.remove(account);
return accounts.remove(account);
}
/**
* Removes all accounts that have been added.
*/
public void removeAllAccounts() {
passwords.clear();
userData.clear();
accounts.clear();
}
@Implementation
public AuthenticatorDescription[] getAuthenticatorTypes() {
return authenticators.values().toArray(new AuthenticatorDescription[authenticators.size()]);
}
@Implementation
public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener,
Handler handler, boolean updateImmediately) {
if (listeners.contains(listener)) {
return;
}
listeners.add(listener);
if (updateImmediately) {
listener.onAccountsUpdated(getAccounts());
}
}
@Implementation
public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
listeners.remove(listener);
}
@Implementation
public String getUserData(Account account, String key) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
if (!userData.containsKey(account)) {
return null;
}
Map<String, String> userDataMap = userData.get(account);
if (userDataMap.containsKey(key)) {
return userDataMap.get(key);
}
return null;
}
@Implementation
public void setUserData(Account account, String key, String value) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
if (!userData.containsKey(account)) {
userData.put(account, new HashMap<String, String>());
}
Map<String, String> userDataMap = userData.get(account);
if (value == null) {
userDataMap.remove(key);
} else {
userDataMap.put(key, value);
}
}
@Implementation
public void setPassword (Account account, String password) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
if (password == null) {
passwords.remove(account);
} else {
passwords.put(account, password);
}
}
@Implementation
public String getPassword (Account account) {
if (account == null) {
throw new IllegalArgumentException("account is null");
}
if (passwords.containsKey(account)) {
return passwords.get(account);
} else {
return null;
}
}
@Implementation
public void invalidateAuthToken(final String accountType, final String authToken) {
Account[] accountsByType = getAccountsByType(accountType);
for (Account account : accountsByType) {
Map<String, String> tokenMap = authTokens.get(account);
if (tokenMap != null) {
Iterator<Entry<String, String>> it = tokenMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> map = it.next();
if (map.getValue().equals(authToken)) {
it.remove();
}
}
authTokens.put(account, tokenMap);
}
}
}
private void notifyListeners() {
Account[] accounts = getAccounts();
Iterator<OnAccountsUpdateListener> iter = listeners.iterator();
OnAccountsUpdateListener listener;
while (iter.hasNext()) {
listener = iter.next();
listener.onAccountsUpdated(accounts);
}
}
/**
* @param account User account.
*/
public void addAccount(Account account) {
accounts.add(account);
if (pendingAddFuture != null) {
pendingAddFuture.resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
start(pendingAddFuture);
pendingAddFuture = null;
}
notifyListeners();
}
/**
* Adds an account to the AccountManager but when {@link AccountManager#getAccountsByTypeForPackage(String, String)}
* is called will be included if is in one of the #visibileToPackages
*
* @param account User account.
*/
public void addAccount(Account account, String... visibileToPackages) {
addAccount(account);
HashSet<String> value = new HashSet<>();
Collections.addAll(value, visibileToPackages);
packageVisibileAccounts.put(account, value);
}
/**
* Consumes and returns the next {@code addAccountOptions} passed to {@link #addAccount}.
*
* @return the next {@code addAccountOptions}
*/
public Bundle getNextAddAccountOptions() {
if (addAccountOptionsList.isEmpty()) {
return null;
} else {
return addAccountOptionsList.remove(0);
}
}
/**
* Returns the next {@code addAccountOptions} passed to {@link #addAccount} without consuming it.
*
* @return the next {@code addAccountOptions}
*/
public Bundle peekNextAddAccountOptions() {
if (addAccountOptionsList.isEmpty()) {
return null;
} else {
return addAccountOptionsList.get(0);
}
}
private class RoboAccountManagerFuture extends BaseRoboAccountManagerFuture<Bundle> {
private final String accountType;
private final Activity activity;
private final Bundle resultBundle;
RoboAccountManagerFuture(AccountManagerCallback<Bundle> callback, Handler handler, String accountType, Activity activity) {
super(callback, handler);
this.accountType = accountType;
this.activity = activity;
this.resultBundle = new Bundle();
}
@Override
public Bundle doWork() throws OperationCanceledException, IOException, AuthenticatorException {
if (!authenticators.containsKey(accountType)) {
throw new AuthenticatorException("No authenticator specified for " + accountType);
}
resultBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
if (activity == null) {
Intent resultIntent = new Intent();
resultBundle.putParcelable(AccountManager.KEY_INTENT, resultIntent);
} else if (callback == null) {
resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, "some_user@gmail.com");
}
return resultBundle;
}
}
@Implementation
public AccountManagerFuture<Bundle> addAccount(final String accountType, String authTokenType, String[] requiredFeatures, Bundle addAccountOptions, Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
addAccountOptionsList.add(addAccountOptions);
pendingAddFuture = new RoboAccountManagerFuture(callback, handler, accountType, activity);
return pendingAddFuture;
}
public void setFeatures(Account account, String[] accountFeatures) {
HashSet<String> featureSet = new HashSet<>();
featureSet.addAll(Arrays.asList(accountFeatures));
this.accountFeatures.put(account, featureSet);
}
/**
* @param authenticator System authenticator.
*/
public void addAuthenticator(AuthenticatorDescription authenticator) {
authenticators.put(authenticator.type, authenticator);
}
public void addAuthenticator(String type) {
addAuthenticator(AuthenticatorDescription.newKey(type));
}
private Map<Account, String> previousNames = new HashMap<Account, String>();
/**
* Sets the previous name for an account, which will be returned by {@link AccountManager#getPreviousName(Account)}.
*
* @param account User account.
* @param previousName Previous account name.
*/
public void setPreviousAccountName(Account account, String previousName) {
previousNames.put(account, previousName);
}
/**
* @see #setPreviousAccountName(Account, String)
*/
@Implementation(minSdk = LOLLIPOP)
public String getPreviousName(Account account) {
return previousNames.get(account);
}
@Implementation
public AccountManagerFuture<Bundle> getAuthToken(
final Account account, final String authTokenType, final Bundle options,
final Activity activity, final AccountManagerCallback<Bundle> callback, Handler handler) {
return start(
new BaseRoboAccountManagerFuture<Bundle>(callback, handler) {
@Override
public Bundle doWork()
throws OperationCanceledException, IOException, AuthenticatorException {
Bundle result = new Bundle();
String authToken = blockingGetAuthToken(account, authTokenType, false);
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
return result;
}
});
}
@Implementation
public AccountManagerFuture<Bundle> getAuthToken(
final Account account,
final String authTokenType,
final Bundle options,
final boolean notifyAuthFailure,
final AccountManagerCallback<Bundle> callback,
Handler handler) {
return start(new BaseRoboAccountManagerFuture<Bundle>(callback, handler) {
@Override
public Bundle doWork() throws OperationCanceledException, IOException, AuthenticatorException {
Bundle result = new Bundle();
String authToken = blockingGetAuthToken(account, authTokenType, false);
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
return result;
}
});
}
@Implementation
public AccountManagerFuture<Boolean> hasFeatures(final Account account,
final String[] features,
AccountManagerCallback<Boolean> callback, Handler handler) {
return start(new BaseRoboAccountManagerFuture<Boolean>(callback, handler) {
@Override
public Boolean doWork() throws OperationCanceledException, IOException, AuthenticatorException {
Set<String> availableFeatures = accountFeatures.get(account);
for (String feature : features) {
if (!availableFeatures.contains(feature)) {
return false;
}
}
return true;
}
});
}
@Implementation
public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
final String type, final String[] features,
AccountManagerCallback<Account[]> callback, Handler handler) {
return start(new BaseRoboAccountManagerFuture<Account[]>(callback, handler) {
@Override
public Account[] doWork() throws OperationCanceledException, IOException, AuthenticatorException {
List<Account> result = new ArrayList<>();
Account[] accountsByType = getAccountsByType(type);
for (Account account : accountsByType) {
Set<String> featureSet = accountFeatures.get(account);
if (featureSet.containsAll(Arrays.asList(features))) {
result.add(account);
}
}
return result.toArray(new Account[result.size()]);
}
});
}
private <T extends BaseRoboAccountManagerFuture> T start(T future) {
future.start();
return future;
}
@Implementation(minSdk = JELLY_BEAN_MR2)
public Account[] getAccountsByTypeForPackage (String type, String packageName) {
List<Account> result = new ArrayList<>();
Account[] accountsByType = getAccountsByType(type);
for (Account account : accountsByType) {
if (packageVisibileAccounts.containsKey(account) && packageVisibileAccounts.get(account).contains(packageName)) {
result.add(account);
}
}
return result.toArray(new Account[result.size()]);
}
private abstract class BaseRoboAccountManagerFuture<T> implements AccountManagerFuture<T> {
protected final AccountManagerCallback<T> callback;
private final Handler handler;
protected T result;
private Exception exception;
private boolean started = false;
BaseRoboAccountManagerFuture(AccountManagerCallback<T> callback, Handler handler) {
this.callback = callback;
this.handler = handler == null ? mainHandler : handler;
}
void start() {
if (started) return;
started = true;
try {
result = doWork();
} catch (OperationCanceledException | IOException | AuthenticatorException e) {
exception = e;
}
if (callback != null) {
handler.post(
new Runnable() {
@Override
public void run() {
callback.run(BaseRoboAccountManagerFuture.this);
}
});
}
}
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
return false;
}
@Override
public boolean isCancelled() {
return false;
}
@Override
public boolean isDone() {
return result != null || exception != null || isCancelled();
}
@Override
public T getResult() throws OperationCanceledException, IOException, AuthenticatorException {
start();
if (exception instanceof OperationCanceledException) {
throw new OperationCanceledException(exception);
} else if (exception instanceof IOException) {
throw new IOException(exception);
} else if (exception instanceof AuthenticatorException) {
throw new AuthenticatorException(exception);
}
return result;
}
@Override
public T getResult(long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException {
return getResult();
}
public abstract T doWork() throws OperationCanceledException, IOException, AuthenticatorException;
}
}