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