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