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.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;
27 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
29 import org.chromium.sync.signin.AccountManagerDelegate;
30 import org.chromium.sync.signin.AccountManagerHelper;
32 import java.io.IOException;
33 import java.util.HashSet;
34 import java.util.LinkedList;
35 import java.util.List;
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;
48 import javax.annotation.Nullable;
51 * The MockAccountManager helps out if you want to mock out all calls to the Android AccountManager.
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.
56 * Currently, this implementation supports adding and removing accounts, handling credentials
57 * (including confirming them), and handling of dummy auth tokens.
59 * If you want the MockAccountManager to popup an activity for granting/denying access to an
60 * authtokentype for a given account, use prepareGrantAppPermission(...).
62 * If you want to auto-approve a given authtokentype, use addAccountHolderExplicitly(...) with
63 * an AccountHolder you have built with hasBeenAccepted("yourAuthTokenType", true).
65 * If you want to auto-approve all auth token types for a given account, use the {@link
66 * AccountHolder} builder method alwaysAccept(true).
68 public class MockAccountManager implements AccountManagerDelegate {
70 private static final String TAG = "MockAccountManager";
72 private static final long WAIT_TIME_FOR_GRANT_BROADCAST_MS = scaleTimeout(20000);
74 static final String MUTEX_WAIT_ACTION =
75 "org.chromium.sync.test.util.MockAccountManager.MUTEX_WAIT_ACTION";
77 protected final Context mContext;
79 private final Context mTestContext;
81 private final Set<AccountHolder> mAccounts;
83 private final List<AccountAuthTokenPreparation> mAccountPermissionPreparations;
85 private final Handler mMainHandler;
87 private final SingleThreadedExecutor mExecutor;
89 public MockAccountManager(Context context, Context testContext, Account... accounts) {
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());
105 private static class SingleThreadedExecutor extends ThreadPoolExecutor {
106 public SingleThreadedExecutor() {
107 super(1, 1, 1, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
112 public Account[] getAccounts() {
113 return getAccountsByType(null);
117 public Account[] getAccountsByType(@Nullable String type) {
118 if (!AccountManagerHelper.GOOGLE_ACCOUNT_TYPE.equals(type)) {
119 throw new IllegalArgumentException("Invalid account type: " + type);
121 if (mAccounts == null) {
122 return new Account[0];
124 Account[] accounts = new Account[mAccounts.size()];
126 for (AccountHolder ah : mAccounts) {
127 accounts[i++] = ah.getAccount();
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);
140 public boolean addAccountHolderExplicitly(AccountHolder accountHolder) {
141 boolean result = mAccounts.add(accountHolder);
142 postAsyncAccountChangedEvent();
146 public boolean removeAccountHolderExplicitly(AccountHolder accountHolder) {
147 boolean result = mAccounts.remove(accountHolder);
148 postAsyncAccountChangedEvent();
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>() {
160 public Boolean call() throws Exception {
161 // Removal always successful.
168 public String getPassword(Account account) {
169 return getAccountHolder(account).getPassword();
173 public void setPassword(Account account, String password) {
174 mAccounts.add(getAccountHolder(account).withPassword(password));
178 public void clearPassword(Account account) {
179 setPassword(account, null);
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");
189 final AccountHolder accountHolder = getAccountHolder(account);
190 final boolean correctPassword = password.equals(accountHolder.getPassword());
191 return runTask(mExecutor, new AccountManagerTask<Bundle>(handler, callback,
192 new Callable<Bundle>() {
194 public Bundle call() throws Exception {
195 Bundle result = new Bundle();
196 result.putString(AccountManager.KEY_ACCOUNT_NAME,
197 accountHolder.getAccount().name);
199 AccountManager.KEY_ACCOUNT_TYPE,
200 AccountManagerHelper.GOOGLE_ACCOUNT_TYPE);
201 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, correctPassword);
208 public String blockingGetAuthToken(Account account, String authTokenType,
209 boolean notifyAuthFailure)
210 throws OperationCanceledException, IOException, AuthenticatorException {
211 AccountHolder accountHolder = getAccountHolder(account);
212 if (accountHolder.hasBeenAccepted(authTokenType)) {
213 // If account has already been accepted we can just return the auth token.
214 return internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
216 AccountAuthTokenPreparation prepared = getPreparedPermission(account, authTokenType);
217 Intent intent = newGrantCredentialsPermissionIntent(false, account, authTokenType);
218 waitForActivity(mContext, intent);
219 applyPreparedPermission(prepared);
220 return internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
224 public AccountManagerFuture<Bundle> getAuthToken(Account account, String authTokenType,
225 Bundle options, Activity activity, AccountManagerCallback<Bundle> callback,
227 return getAuthTokenFuture(account, authTokenType, activity, callback, handler);
231 public AccountManagerFuture<Bundle> getAuthToken(Account account, String authTokenType,
232 boolean notifyAuthFailure, AccountManagerCallback<Bundle> callback, Handler handler) {
233 return getAuthTokenFuture(account, authTokenType, null, callback, handler);
236 private AccountManagerFuture<Bundle> getAuthTokenFuture(Account account, String authTokenType,
237 Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) {
238 final AccountHolder ah = getAccountHolder(account);
239 if (ah.hasBeenAccepted(authTokenType)) {
240 final String authToken = internalGenerateAndStoreAuthToken(ah, authTokenType);
241 return runTask(mExecutor,
242 new AccountManagerAuthTokenTask(activity, handler, callback,
243 account, authTokenType,
244 new Callable<Bundle>() {
246 public Bundle call() throws Exception {
247 return getAuthTokenBundle(ah.getAccount(), authToken);
251 Log.d(TAG, "getAuthTokenFuture: Account " + ah.getAccount()
252 + " is asking for permission for " + authTokenType);
253 final Intent intent = newGrantCredentialsPermissionIntent(
254 activity != null, account, authTokenType);
255 return runTask(mExecutor,
256 new AccountManagerAuthTokenTask(activity, handler, callback,
257 account, authTokenType,
258 new Callable<Bundle>() {
260 public Bundle call() throws Exception {
261 Bundle result = new Bundle();
262 result.putParcelable(AccountManager.KEY_INTENT, intent);
269 private static Bundle getAuthTokenBundle(Account account, String authToken) {
270 Bundle result = new Bundle();
271 result.putString(AccountManager.KEY_AUTHTOKEN, authToken);
272 result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
273 result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
277 private String internalGenerateAndStoreAuthToken(AccountHolder ah, String authTokenType) {
278 synchronized (mAccounts) {
279 // Some tests register auth tokens with value null, and those should be preserved.
280 if (!ah.hasAuthTokenRegistered(authTokenType)
281 && ah.getAuthToken(authTokenType) == null) {
282 // No authtoken registered. Need to create one.
283 String authToken = UUID.randomUUID().toString();
284 Log.d(TAG, "Created new auth token for " + ah.getAccount()
285 + ": autTokenType = " + authTokenType + ", authToken = " + authToken);
286 ah = ah.withAuthToken(authTokenType, authToken);
290 return ah.getAuthToken(authTokenType);
294 public String peekAuthToken(Account account, String authTokenType) {
295 return getAccountHolder(account).getAuthToken(authTokenType);
299 public void invalidateAuthToken(String accountType, String authToken) {
300 if (!AccountManagerHelper.GOOGLE_ACCOUNT_TYPE.equals(accountType)) {
301 throw new IllegalArgumentException("Invalid account type: " + accountType);
303 if (authToken == null) {
304 throw new IllegalArgumentException("AuthToken can not be null");
306 for (AccountHolder ah : mAccounts) {
307 if (ah.removeAuthToken(authToken)) {
314 public AuthenticatorDescription[] getAuthenticatorTypes() {
315 AuthenticatorDescription googleAuthenticator = new AuthenticatorDescription(
316 AccountManagerHelper.GOOGLE_ACCOUNT_TYPE, "p1", 0, 0, 0, 0);
318 return new AuthenticatorDescription[] { googleAuthenticator };
321 public void prepareAllowAppPermission(Account account, String authTokenType) {
322 addPreparedAppPermission(new AccountAuthTokenPreparation(account, authTokenType, true));
325 public void prepareDenyAppPermission(Account account, String authTokenType) {
326 addPreparedAppPermission(new AccountAuthTokenPreparation(account, authTokenType, false));
329 private void addPreparedAppPermission(AccountAuthTokenPreparation accountAuthTokenPreparation) {
330 Log.d(TAG, "Adding " + accountAuthTokenPreparation);
331 mAccountPermissionPreparations.add(accountAuthTokenPreparation);
334 private AccountAuthTokenPreparation getPreparedPermission(Account account,
335 String authTokenType) {
336 for (AccountAuthTokenPreparation accountPrep : mAccountPermissionPreparations) {
337 if (accountPrep.getAccount().equals(account)
338 && accountPrep.getAuthTokenType().equals(authTokenType)) {
345 private void applyPreparedPermission(AccountAuthTokenPreparation prep) {
347 Log.d(TAG, "Applying " + prep);
348 mAccountPermissionPreparations.remove(prep);
349 mAccounts.add(getAccountHolder(prep.getAccount()).withHasBeenAccepted(
350 prep.getAuthTokenType(), prep.isAllowed()));
354 private Intent newGrantCredentialsPermissionIntent(boolean hasActivity, Account account,
355 String authTokenType) {
356 ComponentName component = new ComponentName(mTestContext,
357 MockGrantCredentialsPermissionActivity.class.getCanonicalName());
359 // Make sure we can start the activity.
360 ActivityInfo ai = null;
362 ai = mContext.getPackageManager().getActivityInfo(component, 0);
363 } catch (PackageManager.NameNotFoundException e) {
364 throw new IllegalStateException(
365 "Unable to find " + component.getClassName());
367 if (ai.applicationInfo != mContext.getApplicationInfo() && !ai.exported) {
368 throw new IllegalStateException(
369 "Unable to start " + ai.name + ". "
370 + "The accounts you added to MockAccountManager may not be "
371 + "configured correctly.");
374 Intent intent = new Intent();
375 intent.setComponent(component);
376 intent.putExtra(MockGrantCredentialsPermissionActivity.ACCOUNT, account);
377 intent.putExtra(MockGrantCredentialsPermissionActivity.AUTH_TOKEN_TYPE, authTokenType);
379 // No activity provided, so we help the caller by adding the new task flag
380 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
385 private AccountHolder getAccountHolder(Account account) {
386 if (account == null) {
387 throw new IllegalArgumentException("Account can not be null");
389 for (AccountHolder accountHolder : mAccounts) {
390 if (account.equals(accountHolder.getAccount())) {
391 return accountHolder;
394 throw new IllegalArgumentException("Can not find AccountHolder for account " + account);
397 private static <T> AccountManagerFuture<T> runTask(Executor executorService,
398 AccountManagerTask<T> accountManagerBundleTask) {
399 executorService.execute(accountManagerBundleTask);
400 return accountManagerBundleTask;
403 private class AccountManagerTask<T> extends FutureTask<T> implements AccountManagerFuture<T> {
405 protected final Handler mHandler;
407 protected final AccountManagerCallback<T> mCallback;
409 protected final Callable<T> mCallable;
411 public AccountManagerTask(Handler handler,
412 AccountManagerCallback<T> callback, Callable<T> callable) {
413 super(new Callable<T>() {
415 public T call() throws Exception {
416 throw new IllegalStateException("this should never be called, "
417 + "but call must be overridden.");
421 mCallback = callback;
422 mCallable = callable;
425 private T internalGetResult(long timeout, TimeUnit unit)
426 throws OperationCanceledException, IOException, AuthenticatorException {
431 return get(timeout, unit);
433 } catch (CancellationException e) {
434 throw new OperationCanceledException();
435 } catch (TimeoutException e) {
436 // Fall through and cancel.
437 } catch (InterruptedException e) {
438 // Fall through and cancel.
439 } catch (ExecutionException e) {
440 final Throwable cause = e.getCause();
441 if (cause instanceof IOException) {
442 throw (IOException) cause;
443 } else if (cause instanceof UnsupportedOperationException) {
444 throw new AuthenticatorException(cause);
445 } else if (cause instanceof AuthenticatorException) {
446 throw (AuthenticatorException) cause;
447 } else if (cause instanceof RuntimeException) {
448 throw (RuntimeException) cause;
449 } else if (cause instanceof Error) {
452 throw new IllegalStateException(cause);
455 cancel(true /* Interrupt if running. */);
457 throw new OperationCanceledException();
462 throws OperationCanceledException, IOException, AuthenticatorException {
463 return internalGetResult(-1, null);
467 public T getResult(long timeout, TimeUnit unit)
468 throws OperationCanceledException, IOException, AuthenticatorException {
469 return internalGetResult(timeout, unit);
475 set(mCallable.call());
476 } catch (Exception e) {
482 protected void done() {
483 if (mCallback != null) {
484 postToHandler(getHandler(), mCallback, this);
488 protected Handler getHandler() {
489 return mHandler == null ? mMainHandler : mHandler;
494 private static <T> void postToHandler(Handler handler, final AccountManagerCallback<T> callback,
495 final AccountManagerFuture<T> future) {
496 handler.post(new Runnable() {
499 callback.run(future);
504 private class AccountManagerAuthTokenTask extends AccountManagerTask<Bundle> {
506 private final Activity mActivity;
508 private final AccountAuthTokenPreparation mAccountAuthTokenPreparation;
510 private final Account mAccount;
512 private final String mAuthTokenType;
514 public AccountManagerAuthTokenTask(Activity activity, Handler handler,
515 AccountManagerCallback<Bundle> callback,
516 Account account, String authTokenType,
517 Callable<Bundle> callable) {
518 super(handler, callback, callable);
519 mActivity = activity;
520 mAccountAuthTokenPreparation = getPreparedPermission(account, authTokenType);
522 mAuthTokenType = authTokenType;
528 Bundle bundle = mCallable.call();
529 Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
530 if (intent != null) {
531 // Start the intent activity and wait for it to finish.
532 if (mActivity != null) {
533 waitForActivity(mActivity, intent);
535 waitForActivity(mContext, intent);
537 if (mAccountAuthTokenPreparation == null) {
538 throw new IllegalStateException("No account preparation ready for "
539 + mAccount + ", authTokenType = " + mAuthTokenType
540 + ". Add a call to either prepareGrantAppPermission(...) or "
541 + "prepareRevokeAppPermission(...) in your test before asking for "
544 // We have shown the Allow/Deny activity, and it has gone away. We can now
545 // apply the pre-stored permission.
546 applyPreparedPermission(mAccountAuthTokenPreparation);
547 generateResult(getAccountHolder(mAccount), mAuthTokenType);
552 } catch (Exception e) {
557 private void generateResult(AccountHolder accountHolder, String authTokenType)
558 throws OperationCanceledException {
559 if (accountHolder.hasBeenAccepted(authTokenType)) {
560 String authToken = internalGenerateAndStoreAuthToken(accountHolder, authTokenType);
561 // Return a valid auth token.
562 set(getAuthTokenBundle(accountHolder.getAccount(), authToken));
564 // Throw same exception as when user clicks "Deny".
565 throw new OperationCanceledException("User denied request");
571 * This method starts {@link MockGrantCredentialsPermissionActivity} and waits for it
572 * to be started before it returns.
574 * @param context the context to start the intent in
575 * @param intent the intent to use to start MockGrantCredentialsPermissionActivity
577 private void waitForActivity(Context context, Intent intent) {
578 final Object mutex = new Object();
579 BroadcastReceiver receiver = new BroadcastReceiver() {
581 public void onReceive(Context context, Intent intent) {
582 synchronized (mutex) {
587 if (!MockGrantCredentialsPermissionActivity.class.getCanonicalName()
588 .equals(intent.getComponent().getClassName())) {
589 throw new IllegalArgumentException("Can only wait for "
590 + "MockGrantCredentialsPermissionActivity");
592 mContext.registerReceiver(receiver, new IntentFilter(MUTEX_WAIT_ACTION));
593 context.startActivity(intent);
595 Log.d(TAG, "Waiting for broadcast of " + MUTEX_WAIT_ACTION);
596 synchronized (mutex) {
597 mutex.wait(WAIT_TIME_FOR_GRANT_BROADCAST_MS);
599 } catch (InterruptedException e) {
600 throw new IllegalStateException("Got unexpected InterruptedException");
602 Log.d(TAG, "Got broadcast of " + MUTEX_WAIT_ACTION);
603 mContext.unregisterReceiver(receiver);
606 private void postAsyncAccountChangedEvent() {
607 // Mimic that this does not happen on the main thread.
608 new AsyncTask<Void, Void, Void>() {
610 protected Void doInBackground(Void... params) {
611 mContext.sendBroadcast(new Intent(AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION));
618 * Internal class for storage of prepared account auth token permissions.
620 * This is used internally by {@link MockAccountManager} to mock the same behavior as clicking
621 * Allow/Deny in the Android {@link GrantCredentialsPermissionActivity}.
623 private static class AccountAuthTokenPreparation {
625 private final Account mAccount;
627 private final String mAuthTokenType;
629 private final boolean mAllowed;
631 private AccountAuthTokenPreparation(Account account, String authTokenType,
634 mAuthTokenType = authTokenType;
638 public Account getAccount() {
642 public String getAuthTokenType() {
643 return mAuthTokenType;
646 public boolean isAllowed() {
651 public String toString() {
652 return "AccountAuthTokenPreparation{"
653 + "mAccount=" + mAccount
654 + ", mAuthTokenType='" + mAuthTokenType + '\''
655 + ", mAllowed=" + mAllowed