All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.robolectric.shadows.ShadowAccountManager Maven / Gradle / Ivy

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 static android.os.Build.VERSION_CODES.O;

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 androidx.annotation.Nullable;
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 accounts = new ArrayList<>();
  private Map> authTokens = new HashMap<>();
  private Map authenticators = new LinkedHashMap<>();
  /**
   * Maps listeners to a set of account types. If null, the listener should be notified for changes
   * to accounts of any type. Otherwise, the listener is only notified of changes to accounts of the
   * given type.
   */
  private Map> listeners = new LinkedHashMap<>();

  private Map> userData = new HashMap<>();
  private Map passwords = new HashMap<>();
  private Map> accountFeatures = new HashMap<>();
  private Map> packageVisibleAccounts = new HashMap<>();

  private List addAccountOptionsList = new ArrayList<>();
  private Handler mainHandler;
  private RoboAccountManagerFuture pendingAddFuture;
  private boolean authenticationErrorOnNextResponse = false;
  private Intent removeAccountIntent;

  @Implementation
  protected void __constructor__(Context context, IAccountManager service) {
    mainHandler = new Handler(context.getMainLooper());
  }

  @Implementation
  protected static AccountManager get(Context context) {
    return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
  }

  @Implementation
  protected Account[] getAccounts() {
    return accounts.toArray(new Account[accounts.size()]);
  }

  @Implementation
  protected Account[] getAccountsByType(String type) {
    if (type == null) {
      return getAccounts();
    }
    List accountsByType = new ArrayList<>();

    for (Account a : accounts) {
      if (type.equals(a.type)) {
        accountsByType.add(a);
      }
    }

    return accountsByType.toArray(new Account[accountsByType.size()]);
  }

  @Implementation
  protected synchronized void setAuthToken(Account account, String tokenType, String authToken) {
    if (accounts.contains(account)) {
      Map tokenMap = authTokens.get(account);
      if (tokenMap == null) {
        tokenMap = new HashMap<>();
        authTokens.put(account, tokenMap);
      }
      tokenMap.put(tokenType, authToken);
    }
  }

  @Implementation
  protected String peekAuthToken(Account account, String tokenType) {
    Map tokenMap = authTokens.get(account);
    if (tokenMap != null) {
      return tokenMap.get(tokenType);
    }
    return null;
  }

  @SuppressWarnings("InconsistentCapitalization")
  @Implementation
  protected 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());
      }
    }

    notifyListeners(account);

    return true;
  }

  @Implementation
  protected 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 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
  protected AccountManagerFuture removeAccount(
      final Account account, AccountManagerCallback callback, Handler handler) {
    if (account == null) {
      throw new IllegalArgumentException("account is null");
    }

    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Boolean doWork() {
            return removeAccountExplicitly(account);
          }
        });
  }

  /**
   * Removes the account unless {@link #setRemoveAccountIntent} has been set. If set, the future
   * Bundle will include the Intent and {@link AccountManager#KEY_BOOLEAN_RESULT} will be false.
   */
  @Implementation(minSdk = LOLLIPOP_MR1)
  protected AccountManagerFuture removeAccount(
      Account account,
      Activity activity,
      AccountManagerCallback callback,
      Handler handler) {
    if (account == null) {
      throw new IllegalArgumentException("account is null");
    }
    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Bundle doWork() {
            Bundle result = new Bundle();
            if (removeAccountIntent == null) {
              result.putBoolean(
                  AccountManager.KEY_BOOLEAN_RESULT, removeAccountExplicitly(account));
            } else {
              result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
              result.putParcelable(AccountManager.KEY_INTENT, removeAccountIntent);
            }
            return result;
          }
        });
  }

  @Implementation(minSdk = LOLLIPOP_MR1)
  protected boolean removeAccountExplicitly(Account account) {
    passwords.remove(account);
    userData.remove(account);
    if (accounts.remove(account)) {
      notifyListeners(account);
      return true;
    }
    return false;
  }

  /**
   * Removes all accounts that have been added.
   */
  public void removeAllAccounts() {
    passwords.clear();
    userData.clear();
    accounts.clear();
  }

  @Implementation
  protected AuthenticatorDescription[] getAuthenticatorTypes() {
    return authenticators.values().toArray(new AuthenticatorDescription[authenticators.size()]);
  }

  @Implementation
  protected void addOnAccountsUpdatedListener(
      final OnAccountsUpdateListener listener, Handler handler, boolean updateImmediately) {
    addOnAccountsUpdatedListener(listener, handler, updateImmediately, /* accountTypes= */ null);
  }

  /**
   * Based on {@link AccountManager#addOnAccountsUpdatedListener(OnAccountsUpdateListener, Handler,
   * boolean, String[])}. {@link Handler} is ignored.
   */
  @Implementation(minSdk = O)
  protected void addOnAccountsUpdatedListener(
      @Nullable final OnAccountsUpdateListener listener,
      @Nullable Handler handler,
      boolean updateImmediately,
      @Nullable String[] accountTypes) {
    // TODO: Match real method behavior by throwing IllegalStateException.
    if (listeners.containsKey(listener)) {
      return;
    }

    Set types = null;
    if (accountTypes != null) {
      types = new HashSet<>(Arrays.asList(accountTypes));
    }
    listeners.put(listener, types);

    if (updateImmediately) {
      notifyListener(listener, types, getAccounts());
    }
  }

  @Implementation
  protected void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
    listeners.remove(listener);
  }

  @Implementation
  protected String getUserData(Account account, String key) {
    if (account == null) {
      throw new IllegalArgumentException("account is null");
    }

    if (!userData.containsKey(account)) {
      return null;
    }

    Map userDataMap = userData.get(account);
    if (userDataMap.containsKey(key)) {
      return userDataMap.get(key);
    }

    return null;
  }

  @Implementation
  protected 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());
    }

    Map userDataMap = userData.get(account);

    if (value == null) {
      userDataMap.remove(key);
    } else {
      userDataMap.put(key, value);
    }
  }

  @Implementation
  protected 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
  protected 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
  protected void invalidateAuthToken(final String accountType, final String authToken) {
    Account[] accountsByType = getAccountsByType(accountType);
    for (Account account : accountsByType) {
      Map tokenMap = authTokens.get(account);
      if (tokenMap != null) {
        Iterator> it = tokenMap.entrySet().iterator();
        while (it.hasNext()) {
          Map.Entry map = it.next();
          if (map.getValue().equals(authToken)) {
            it.remove();
          }
        }
        authTokens.put(account, tokenMap);
      }
    }
  }

  /**
   * Returns a bundle that contains the account session bundle under {@link
   * AccountManager#KEY_ACCOUNT_SESSION_BUNDLE} to later be passed on to {@link
   * AccountManager#finishSession(Bundle,Activity,AccountManagerCallback,Handler)}. The
   * session bundle simply propagates the given {@code accountType} so as not to be empty and is not
   * encrypted as it would be in the real implementation. If an activity isn't provided, resulting
   * bundle will only have a dummy {@link Intent} under {@link AccountManager#KEY_INTENT}.
   *
   * @param accountType An authenticator must exist for the accountType, or else {@link
   *     AuthenticatorException} is thrown.
   * @param authTokenType is ignored.
   * @param requiredFeatures is ignored.
   * @param options is ignored.
   * @param activity if null, only {@link AccountManager#KEY_INTENT} will be present in result.
   * @param callback if not null, will be called with result bundle.
   * @param handler is ignored.
   * @return future for bundle containing {@link AccountManager#KEY_ACCOUNT_SESSION_BUNDLE} if
   *     activity is provided, or {@link AccountManager#KEY_INTENT} otherwise.
   */
  @Implementation(minSdk = O)
  protected AccountManagerFuture startAddAccountSession(
      String accountType,
      String authTokenType,
      String[] requiredFeatures,
      Bundle options,
      Activity activity,
      AccountManagerCallback callback,
      Handler handler) {

    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Bundle doWork() throws AuthenticatorException {
            if (!authenticators.containsKey(accountType)) {
              throw new AuthenticatorException("No authenticator specified for " + accountType);
            }

            Bundle resultBundle = new Bundle();

            if (activity == null) {
              Intent resultIntent = new Intent();
              resultBundle.putParcelable(AccountManager.KEY_INTENT, resultIntent);
            } else {
              // This would actually be an encrypted bundle. Account type is copied as is simply to
              // make it non-empty.
              Bundle accountSessionBundle = new Bundle();
              accountSessionBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
              resultBundle.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, Bundle.EMPTY);
            }

            return resultBundle;
          }
        });
  }

  /**
   * Returns sessionBundle as the result of finishSession.
   *
   * @param sessionBundle is returned as the result bundle.
   * @param activity is ignored.
   * @param callback if not null, will be called with result bundle.
   * @param handler is ignored.
   */
  @Implementation(minSdk = O)
  protected AccountManagerFuture finishSession(
      Bundle sessionBundle,
      Activity activity,
      AccountManagerCallback callback,
      Handler handler) {

    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Bundle doWork() {
            // Just return sessionBundle as the result since it's not really used, allowing it to
            // be easily controlled in tests.
            return sessionBundle;
          }
        });
  }

  /**
   * Based off of private method postToHandler(Handler, OnAccountsUpdateListener, Account[]) in
   * {@link AccountManager}
   */
  private void notifyListener(
      OnAccountsUpdateListener listener,
      @Nullable Set accountTypesToReportOn,
      Account[] allAccounts) {
    if (accountTypesToReportOn != null) {
      ArrayList filtered = new ArrayList<>();
      for (Account account : allAccounts) {
        if (accountTypesToReportOn.contains(account.type)) {
          filtered.add(account);
        }
      }
      listener.onAccountsUpdated(filtered.toArray(new Account[0]));
    } else {
      listener.onAccountsUpdated(allAccounts);
    }
  }

  private void notifyListeners(Account changedAccount) {
    Account[] accounts = getAccounts();
    for (Map.Entry> entry : listeners.entrySet()) {
      OnAccountsUpdateListener listener = entry.getKey();
      Set types = entry.getValue();
      if (types == null || types.contains(changedAccount.type)) {
        notifyListener(listener, types, 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(account);
  }

  /**
   * Adds an account to the AccountManager but when {@link
   * AccountManager#getAccountsByTypeForPackage(String, String)} is called will be included if is in
   * one of the #visibleToPackages
   *
   * @param account User account.
   */
  public void addAccount(Account account, String... visibleToPackages) {
    addAccount(account);
    HashSet value = new HashSet<>();
    Collections.addAll(value, visibleToPackages);
    packageVisibleAccounts.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 {
    private final String accountType;
    private final Activity activity;
    private final Bundle resultBundle;

    RoboAccountManagerFuture(AccountManagerCallback 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 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, "[email protected]");
      }

      return resultBundle;
    }
  }

  @Implementation
  protected AccountManagerFuture addAccount(
      final String accountType,
      String authTokenType,
      String[] requiredFeatures,
      Bundle addAccountOptions,
      Activity activity,
      AccountManagerCallback callback,
      Handler handler) {
    addAccountOptionsList.add(addAccountOptions);
    if (activity == null) {
      // Caller only wants to get the intent, so start the future immediately.
      RoboAccountManagerFuture future =
          new RoboAccountManagerFuture(callback, handler, accountType, null);
      start(future);
      return future;
    } else {
      // Caller wants to start the sign in flow and return the intent with the new account added.
      // Account can be added via ShadowAccountManager#addAccount.
      pendingAddFuture = new RoboAccountManagerFuture(callback, handler, accountType, activity);
      return pendingAddFuture;
    }
  }

  public void setFeatures(Account account, String[] accountFeatures) {
    HashSet 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 previousNames = new HashMap();

  /**
   * 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)
  protected String getPreviousName(Account account) {
    return previousNames.get(account);
  }

  @Implementation
  protected AccountManagerFuture getAuthToken(
      final Account account,
      final String authTokenType,
      final Bundle options,
      final Activity activity,
      final AccountManagerCallback callback,
      Handler handler) {

    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Bundle doWork() throws AuthenticatorException {
            return getAuthToken(account, authTokenType);
          }
        });
  }

  @Implementation
  protected AccountManagerFuture getAuthToken(
      final Account account,
      final String authTokenType,
      final Bundle options,
      final boolean notifyAuthFailure,
      final AccountManagerCallback callback,
      Handler handler) {

    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Bundle doWork() throws AuthenticatorException {
            return getAuthToken(account, authTokenType);
          }
        });
  }

  private Bundle getAuthToken(Account account, String authTokenType) throws 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);

    if (authToken != null) {
      return result;
    }

    if (!authenticators.containsKey(account.type)) {
      throw new AuthenticatorException("No authenticator specified for " + account.type);
    }

    Intent resultIntent = new Intent();
    result.putParcelable(AccountManager.KEY_INTENT, resultIntent);

    return result;
  }

  @Implementation
  protected AccountManagerFuture hasFeatures(
      final Account account,
      final String[] features,
      AccountManagerCallback callback,
      Handler handler) {
    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Boolean doWork() {
            Set availableFeatures = accountFeatures.get(account);
            for (String feature : features) {
              if (!availableFeatures.contains(feature)) {
                return false;
              }
            }
            return true;
          }
        });
  }

  @Implementation
  protected AccountManagerFuture getAccountsByTypeAndFeatures(
      final String type,
      final String[] features,
      AccountManagerCallback callback,
      Handler handler) {
    return start(
        new BaseRoboAccountManagerFuture(callback, handler) {
          @Override
          public Account[] doWork() throws AuthenticatorException {

            if (authenticationErrorOnNextResponse) {
              setAuthenticationErrorOnNextResponse(false);
              throw new AuthenticatorException();
            }

            List result = new ArrayList<>();

            Account[] accountsByType = getAccountsByType(type);
            for (Account account : accountsByType) {
              Set featureSet = accountFeatures.get(account);
              if (features == null
                  || (featureSet != null && featureSet.containsAll(Arrays.asList(features)))) {
                result.add(account);
              }
            }
            return result.toArray(new Account[result.size()]);
          }
        });
  }

  private  T start(T future) {
    future.start();
    return future;
  }

  @Implementation(minSdk = JELLY_BEAN_MR2)
  protected Account[] getAccountsByTypeForPackage(String type, String packageName) {
    List result = new ArrayList<>();

    Account[] accountsByType = getAccountsByType(type);
    for (Account account : accountsByType) {
      if (packageVisibleAccounts.containsKey(account)
          && packageVisibleAccounts.get(account).contains(packageName)) {
        result.add(account);
      }
    }

    return result.toArray(new Account[result.size()]);
  }

  /**
   * Sets authenticator exception, which will be thrown by {@link #getAccountsByTypeAndFeatures}.
   *
   * @param authenticationErrorOnNextResponse to set flag that exception will be thrown on next
   *     response.
   */
  public void setAuthenticationErrorOnNextResponse(boolean authenticationErrorOnNextResponse) {
    this.authenticationErrorOnNextResponse = authenticationErrorOnNextResponse;
  }

  /**
   * Sets the intent to include in Bundle result from {@link #removeAccount} if Activity is given.
   *
   * @param removeAccountIntent the intent to surface as {@link AccountManager#KEY_INTENT}.
   */
  public void setRemoveAccountIntent(Intent removeAccountIntent) {
    this.removeAccountIntent = removeAccountIntent;
  }

  public Map> getListeners() {
    return listeners;
  }

  private abstract class BaseRoboAccountManagerFuture implements AccountManagerFuture {
    protected final AccountManagerCallback callback;
    private final Handler handler;
    protected T result;
    private Exception exception;
    private boolean started = false;

    BaseRoboAccountManagerFuture(AccountManagerCallback 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;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy