Home | History | Annotate | Download | only in util
      1 // Copyright 2013 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 package org.chromium.sync.test.util;
      6 
      7 import android.accounts.Account;
      8 import android.accounts.AccountManager;
      9 import android.accounts.AccountManagerCallback;
     10 import android.accounts.AccountManagerFuture;
     11 import android.accounts.AuthenticatorDescription;
     12 import android.accounts.AuthenticatorException;
     13 import android.accounts.OperationCanceledException;
     14 import android.app.Activity;
     15 import android.content.BroadcastReceiver;
     16 import android.content.ComponentName;
     17 import android.content.Context;
     18 import android.content.Intent;
     19 import android.content.IntentFilter;
     20 import android.content.pm.ActivityInfo;
     21 import android.content.pm.PackageManager;
     22 import android.os.AsyncTask;
     23 import android.os.Bundle;
     24 import android.os.Handler;
     25 import android.util.Log;
     26 
     27 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
     28 
     29 import org.chromium.sync.signin.AccountManagerDelegate;
     30 import org.chromium.sync.signin.AccountManagerHelper;
     31 
     32 import java.io.IOException;
     33 import java.util.HashSet;
     34 import java.util.LinkedList;
     35 import java.util.List;
     36 import java.util.Set;
     37 import java.util.UUID;
     38 import java.util.concurrent.Callable;
     39 import java.util.concurrent.CancellationException;
     40 import java.util.concurrent.ExecutionException;
     41 import java.util.concurrent.Executor;
     42 import java.util.concurrent.FutureTask;
     43 import java.util.concurrent.LinkedBlockingDeque;
     44 import java.util.concurrent.ThreadPoolExecutor;
     45 import java.util.concurrent.TimeUnit;
     46 import java.util.concurrent.TimeoutException;
     47 
     48 import javax.annotation.Nullable;
     49 
     50 /**
     51  * The MockAccountManager helps out if you want to mock out all calls to the Android AccountManager.
     52  *
     53  * You should provide a set of accounts as a constructor argument, or use the more direct approach
     54  * and provide an array of AccountHolder objects.
     55  *
     56  * Currently, this implementation supports adding and removing accounts, handling credentials
     57  * (including confirming them), and handling of dummy auth tokens.
     58  *
     59  * If you want the MockAccountManager to popup an activity for granting/denying access to an
     60  * authtokentype for a given account, use prepareGrantAppPermission(...).
     61  *
     62  * If you want to auto-approve a given authtokentype, use addAccountHolderExplicitly(...) with
     63  * an AccountHolder you have built with hasBeenAccepted("yourAuthTokenType", true).
     64  *
     65  * If you want to auto-approve all auth token types for a given account, use the {@link
     66  * AccountHolder} builder method alwaysAccept(true).
     67  */
     68 public class MockAccountManager implements AccountManagerDelegate {
     69 
     70     private static final String TAG = "MockAccountManager";
     71 
     72     private static final long WAIT_TIME_FOR_GRANT_BROADCAST_MS = scaleTimeout(20000);
     73 
     74     static final String MUTEX_WAIT_ACTION =
     75             "org.chromium.sync.test.util.MockAccountManager.MUTEX_WAIT_ACTION";
     76 
     77     protected final Context mContext;
     78 
     79     private final Context mTestContext;
     80 
     81     private final Set<AccountHolder> mAccounts;
     82 
     83     private final List<AccountAuthTokenPreparation> mAccountPermissionPreparations;
     84 
     85     private final Handler mMainHandler;
     86 
     87     private final SingleThreadedExecutor mExecutor;
     88 
     89     public MockAccountManager(Context context, Context testContext, Account... accounts) {
     90         mContext = context;
     91         // The manifest that is backing testContext needs to provide the
     92         // MockGrantCredentialsPermissionActivity.
     93         mTestContext = testContext;
     94         mMainHandler = new Handler(mContext.getMainLooper());
     95         mExecutor = new SingleThreadedExecutor();
     96         mAccounts = new HashSet<AccountHolder>();
     97         mAccountPermissionPreparations = new LinkedList<AccountAuthTokenPreparation>();
     98         if (accounts != null) {
     99             for (Account account : accounts) {
    100                 mAccounts.add(AccountHolder.create().account(account).alwaysAccept(true).build());
    101             }
    102         }
    103     }
    104 
    105     private static class SingleThreadedExecutor extends ThreadPoolExecutor {
    106         public SingleThreadedExecutor() {
    107             super(1, 1, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
    108         }
    109     }
    110 
    111     @Override
    112     public Account[] getAccounts() {
    113         return getAccountsByType(null);
    114     }
    115 
    116     @Override
    117     public Account[] getAccountsByType(@Nullable String type) {
    118         if (!AccountManagerHelper.GOOGLE_ACCOUNT_TYPE.equals(type)) {
    119             throw new IllegalArgumentException("Invalid account type: " + type);
    120         }
    121         if (mAccounts == null) {
    122             return new Account[0];
    123         } else {
    124             Account[] accounts = new Account[mAccounts.size()];
    125             int i = 0;
    126             for (AccountHolder ah : mAccounts) {
    127                 accounts[i++] = ah.getAccount();
    128             }
    129             return accounts;
    130         }
    131     }
    132 
    133     @Override
    134     public boolean addAccountExplicitly(Account account, String password, Bundle userdata) {
    135         AccountHolder accountHolder =
    136                 AccountHolder.create().account(account).password(password).build();
    137         return addAccountHolderExplicitly(accountHolder);
    138     }
    139 
    140     public boolean addAccountHolderExplicitly(AccountHolder accountHolder) {
    141         boolean result = mAccounts.add(accountHolder);
    142         postAsyncAccountChangedEvent();
    143         return result;
    144     }
    145 
    146     public boolean removeAccountHolderExplicitly(AccountHolder accountHolder) {
    147         boolean result = mAccounts.remove(accountHolder);
    148         postAsyncAccountChangedEvent();
    149         return result;
    150     }
    151 
    152     @Override
    153     public AccountManagerFuture<Boolean> removeAccount(Account account,
    154             AccountManagerCallback<Boolean> callback, Handler handler) {
    155         mAccounts.remove(getAccountHolder(account));
    156         postAsyncAccountChangedEvent();
    157         return runTask(mExecutor,
    158                 new AccountManagerTask<Boolean>(handler, callback, new Callable<Boolean>() {
    159                     @Override
    160                     public Boolean call() throws Exception {
    161                         // Removal always successful.
    162                         return true;
    163                     }
    164                 }));
    165     }
    166 
    167     @Override
    168     public String getPassword(Account account) {
    169         return getAccountHolder(account).getPassword();
    170     }
    171 
    172     @Override
    173     public void setPassword(Account account, String password) {
    174         mAccounts.add(getAccountHolder(account).withPassword(password));
    175     }
    176 
    177     @Override
    178     public void clearPassword(Account account) {
    179         setPassword(account, null);
    180     }
    181 
    182     @Override
    183     public AccountManagerFuture<Bundle> confirmCredentials(Account account, Bundle bundle,
    184             Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
    185         String password = bundle.getString(AccountManager.KEY_PASSWORD);
    186         if (password == null) {
    187             throw new IllegalArgumentException("Password is null");
    188         }
    189         final AccountHolder accountHolder = getAccountHolder(account);
    190         final boolean correctPassword = password.equals(accountHolder.getPassword());
    191         return runTask(mExecutor,
    192                 new AccountManagerTask<Bundle>(handler, callback, new Callable<Bundle>() {
    193             @Override
    194             public Bundle call() throws Exception {
    195                 Bundle result = new Bundle();
    196                 result.putString(AccountManager.KEY_ACCOUNT_NAME, accountHolder.getAccount().name);
    197                 result.putString(
    198                         AccountManager.KEY_ACCOUNT_TYPE, AccountManagerHelper.GOOGLE_ACCOUNT_TYPE);
    199                 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, correctPassword);
    200                 return result;
    201             }
    202         }));
    203     }
    204 
    205     @Override
    206     public String blockingGetAuthToken(Account account, String authTokenType,
    207             boolean notifyAuthFailure)
    208             throws OperationCanceledException, IOException, AuthenticatorException {
    209         AccountHolder accountHolder = getAccountHolder(account);
    210         if (accountHolder.hasBeenAccepted(authTokenType)) {
    211             // If account has already been accepted we can just return the auth token.
    212             return internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
    213         }
    214         AccountAuthTokenPreparation prepared = getPreparedPermission(account, authTokenType);
    215         Intent intent = newGrantCredentialsPermissionIntent(false, account, authTokenType);
    216         waitForActivity(mContext, intent);
    217         applyPreparedPermission(prepared);
    218         return internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
    219     }
    220 
    221     @Override
    222     public AccountManagerFuture<Bundle> getAuthToken(Account account, String authTokenType,
    223             Bundle options, Activity activity, AccountManagerCallback<Bundle> callback,
    224             Handler handler) {
    225         return getAuthTokenFuture(account, authTokenType, activity, callback, handler);
    226     }
    227 
    228     @Override
    229     public AccountManagerFuture<Bundle> getAuthToken(Account account, String authTokenType,
    230             boolean notifyAuthFailure, AccountManagerCallback<Bundle> callback, Handler handler) {
    231         return getAuthTokenFuture(account, authTokenType, null, callback, handler);
    232     }
    233 
    234     private AccountManagerFuture<Bundle> getAuthTokenFuture(Account account, String authTokenType,
    235             Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
    236         final AccountHolder ah = getAccountHolder(account);
    237         if (ah.hasBeenAccepted(authTokenType)) {
    238             final String authToken = internalGenerateAndStoreAuthToken(ah, authTokenType);
    239             return runTask(mExecutor,
    240                     new AccountManagerAuthTokenTask(activity, handler, callback,
    241                             account, authTokenType,
    242                             new Callable<Bundle>() {
    243                         @Override
    244                         public Bundle call() throws Exception {
    245                             return getAuthTokenBundle(ah.getAccount(), authToken);
    246                         }
    247                     }));
    248         } else {
    249             Log.d(TAG, "getAuthTokenFuture: Account " + ah.getAccount() +
    250                     " is asking for permission for " + authTokenType);
    251             final Intent intent = newGrantCredentialsPermissionIntent(
    252                     activity != null, account, authTokenType);
    253             return runTask(mExecutor,
    254                     new AccountManagerAuthTokenTask(activity, handler, callback,
    255                             account, authTokenType,
    256                             new Callable<Bundle>() {
    257                         @Override
    258                         public Bundle call() throws Exception {
    259                             Bundle result = new Bundle();
    260                             result.putParcelable(AccountManager.KEY_INTENT, intent);
    261                             return result;
    262                         }
    263                     }));
    264         }
    265     }
    266 
    267     private static Bundle getAuthTokenBundle(Account account, String authToken) {
    268         Bundle result = new Bundle();
    269         result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
    270         result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
    271         result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
    272         return result;
    273     }
    274 
    275     private String internalGenerateAndStoreAuthToken(AccountHolder ah, String authTokenType) {
    276         synchronized (mAccounts) {
    277             // Some tests register auth tokens with value null, and those should be preserved.
    278             if (!ah.hasAuthTokenRegistered(authTokenType) &&
    279                     ah.getAuthToken(authTokenType) == null) {
    280                 // No authtoken registered. Need to create one.
    281                 String authToken = UUID.randomUUID().toString();
    282                 Log.d(TAG, "Created new auth token for " + ah.getAccount() +
    283                         ": autTokenType = " + authTokenType + ", authToken = " + authToken);
    284                 ah = ah.withAuthToken(authTokenType, authToken);
    285                 mAccounts.add(ah);
    286             }
    287         }
    288         return ah.getAuthToken(authTokenType);
    289     }
    290 
    291     @Override
    292     public String peekAuthToken(Account account, String authTokenType) {
    293         return getAccountHolder(account).getAuthToken(authTokenType);
    294     }
    295 
    296     @Override
    297     public void invalidateAuthToken(String accountType, String authToken) {
    298         if (!AccountManagerHelper.GOOGLE_ACCOUNT_TYPE.equals(accountType)) {
    299             throw new IllegalArgumentException("Invalid account type: " + accountType);
    300         }
    301         if (authToken == null) {
    302             throw new IllegalArgumentException("AuthToken can not be null");
    303         }
    304         for (AccountHolder ah : mAccounts) {
    305             if (ah.removeAuthToken(authToken)) {
    306                 break;
    307             }
    308         }
    309     }
    310 
    311     @Override
    312     public AuthenticatorDescription[] getAuthenticatorTypes() {
    313         AuthenticatorDescription googleAuthenticator = new AuthenticatorDescription(
    314                 AccountManagerHelper.GOOGLE_ACCOUNT_TYPE, "p1", 0, 0, 0, 0);
    315 
    316         return new AuthenticatorDescription[] { googleAuthenticator };
    317     }
    318 
    319     public void prepareAllowAppPermission(Account account, String authTokenType) {
    320         addPreparedAppPermission(new AccountAuthTokenPreparation(account, authTokenType, true));
    321     }
    322 
    323     public void prepareDenyAppPermission(Account account, String authTokenType) {
    324         addPreparedAppPermission(new AccountAuthTokenPreparation(account, authTokenType, false));
    325     }
    326 
    327     private void addPreparedAppPermission(AccountAuthTokenPreparation accountAuthTokenPreparation) {
    328         Log.d(TAG, "Adding " + accountAuthTokenPreparation);
    329         mAccountPermissionPreparations.add(accountAuthTokenPreparation);
    330     }
    331 
    332     private AccountAuthTokenPreparation getPreparedPermission(Account account,
    333             String authTokenType) {
    334         for (AccountAuthTokenPreparation accountPrep : mAccountPermissionPreparations) {
    335             if (accountPrep.getAccount().equals(account) &&
    336                     accountPrep.getAuthTokenType().equals(authTokenType)) {
    337                 return accountPrep;
    338             }
    339         }
    340         return null;
    341     }
    342 
    343     private void applyPreparedPermission(AccountAuthTokenPreparation prep) {
    344         if (prep != null) {
    345             Log.d(TAG, "Applying " + prep);
    346             mAccountPermissionPreparations.remove(prep);
    347             mAccounts.add(getAccountHolder(prep.getAccount()).withHasBeenAccepted(
    348                     prep.getAuthTokenType(), prep.isAllowed()));
    349         }
    350     }
    351 
    352     private Intent newGrantCredentialsPermissionIntent(boolean hasActivity, Account account,
    353             String authTokenType) {
    354         ComponentName component = new ComponentName(mTestContext,
    355                 MockGrantCredentialsPermissionActivity.class.getCanonicalName());
    356 
    357         // Make sure we can start the activity.
    358         ActivityInfo ai = null;
    359         try {
    360             ai = mContext.getPackageManager().getActivityInfo(component, 0);
    361         } catch (PackageManager.NameNotFoundException e) {
    362             throw new IllegalStateException(
    363                     "Unable to find " + component.getClassName());
    364         }
    365         if (ai.applicationInfo != mContext.getApplicationInfo() && !ai.exported) {
    366             throw new IllegalStateException(
    367                     "Unable to start " + ai.name + ". " +
    368                     "The accounts you added to MockAccountManager may not be " +
    369                     "configured correctly.");
    370         }
    371 
    372         Intent intent = new Intent();
    373         intent.setComponent(component);
    374         intent.putExtra(MockGrantCredentialsPermissionActivity.ACCOUNT, account);
    375         intent.putExtra(MockGrantCredentialsPermissionActivity.AUTH_TOKEN_TYPE, authTokenType);
    376         if (!hasActivity) {
    377             // No activity provided, so we help the caller by adding the new task flag
    378             intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    379         }
    380         return intent;
    381     }
    382 
    383     private AccountHolder getAccountHolder(Account account) {
    384         if (account == null) {
    385             throw new IllegalArgumentException("Account can not be null");
    386         }
    387         for (AccountHolder accountHolder : mAccounts) {
    388             if (account.equals(accountHolder.getAccount())) {
    389                 return accountHolder;
    390             }
    391         }
    392         throw new IllegalArgumentException("Can not find AccountHolder for account " + account);
    393     }
    394 
    395     private static <T> AccountManagerFuture<T> runTask(Executor executorService,
    396             AccountManagerTask<T> accountManagerBundleTask) {
    397         executorService.execute(accountManagerBundleTask);
    398         return accountManagerBundleTask;
    399     }
    400 
    401     private class AccountManagerTask<T> extends FutureTask<T> implements AccountManagerFuture<T> {
    402 
    403         protected final Handler mHandler;
    404 
    405         protected final AccountManagerCallback<T> mCallback;
    406 
    407         protected final Callable<T> mCallable;
    408 
    409         public AccountManagerTask(Handler handler,
    410                 AccountManagerCallback<T> callback, Callable<T> callable) {
    411             super(new Callable<T>() {
    412                 @Override
    413                 public T call() throws Exception {
    414                     throw new IllegalStateException("this should never be called, "
    415                             + "but call must be overridden.");
    416                 }
    417             });
    418             mHandler = handler;
    419             mCallback = callback;
    420             mCallable = callable;
    421         }
    422 
    423         private T internalGetResult(long timeout, TimeUnit unit)
    424                 throws OperationCanceledException, IOException, AuthenticatorException {
    425             try {
    426                 if (timeout == -1) {
    427                     return get();
    428                 } else {
    429                     return get(timeout, unit);
    430                 }
    431             } catch (CancellationException e) {
    432                 throw new OperationCanceledException();
    433             } catch (TimeoutException e) {
    434                 // Fall through and cancel.
    435             } catch (InterruptedException e) {
    436                 // Fall through and cancel.
    437             } catch (ExecutionException e) {
    438                 final Throwable cause = e.getCause();
    439                 if (cause instanceof IOException) {
    440                     throw (IOException) cause;
    441                 } else if (cause instanceof UnsupportedOperationException) {
    442                     throw new AuthenticatorException(cause);
    443                 } else if (cause instanceof AuthenticatorException) {
    444                     throw (AuthenticatorException) cause;
    445                 } else if (cause instanceof RuntimeException) {
    446                     throw (RuntimeException) cause;
    447                 } else if (cause instanceof Error) {
    448                     throw (Error) cause;
    449                 } else {
    450                     throw new IllegalStateException(cause);
    451                 }
    452             } finally {
    453                 cancel(true /* Interrupt if running. */);
    454             }
    455             throw new OperationCanceledException();
    456         }
    457 
    458         @Override
    459         public T getResult()
    460                 throws OperationCanceledException, IOException, AuthenticatorException {
    461             return internalGetResult(-1, null);
    462         }
    463 
    464         @Override
    465         public T getResult(long timeout, TimeUnit unit)
    466                 throws OperationCanceledException, IOException, AuthenticatorException {
    467             return internalGetResult(timeout, unit);
    468         }
    469 
    470         @Override
    471         public void run() {
    472             try {
    473                 set(mCallable.call());
    474             } catch (Exception e) {
    475                 setException(e);
    476             }
    477         }
    478 
    479         @Override
    480         protected void done() {
    481             if (mCallback != null) {
    482                 postToHandler(getHandler(), mCallback, this);
    483             }
    484         }
    485 
    486         protected Handler getHandler() {
    487             return mHandler == null ? mMainHandler : mHandler;
    488         }
    489 
    490     }
    491 
    492     private static <T> void postToHandler(Handler handler, final AccountManagerCallback<T> callback,
    493             final AccountManagerFuture<T> future) {
    494         handler.post(new Runnable() {
    495             @Override
    496             public void run() {
    497                 callback.run(future);
    498             }
    499         });
    500     }
    501 
    502     private class AccountManagerAuthTokenTask extends AccountManagerTask<Bundle> {
    503 
    504         private final Activity mActivity;
    505 
    506         private final AccountAuthTokenPreparation mAccountAuthTokenPreparation;
    507 
    508         private final Account mAccount;
    509 
    510         private final String mAuthTokenType;
    511 
    512         public AccountManagerAuthTokenTask(Activity activity, Handler handler,
    513                 AccountManagerCallback<Bundle> callback,
    514                 Account account, String authTokenType,
    515                 Callable<Bundle> callable) {
    516             super(handler, callback, callable);
    517             mActivity = activity;
    518             mAccountAuthTokenPreparation = getPreparedPermission(account, authTokenType);
    519             mAccount = account;
    520             mAuthTokenType = authTokenType;
    521         }
    522 
    523         @Override
    524         public void run() {
    525             try {
    526                 Bundle bundle = mCallable.call();
    527                 Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
    528                 if (intent != null) {
    529                     // Start the intent activity and wait for it to finish.
    530                     if (mActivity != null) {
    531                         waitForActivity(mActivity, intent);
    532                     } else {
    533                         waitForActivity(mContext, intent);
    534                     }
    535                     if (mAccountAuthTokenPreparation == null) {
    536                         throw new IllegalStateException("No account preparation ready for " +
    537                                 mAccount + ", authTokenType = " + mAuthTokenType +
    538                                 ". Add a call to either prepareGrantAppPermission(...) or " +
    539                                 "prepareRevokeAppPermission(...) in your test before asking for " +
    540                                 "an auth token");
    541                     } else {
    542                         // We have shown the Allow/Deny activity, and it has gone away. We can now
    543                         // apply the pre-stored permission.
    544                         applyPreparedPermission(mAccountAuthTokenPreparation);
    545                         generateResult(getAccountHolder(mAccount), mAuthTokenType);
    546                     }
    547                 } else {
    548                     set(bundle);
    549                 }
    550             } catch (Exception e) {
    551                 setException(e);
    552             }
    553         }
    554 
    555         private void generateResult(AccountHolder accountHolder, String authTokenType)
    556                 throws OperationCanceledException {
    557             if (accountHolder.hasBeenAccepted(authTokenType)) {
    558                 String authToken = internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
    559                 // Return a valid auth token.
    560                 set(getAuthTokenBundle(accountHolder.getAccount(), authToken));
    561             } else {
    562                 // Throw same exception as when user clicks "Deny".
    563                 throw new OperationCanceledException("User denied request");
    564             }
    565         }
    566     }
    567 
    568     /**
    569      * This method starts {@link MockGrantCredentialsPermissionActivity} and waits for it
    570      * to be started before it returns.
    571      *
    572      * @param context the context to start the intent in
    573      * @param intent the intent to use to start MockGrantCredentialsPermissionActivity
    574      */
    575     private void waitForActivity(Context context, Intent intent) {
    576         final Object mutex = new Object();
    577         BroadcastReceiver receiver = new BroadcastReceiver() {
    578             @Override
    579             public void onReceive(Context context, Intent intent) {
    580                 synchronized (mutex) {
    581                     mutex.notifyAll();
    582                 }
    583             }
    584         };
    585         if (!MockGrantCredentialsPermissionActivity.class.getCanonicalName().
    586                 equals(intent.getComponent().getClassName())) {
    587             throw new IllegalArgumentException("Can only wait for "
    588                     + "MockGrantCredentialsPermissionActivity");
    589         }
    590         mContext.registerReceiver(receiver, new IntentFilter(MUTEX_WAIT_ACTION));
    591         context.startActivity(intent);
    592         try {
    593             Log.d(TAG, "Waiting for broadcast of " + MUTEX_WAIT_ACTION);
    594             synchronized (mutex) {
    595                 mutex.wait(WAIT_TIME_FOR_GRANT_BROADCAST_MS);
    596             }
    597         } catch (InterruptedException e) {
    598             throw new IllegalStateException("Got unexpected InterruptedException");
    599         }
    600         Log.d(TAG, "Got broadcast of " + MUTEX_WAIT_ACTION);
    601         mContext.unregisterReceiver(receiver);
    602     }
    603 
    604     private void postAsyncAccountChangedEvent() {
    605         // Mimic that this does not happen on the main thread.
    606         new AsyncTask<Void, Void, Void>() {
    607             @Override
    608             protected Void doInBackground(Void... params) {
    609                 mContext.sendBroadcast(new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION));
    610                 return null;
    611             }
    612         }.execute();
    613     }
    614 
    615     /**
    616      * Internal class for storage of prepared account auth token permissions.
    617      *
    618      * This is used internally by {@link MockAccountManager} to mock the same behavior as clicking
    619      * Allow/Deny in the Android {@link GrantCredentialsPermissionActivity}.
    620      */
    621     private static class AccountAuthTokenPreparation {
    622 
    623         private final Account mAccount;
    624 
    625         private final String mAuthTokenType;
    626 
    627         private final boolean mAllowed;
    628 
    629         private AccountAuthTokenPreparation(Account account, String authTokenType,
    630                 boolean allowed) {
    631             mAccount = account;
    632             mAuthTokenType = authTokenType;
    633             mAllowed = allowed;
    634         }
    635 
    636         public Account getAccount() {
    637             return mAccount;
    638         }
    639 
    640         public String getAuthTokenType() {
    641             return mAuthTokenType;
    642         }
    643 
    644         public boolean isAllowed() {
    645             return mAllowed;
    646         }
    647 
    648         @Override
    649         public String toString() {
    650             return "AccountAuthTokenPreparation{" +
    651                     "mAccount=" + mAccount +
    652                     ", mAuthTokenType='" + mAuthTokenType + '\'' +
    653                     ", mAllowed=" + mAllowed +
    654                     '}';
    655         }
    656     }
    657 }
    658