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.
5 package org.chromium.sync.test.util;
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.os.AsyncTask;
21 import android.os.Bundle;
22 import android.os.Handler;
23 import android.util.Log;
25 import org.chromium.sync.signin.AccountManagerDelegate;
26 import org.chromium.sync.signin.AccountManagerHelper;
28 import java.io.IOException;
29 import java.util.HashSet;
30 import java.util.LinkedList;
31 import java.util.List;
33 import java.util.UUID;
34 import java.util.concurrent.Callable;
35 import java.util.concurrent.CancellationException;
36 import java.util.concurrent.ExecutionException;
37 import java.util.concurrent.Executor;
38 import java.util.concurrent.FutureTask;
39 import java.util.concurrent.LinkedBlockingDeque;
40 import java.util.concurrent.ThreadPoolExecutor;
41 import java.util.concurrent.TimeUnit;
42 import java.util.concurrent.TimeoutException;
44 import javax.annotation.Nullable;
47 * The MockAccountManager helps out if you want to mock out all calls to the Android AccountManager.
49 * You should provide a set of accounts as a constructor argument, or use the more direct approach
50 * and provide an array of AccountHolder objects.
52 * Currently, this implementation supports adding and removing accounts, handling credentials
53 * (including confirming them), and handling of dummy auth tokens.
55 * If you want the MockAccountManager to popup an activity for granting/denying access to an
56 * authtokentype for a given account, use prepareGrantAppPermission(...).
58 * If you want to auto-approve a given authtokentype, use addAccountHolderExplicitly(...) with
59 * an AccountHolder you have built with hasBeenAccepted("yourAuthTokenType", true).
61 * If you want to auto-approve all auth token types for a given account, use the {@link
62 * AccountHolder} builder method alwaysAccept(true).
64 public class MockAccountManager implements AccountManagerDelegate {
66 private static final String TAG = "MockAccountManager";
68 private static final int WAIT_TIME_FOR_GRANT_BROADCAST_MS = 20000;
70 static final String MUTEX_WAIT_ACTION =
71 "org.chromium.sync.test.util.MockAccountManager.MUTEX_WAIT_ACTION";
73 protected final Context mContext;
75 private final Context mTestContext;
77 private final Set<AccountHolder> mAccounts;
79 private final List<AccountAuthTokenPreparation> mAccountPermissionPreparations;
81 private final Handler mMainHandler;
83 private final SingleThreadedExecutor mExecutor;
85 public MockAccountManager(Context context, Context testContext, Account... accounts) {
87 // The manifest that is backing testContext needs to provide the
88 // MockGrantCredentialsPermissionActivity.
89 mTestContext = testContext;
90 mMainHandler = new Handler(mContext.getMainLooper());
91 mExecutor = new SingleThreadedExecutor();
92 mAccounts = new HashSet<AccountHolder>();
93 mAccountPermissionPreparations = new LinkedList<AccountAuthTokenPreparation>();
94 if (accounts != null) {
95 for (Account account : accounts) {
96 mAccounts.add(AccountHolder.create().account(account).alwaysAccept(true).build());
101 private static class SingleThreadedExecutor extends ThreadPoolExecutor {
102 public SingleThreadedExecutor() {
103 super(1, 1, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
108 public Account[] getAccounts() {
109 return getAccountsByType(null);
113 public Account[] getAccountsByType(@Nullable String type) {
114 if(!AccountManagerHelper.GOOGLE_ACCOUNT_TYPE.equals(type)) {
115 throw new IllegalArgumentException("Invalid account type: " + type);
117 if (mAccounts == null) {
118 return new Account[0];
120 Account[] accounts = new Account[mAccounts.size()];
122 for (AccountHolder ah : mAccounts) {
123 accounts[i++] = ah.getAccount();
130 public boolean addAccountExplicitly(Account account, String password, Bundle userdata) {
131 AccountHolder accountHolder =
132 AccountHolder.create().account(account).password(password).build();
133 return addAccountHolderExplicitly(accountHolder);
136 public boolean addAccountHolderExplicitly(AccountHolder accountHolder) {
137 boolean result = mAccounts.add(accountHolder);
138 postAsyncAccountChangedEvent();
143 public AccountManagerFuture<Boolean> removeAccount(Account account,
144 AccountManagerCallback<Boolean> callback, Handler handler) {
145 mAccounts.remove(getAccountHolder(account));
146 postAsyncAccountChangedEvent();
147 return runTask(mExecutor,
148 new AccountManagerTask<Boolean>(handler, callback, new Callable<Boolean>() {
150 public Boolean call() throws Exception {
151 // Removal always successful.
158 public String getPassword(Account account) {
159 return getAccountHolder(account).getPassword();
163 public void setPassword(Account account, String password) {
164 mAccounts.add(getAccountHolder(account).withPassword(password));
168 public void clearPassword(Account account) {
169 setPassword(account, null);
173 public AccountManagerFuture<Bundle> confirmCredentials(Account account, Bundle bundle,
174 Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
175 String password = bundle.getString(AccountManager.KEY_PASSWORD);
176 if (password == null) {
177 throw new IllegalArgumentException("Password is null");
179 final AccountHolder accountHolder = getAccountHolder(account);
180 final boolean correctPassword = password.equals(accountHolder.getPassword());
181 return runTask(mExecutor,
182 new AccountManagerTask<Bundle>(handler, callback, new Callable<Bundle>() {
184 public Bundle call() throws Exception {
185 Bundle result = new Bundle();
186 result.putString(AccountManager.KEY_ACCOUNT_NAME, accountHolder.getAccount().name);
188 AccountManager.KEY_ACCOUNT_TYPE, AccountManagerHelper.GOOGLE_ACCOUNT_TYPE);
189 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, correctPassword);
196 public String blockingGetAuthToken(Account account, String authTokenType,
197 boolean notifyAuthFailure)
198 throws OperationCanceledException, IOException, AuthenticatorException {
199 AccountHolder accountHolder = getAccountHolder(account);
200 if (accountHolder.hasBeenAccepted(authTokenType)) {
201 // If account has already been accepted we can just return the auth token.
202 return internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
204 AccountAuthTokenPreparation prepared = getPreparedPermission(account, authTokenType);
205 Intent intent = newGrantCredentialsPermissionIntent(false, account, authTokenType);
206 waitForActivity(mContext, intent);
207 applyPreparedPermission(prepared);
208 return internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
212 public AccountManagerFuture<Bundle> getAuthToken(Account account, String authTokenType,
213 Bundle options, Activity activity, AccountManagerCallback<Bundle> callback,
215 return getAuthTokenFuture(account, authTokenType, activity, callback, handler);
219 public AccountManagerFuture<Bundle> getAuthToken(Account account, String authTokenType,
220 boolean notifyAuthFailure, AccountManagerCallback<Bundle> callback, Handler handler) {
221 return getAuthTokenFuture(account, authTokenType, null, callback, handler);
224 private AccountManagerFuture<Bundle> getAuthTokenFuture(Account account, String authTokenType,
225 Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
226 final AccountHolder ah = getAccountHolder(account);
227 if (ah.hasBeenAccepted(authTokenType)) {
228 final String authToken = internalGenerateAndStoreAuthToken(ah, authTokenType);
229 return runTask(mExecutor,
230 new AccountManagerAuthTokenTask(activity, handler, callback,
231 account, authTokenType,
232 new Callable<Bundle>() {
234 public Bundle call() throws Exception {
235 return getAuthTokenBundle(ah.getAccount(), authToken);
239 Log.d(TAG, "getAuthTokenFuture: Account " + ah.getAccount() +
240 " is asking for permission for " + authTokenType);
241 final Intent intent = newGrantCredentialsPermissionIntent(
242 activity != null, account, authTokenType);
243 return runTask(mExecutor,
244 new AccountManagerAuthTokenTask(activity, handler, callback,
245 account, authTokenType,
246 new Callable<Bundle>() {
248 public Bundle call() throws Exception {
249 Bundle result = new Bundle();
250 result.putParcelable(AccountManager.KEY_INTENT, intent);
257 private static Bundle getAuthTokenBundle(Account account, String authToken) {
258 Bundle result = new Bundle();
259 result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
260 result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
261 result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
265 private String internalGenerateAndStoreAuthToken(AccountHolder ah, String authTokenType) {
266 synchronized (mAccounts) {
267 // Some tests register auth tokens with value null, and those should be preserved.
268 if (!ah.hasAuthTokenRegistered(authTokenType) &&
269 ah.getAuthToken(authTokenType) == null) {
270 // No authtoken registered. Need to create one.
271 String authToken = UUID.randomUUID().toString();
272 Log.d(TAG, "Created new auth token for " + ah.getAccount() +
273 ": autTokenType = " + authTokenType + ", authToken = " + authToken);
274 ah = ah.withAuthToken(authTokenType, authToken);
278 return ah.getAuthToken(authTokenType);
282 public String peekAuthToken(Account account, String authTokenType) {
283 return getAccountHolder(account).getAuthToken(authTokenType);
287 public void invalidateAuthToken(String accountType, String authToken) {
288 if(!AccountManagerHelper.GOOGLE_ACCOUNT_TYPE.equals(accountType)) {
289 throw new IllegalArgumentException("Invalid account type: " + accountType);
291 if (authToken == null) {
292 throw new IllegalArgumentException("AuthToken can not be null");
294 for (AccountHolder ah : mAccounts) {
295 if (ah.removeAuthToken(authToken)) {
302 public AuthenticatorDescription[] getAuthenticatorTypes() {
303 AuthenticatorDescription googleAuthenticator = new AuthenticatorDescription(
304 AccountManagerHelper.GOOGLE_ACCOUNT_TYPE, "p1", 0, 0, 0, 0);
306 return new AuthenticatorDescription[] { googleAuthenticator };
309 public void prepareAllowAppPermission(Account account, String authTokenType) {
310 addPreparedAppPermission(new AccountAuthTokenPreparation(account, authTokenType, true));
313 public void prepareDenyAppPermission(Account account, String authTokenType) {
314 addPreparedAppPermission(new AccountAuthTokenPreparation(account, authTokenType, false));
317 private void addPreparedAppPermission(AccountAuthTokenPreparation accountAuthTokenPreparation) {
318 Log.d(TAG, "Adding " + accountAuthTokenPreparation);
319 mAccountPermissionPreparations.add(accountAuthTokenPreparation);
322 private AccountAuthTokenPreparation getPreparedPermission(Account account,
323 String authTokenType) {
324 for (AccountAuthTokenPreparation accountPrep : mAccountPermissionPreparations) {
325 if (accountPrep.getAccount().equals(account) &&
326 accountPrep.getAuthTokenType().equals(authTokenType)) {
333 private void applyPreparedPermission(AccountAuthTokenPreparation prep) {
335 Log.d(TAG, "Applying " + prep);
336 mAccountPermissionPreparations.remove(prep);
337 mAccounts.add(getAccountHolder(prep.getAccount()).withHasBeenAccepted(
338 prep.getAuthTokenType(), prep.isAllowed()));
342 private Intent newGrantCredentialsPermissionIntent(boolean hasActivity, Account account,
343 String authTokenType) {
344 Intent intent = new Intent();
345 intent.setComponent(new ComponentName(mTestContext,
346 MockGrantCredentialsPermissionActivity.class.getCanonicalName()));
347 intent.putExtra(MockGrantCredentialsPermissionActivity.ACCOUNT, account);
348 intent.putExtra(MockGrantCredentialsPermissionActivity.AUTH_TOKEN_TYPE, authTokenType);
350 // No activity provided, so we help the caller by adding the new task flag
351 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
356 private AccountHolder getAccountHolder(Account account) {
357 if (account == null) {
358 throw new IllegalArgumentException("Account can not be null");
360 for (AccountHolder accountHolder : mAccounts) {
361 if (account.equals(accountHolder.getAccount())) {
362 return accountHolder;
365 throw new IllegalArgumentException("Can not find AccountHolder for account " + account);
368 private static <T> AccountManagerFuture<T> runTask(Executor executorService,
369 AccountManagerTask<T> accountManagerBundleTask) {
370 executorService.execute(accountManagerBundleTask);
371 return accountManagerBundleTask;
374 private class AccountManagerTask<T> extends FutureTask<T> implements AccountManagerFuture<T> {
376 protected final Handler mHandler;
378 protected final AccountManagerCallback<T> mCallback;
380 protected final Callable<T> mCallable;
382 public AccountManagerTask(Handler handler,
383 AccountManagerCallback<T> callback, Callable<T> callable) {
384 super(new Callable<T>() {
386 public T call() throws Exception {
387 throw new IllegalStateException("this should never be called, "
388 + "but call must be overridden.");
392 mCallback = callback;
393 mCallable = callable;
396 private T internalGetResult(long timeout, TimeUnit unit)
397 throws OperationCanceledException, IOException, AuthenticatorException {
402 return get(timeout, unit);
404 } catch (CancellationException e) {
405 throw new OperationCanceledException();
406 } catch (TimeoutException e) {
407 // Fall through and cancel.
408 } catch (InterruptedException e) {
409 // Fall through and cancel.
410 } catch (ExecutionException e) {
411 final Throwable cause = e.getCause();
412 if (cause instanceof IOException) {
413 throw (IOException) cause;
414 } else if (cause instanceof UnsupportedOperationException) {
415 throw new AuthenticatorException(cause);
416 } else if (cause instanceof AuthenticatorException) {
417 throw (AuthenticatorException) cause;
418 } else if (cause instanceof RuntimeException) {
419 throw (RuntimeException) cause;
420 } else if (cause instanceof Error) {
423 throw new IllegalStateException(cause);
426 cancel(true /* Interrupt if running. */);
428 throw new OperationCanceledException();
433 throws OperationCanceledException, IOException, AuthenticatorException {
434 return internalGetResult(-1, null);
438 public T getResult(long timeout, TimeUnit unit)
439 throws OperationCanceledException, IOException, AuthenticatorException {
440 return internalGetResult(timeout, unit);
446 set(mCallable.call());
447 } catch (Exception e) {
453 protected void done() {
454 if (mCallback != null) {
455 postToHandler(getHandler(), mCallback, this);
459 protected Handler getHandler() {
460 return mHandler == null ? mMainHandler : mHandler;
465 private static <T> void postToHandler(Handler handler, final AccountManagerCallback<T> callback,
466 final AccountManagerFuture<T> future) {
467 handler.post(new Runnable() {
470 callback.run(future);
475 private class AccountManagerAuthTokenTask extends AccountManagerTask<Bundle> {
477 private final Activity mActivity;
479 private final AccountAuthTokenPreparation mAccountAuthTokenPreparation;
481 private final Account mAccount;
483 private final String mAuthTokenType;
485 public AccountManagerAuthTokenTask(Activity activity, Handler handler,
486 AccountManagerCallback<Bundle> callback,
487 Account account, String authTokenType,
488 Callable<Bundle> callable) {
489 super(handler, callback, callable);
490 mActivity = activity;
491 mAccountAuthTokenPreparation = getPreparedPermission(account, authTokenType);
493 mAuthTokenType = authTokenType;
499 Bundle bundle = mCallable.call();
500 Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
501 if (intent != null) {
502 // Start the intent activity and wait for it to finish.
503 if (mActivity != null) {
504 waitForActivity(mActivity, intent);
506 waitForActivity(mContext, intent);
508 if (mAccountAuthTokenPreparation == null) {
509 throw new IllegalStateException("No account preparation ready for " +
510 mAccount + ", authTokenType = " + mAuthTokenType +
511 ". Add a call to either prepareGrantAppPermission(...) or " +
512 "prepareRevokeAppPermission(...) in your test before asking for " +
515 // We have shown the Allow/Deny activity, and it has gone away. We can now
516 // apply the pre-stored permission.
517 applyPreparedPermission(mAccountAuthTokenPreparation);
518 generateResult(getAccountHolder(mAccount), mAuthTokenType);
523 } catch (Exception e) {
528 private void generateResult(AccountHolder accountHolder, String authTokenType)
529 throws OperationCanceledException {
530 if (accountHolder.hasBeenAccepted(authTokenType)) {
531 String authToken = internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
532 // Return a valid auth token.
533 set(getAuthTokenBundle(accountHolder.getAccount(), authToken));
535 // Throw same exception as when user clicks "Deny".
536 throw new OperationCanceledException("User denied request");
542 * This method starts {@link MockGrantCredentialsPermissionActivity} and waits for it
543 * to be started before it returns.
545 * @param context the context to start the intent in
546 * @param intent the intent to use to start MockGrantCredentialsPermissionActivity
548 private void waitForActivity(Context context, Intent intent) {
549 final Object mutex = new Object();
550 BroadcastReceiver receiver = new BroadcastReceiver() {
552 public void onReceive(Context context, Intent intent) {
553 synchronized (mutex) {
558 if (!MockGrantCredentialsPermissionActivity.class.getCanonicalName().
559 equals(intent.getComponent().getClassName())) {
560 throw new IllegalArgumentException("Can only wait for "
561 + "MockGrantCredentialsPermissionActivity");
563 mContext.registerReceiver(receiver, new IntentFilter(MUTEX_WAIT_ACTION));
564 context.startActivity(intent);
566 Log.d(TAG, "Waiting for broadcast of " + MUTEX_WAIT_ACTION);
567 synchronized (mutex) {
568 mutex.wait(WAIT_TIME_FOR_GRANT_BROADCAST_MS);
570 } catch (InterruptedException e) {
571 throw new IllegalStateException("Got unexpected InterruptedException");
573 Log.d(TAG, "Got broadcast of " + MUTEX_WAIT_ACTION);
574 mContext.unregisterReceiver(receiver);
577 private void postAsyncAccountChangedEvent() {
578 // Mimic that this does not happen on the main thread.
579 new AsyncTask<Void, Void, Void>() {
581 protected Void doInBackground(Void... params) {
582 mContext.sendBroadcast(new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION));
589 * Internal class for storage of prepared account auth token permissions.
591 * This is used internally by {@link MockAccountManager} to mock the same behavior as clicking
592 * Allow/Deny in the Android {@link GrantCredentialsPermissionActivity}.
594 private static class AccountAuthTokenPreparation {
596 private final Account mAccount;
598 private final String mAuthTokenType;
600 private final boolean mAllowed;
602 private AccountAuthTokenPreparation(Account account, String authTokenType,
605 mAuthTokenType = authTokenType;
609 public Account getAccount() {
613 public String getAuthTokenType() {
614 return mAuthTokenType;
617 public boolean isAllowed() {
622 public String toString() {
623 return "AccountAuthTokenPreparation{" +
624 "mAccount=" + mAccount +
625 ", mAuthTokenType='" + mAuthTokenType + '\'' +
626 ", mAllowed=" + mAllowed +