package org.robolectric.shadows; import android.accounts.*; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import org.robolectric.annotation.Implementation; import org.robolectric.annotation.Implements; import org.robolectric.internal.Shadow; import org.robolectric.util.ReflectionHelpers; import java.io.IOException; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import org.robolectric.util.Scheduler.IdleState; 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; @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; 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<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 LinkedList<>(); 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 LinkedList<>(); 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; handler.post(new Runnable() { @Override public void run() { try { result = doWork(); } catch (OperationCanceledException | IOException | AuthenticatorException e) { exception = e; } if (callback != null) { 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; } @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; } }