1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR2; 4 import static android.os.Build.VERSION_CODES.LOLLIPOP; 5 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; 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.IAccountManager; 14 import android.accounts.OnAccountsUpdateListener; 15 import android.accounts.OperationCanceledException; 16 import android.app.Activity; 17 import android.content.Context; 18 import android.content.Intent; 19 import android.os.Bundle; 20 import android.os.Handler; 21 import java.io.IOException; 22 import java.util.ArrayList; 23 import java.util.Arrays; 24 import java.util.Collections; 25 import java.util.HashMap; 26 import java.util.HashSet; 27 import java.util.Iterator; 28 import java.util.LinkedHashMap; 29 import java.util.List; 30 import java.util.Map; 31 import java.util.Map.Entry; 32 import java.util.Set; 33 import java.util.concurrent.TimeUnit; 34 import org.robolectric.annotation.Implementation; 35 import org.robolectric.annotation.Implements; 36 import org.robolectric.util.Scheduler.IdleState; 37 38 @Implements(AccountManager.class) 39 public class ShadowAccountManager { 40 41 private List<Account> accounts = new ArrayList<>(); 42 private Map<Account, Map<String, String>> authTokens = new HashMap<>(); 43 private Map<String, AuthenticatorDescription> authenticators = new LinkedHashMap<>(); 44 private List<OnAccountsUpdateListener> listeners = new ArrayList<>(); 45 private Map<Account, Map<String, String>> userData = new HashMap<>(); 46 private Map<Account, String> passwords = new HashMap<>(); 47 private Map<Account, Set<String>> accountFeatures = new HashMap<>(); 48 private Map<Account, Set<String>> packageVisibileAccounts = new HashMap<>(); 49 50 private List<Bundle> addAccountOptionsList = new ArrayList<>(); 51 private Handler mainHandler; 52 private RoboAccountManagerFuture pendingAddFuture; 53 private boolean authenticationErrorOnNextResponse = false; 54 55 @Implementation __constructor__(Context context, IAccountManager service)56 protected void __constructor__(Context context, IAccountManager service) { 57 mainHandler = new Handler(context.getMainLooper()); 58 } 59 60 /** 61 * @deprecated This method will be removed in Robolectric 3.4 Use {@link 62 * AccountManager#get(Context)} instead. 63 */ 64 @Deprecated 65 @Implementation get(Context context)66 protected static AccountManager get(Context context) { 67 return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); 68 } 69 70 @Implementation getAccounts()71 protected Account[] getAccounts() { 72 return accounts.toArray(new Account[accounts.size()]); 73 } 74 75 @Implementation getAccountsByType(String type)76 protected Account[] getAccountsByType(String type) { 77 if (type == null) { 78 return getAccounts(); 79 } 80 List<Account> accountsByType = new ArrayList<>(); 81 82 for (Account a : accounts) { 83 if (type.equals(a.type)) { 84 accountsByType.add(a); 85 } 86 } 87 88 return accountsByType.toArray(new Account[accountsByType.size()]); 89 } 90 91 @Implementation setAuthToken(Account account, String tokenType, String authToken)92 protected synchronized void setAuthToken(Account account, String tokenType, String authToken) { 93 if(accounts.contains(account)) { 94 Map<String, String> tokenMap = authTokens.get(account); 95 if(tokenMap == null) { 96 tokenMap = new HashMap<>(); 97 authTokens.put(account, tokenMap); 98 } 99 tokenMap.put(tokenType, authToken); 100 } 101 } 102 103 @Implementation peekAuthToken(Account account, String tokenType)104 protected String peekAuthToken(Account account, String tokenType) { 105 Map<String, String> tokenMap = authTokens.get(account); 106 if(tokenMap != null) { 107 return tokenMap.get(tokenType); 108 } 109 return null; 110 } 111 112 @SuppressWarnings("InconsistentCapitalization") 113 @Implementation addAccountExplicitly(Account account, String password, Bundle userdata)114 protected boolean addAccountExplicitly(Account account, String password, Bundle userdata) { 115 if (account == null) { 116 throw new IllegalArgumentException("account is null"); 117 } 118 for (Account a: getAccountsByType(account.type)) { 119 if (a.name.equals(account.name)) { 120 return false; 121 } 122 } 123 124 if (!accounts.add(account)) { 125 return false; 126 } 127 128 setPassword(account, password); 129 130 if(userdata != null) { 131 for (String key : userdata.keySet()) { 132 setUserData(account, key, userdata.get(key).toString()); 133 } 134 } 135 136 notifyListeners(); 137 138 return true; 139 } 140 141 @Implementation blockingGetAuthToken( Account account, String authTokenType, boolean notifyAuthFailure)142 protected String blockingGetAuthToken( 143 Account account, String authTokenType, boolean notifyAuthFailure) { 144 if (account == null) { 145 throw new IllegalArgumentException("account is null"); 146 } 147 if (authTokenType == null) { 148 throw new IllegalArgumentException("authTokenType is null"); 149 } 150 151 Map<String, String> tokensForAccount = authTokens.get(account); 152 if (tokensForAccount == null) { 153 return null; 154 } 155 return tokensForAccount.get(authTokenType); 156 } 157 158 /** 159 * The remove operation is posted to the given {@code handler}, and will be executed according to 160 * the {@link IdleState} of the corresponding {@link org.robolectric.util.Scheduler}. 161 */ 162 @Implementation removeAccount( final Account account, AccountManagerCallback<Boolean> callback, Handler handler)163 protected AccountManagerFuture<Boolean> removeAccount( 164 final Account account, AccountManagerCallback<Boolean> callback, Handler handler) { 165 if (account == null) { 166 throw new IllegalArgumentException("account is null"); 167 } 168 169 return start( 170 new BaseRoboAccountManagerFuture<Boolean>(callback, handler) { 171 @Override 172 public Boolean doWork() 173 throws OperationCanceledException, IOException, AuthenticatorException { 174 return removeAccountExplicitly(account); 175 } 176 }); 177 } 178 179 @Implementation(minSdk = LOLLIPOP_MR1) 180 protected boolean removeAccountExplicitly(Account account) { 181 passwords.remove(account); 182 userData.remove(account); 183 if (accounts.remove(account)) { 184 notifyListeners(); 185 return true; 186 } 187 return false; 188 } 189 190 /** 191 * Removes all accounts that have been added. 192 */ 193 public void removeAllAccounts() { 194 passwords.clear(); 195 userData.clear(); 196 accounts.clear(); 197 } 198 199 @Implementation 200 protected AuthenticatorDescription[] getAuthenticatorTypes() { 201 return authenticators.values().toArray(new AuthenticatorDescription[authenticators.size()]); 202 } 203 204 @Implementation 205 protected void addOnAccountsUpdatedListener( 206 final OnAccountsUpdateListener listener, Handler handler, boolean updateImmediately) { 207 208 if (listeners.contains(listener)) { 209 return; 210 } 211 212 listeners.add(listener); 213 214 if (updateImmediately) { 215 listener.onAccountsUpdated(getAccounts()); 216 } 217 } 218 219 @Implementation 220 protected void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) { 221 listeners.remove(listener); 222 } 223 224 @Implementation 225 protected String getUserData(Account account, String key) { 226 if (account == null) { 227 throw new IllegalArgumentException("account is null"); 228 } 229 230 if (!userData.containsKey(account)) { 231 return null; 232 } 233 234 Map<String, String> userDataMap = userData.get(account); 235 if (userDataMap.containsKey(key)) { 236 return userDataMap.get(key); 237 } 238 239 return null; 240 } 241 242 @Implementation 243 protected void setUserData(Account account, String key, String value) { 244 if (account == null) { 245 throw new IllegalArgumentException("account is null"); 246 } 247 248 if (!userData.containsKey(account)) { 249 userData.put(account, new HashMap<String, String>()); 250 } 251 252 Map<String, String> userDataMap = userData.get(account); 253 254 if (value == null) { 255 userDataMap.remove(key); 256 } else { 257 userDataMap.put(key, value); 258 } 259 } 260 261 @Implementation 262 protected void setPassword(Account account, String password) { 263 if (account == null) { 264 throw new IllegalArgumentException("account is null"); 265 } 266 267 if (password == null) { 268 passwords.remove(account); 269 } else { 270 passwords.put(account, password); 271 } 272 } 273 274 @Implementation 275 protected String getPassword(Account account) { 276 if (account == null) { 277 throw new IllegalArgumentException("account is null"); 278 } 279 280 if (passwords.containsKey(account)) { 281 return passwords.get(account); 282 } else { 283 return null; 284 } 285 } 286 287 @Implementation 288 protected void invalidateAuthToken(final String accountType, final String authToken) { 289 Account[] accountsByType = getAccountsByType(accountType); 290 for (Account account : accountsByType) { 291 Map<String, String> tokenMap = authTokens.get(account); 292 if (tokenMap != null) { 293 Iterator<Entry<String, String>> it = tokenMap.entrySet().iterator(); 294 while (it.hasNext()) { 295 Map.Entry<String, String> map = it.next(); 296 if (map.getValue().equals(authToken)) { 297 it.remove(); 298 } 299 } 300 authTokens.put(account, tokenMap); 301 } 302 } 303 } 304 305 private void notifyListeners() { 306 Account[] accounts = getAccounts(); 307 Iterator<OnAccountsUpdateListener> iter = listeners.iterator(); 308 OnAccountsUpdateListener listener; 309 while (iter.hasNext()) { 310 listener = iter.next(); 311 listener.onAccountsUpdated(accounts); 312 } 313 } 314 315 /** 316 * @param account User account. 317 */ 318 public void addAccount(Account account) { 319 accounts.add(account); 320 if (pendingAddFuture != null) { 321 pendingAddFuture.resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); 322 start(pendingAddFuture); 323 pendingAddFuture = null; 324 } 325 notifyListeners(); 326 } 327 328 /** 329 * Adds an account to the AccountManager but when {@link AccountManager#getAccountsByTypeForPackage(String, String)} 330 * is called will be included if is in one of the #visibileToPackages 331 * 332 * @param account User account. 333 */ 334 public void addAccount(Account account, String... visibileToPackages) { 335 addAccount(account); 336 HashSet<String> value = new HashSet<>(); 337 Collections.addAll(value, visibileToPackages); 338 packageVisibileAccounts.put(account, value); 339 } 340 341 /** 342 * Consumes and returns the next {@code addAccountOptions} passed to {@link #addAccount}. 343 * 344 * @return the next {@code addAccountOptions} 345 */ 346 public Bundle getNextAddAccountOptions() { 347 if (addAccountOptionsList.isEmpty()) { 348 return null; 349 } else { 350 return addAccountOptionsList.remove(0); 351 } 352 } 353 354 /** 355 * Returns the next {@code addAccountOptions} passed to {@link #addAccount} without consuming it. 356 * 357 * @return the next {@code addAccountOptions} 358 */ 359 public Bundle peekNextAddAccountOptions() { 360 if (addAccountOptionsList.isEmpty()) { 361 return null; 362 } else { 363 return addAccountOptionsList.get(0); 364 } 365 } 366 367 private class RoboAccountManagerFuture extends BaseRoboAccountManagerFuture<Bundle> { 368 private final String accountType; 369 private final Activity activity; 370 private final Bundle resultBundle; 371 372 RoboAccountManagerFuture(AccountManagerCallback<Bundle> callback, Handler handler, String accountType, Activity activity) { 373 super(callback, handler); 374 375 this.accountType = accountType; 376 this.activity = activity; 377 this.resultBundle = new Bundle(); 378 } 379 380 @Override 381 public Bundle doWork() throws OperationCanceledException, IOException, AuthenticatorException { 382 if (!authenticators.containsKey(accountType)) { 383 throw new AuthenticatorException("No authenticator specified for " + accountType); 384 } 385 386 resultBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType); 387 388 if (activity == null) { 389 Intent resultIntent = new Intent(); 390 resultBundle.putParcelable(AccountManager.KEY_INTENT, resultIntent); 391 } else if (callback == null) { 392 resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, "some_user@gmail.com"); 393 } 394 395 return resultBundle; 396 } 397 } 398 399 @Implementation 400 protected AccountManagerFuture<Bundle> addAccount( 401 final String accountType, 402 String authTokenType, 403 String[] requiredFeatures, 404 Bundle addAccountOptions, 405 Activity activity, 406 AccountManagerCallback<Bundle> callback, 407 Handler handler) { 408 addAccountOptionsList.add(addAccountOptions); 409 pendingAddFuture = new RoboAccountManagerFuture(callback, handler, accountType, activity); 410 return pendingAddFuture; 411 } 412 413 public void setFeatures(Account account, String[] accountFeatures) { 414 HashSet<String> featureSet = new HashSet<>(); 415 featureSet.addAll(Arrays.asList(accountFeatures)); 416 this.accountFeatures.put(account, featureSet); 417 } 418 419 /** 420 * @param authenticator System authenticator. 421 */ 422 public void addAuthenticator(AuthenticatorDescription authenticator) { 423 authenticators.put(authenticator.type, authenticator); 424 } 425 426 public void addAuthenticator(String type) { 427 addAuthenticator(AuthenticatorDescription.newKey(type)); 428 } 429 430 private Map<Account, String> previousNames = new HashMap<Account, String>(); 431 432 /** 433 * Sets the previous name for an account, which will be returned by {@link AccountManager#getPreviousName(Account)}. 434 * 435 * @param account User account. 436 * @param previousName Previous account name. 437 */ 438 public void setPreviousAccountName(Account account, String previousName) { 439 previousNames.put(account, previousName); 440 } 441 442 /** @see #setPreviousAccountName(Account, String) */ 443 @Implementation(minSdk = LOLLIPOP) 444 protected String getPreviousName(Account account) { 445 return previousNames.get(account); 446 } 447 448 @Implementation 449 protected AccountManagerFuture<Bundle> getAuthToken( 450 final Account account, 451 final String authTokenType, 452 final Bundle options, 453 final Activity activity, 454 final AccountManagerCallback<Bundle> callback, 455 Handler handler) { 456 457 return start( 458 new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 459 @Override 460 public Bundle doWork() 461 throws OperationCanceledException, IOException, AuthenticatorException { 462 return getAuthToken(account, authTokenType); 463 } 464 }); 465 } 466 467 @Implementation 468 protected AccountManagerFuture<Bundle> getAuthToken( 469 final Account account, 470 final String authTokenType, 471 final Bundle options, 472 final boolean notifyAuthFailure, 473 final AccountManagerCallback<Bundle> callback, 474 Handler handler) { 475 476 return start(new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 477 @Override 478 public Bundle doWork() 479 throws OperationCanceledException, IOException, AuthenticatorException { 480 return getAuthToken(account, authTokenType); 481 } 482 }); 483 } 484 485 private Bundle getAuthToken(Account account, String authTokenType) 486 throws OperationCanceledException, IOException, AuthenticatorException { 487 Bundle result = new Bundle(); 488 489 String authToken = blockingGetAuthToken(account, authTokenType, false); 490 result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); 491 result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); 492 result.putString(AccountManager.KEY_AUTHTOKEN, authToken); 493 494 if (authToken != null) { 495 return result; 496 } 497 498 if (!authenticators.containsKey(account.type)) { 499 throw new AuthenticatorException("No authenticator specified for " + account.type); 500 } 501 502 Intent resultIntent = new Intent(); 503 result.putParcelable(AccountManager.KEY_INTENT, resultIntent); 504 505 return result; 506 } 507 508 @Implementation 509 protected AccountManagerFuture<Boolean> hasFeatures( 510 final Account account, 511 final String[] features, 512 AccountManagerCallback<Boolean> callback, 513 Handler handler) { 514 return start(new BaseRoboAccountManagerFuture<Boolean>(callback, handler) { 515 @Override 516 public Boolean doWork() throws OperationCanceledException, IOException, AuthenticatorException { 517 Set<String> availableFeatures = accountFeatures.get(account); 518 for (String feature : features) { 519 if (!availableFeatures.contains(feature)) { 520 return false; 521 } 522 } 523 return true; 524 } 525 }); 526 } 527 528 @Implementation 529 protected AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures( 530 final String type, 531 final String[] features, 532 AccountManagerCallback<Account[]> callback, 533 Handler handler) { 534 return start( 535 new BaseRoboAccountManagerFuture<Account[]>(callback, handler) { 536 @Override 537 public Account[] doWork() 538 throws OperationCanceledException, IOException, AuthenticatorException { 539 540 if (authenticationErrorOnNextResponse) { 541 setAuthenticationErrorOnNextResponse(false); 542 throw new AuthenticatorException(); 543 } 544 545 List<Account> result = new ArrayList<>(); 546 547 Account[] accountsByType = getAccountsByType(type); 548 for (Account account : accountsByType) { 549 Set<String> featureSet = accountFeatures.get(account); 550 if (features == null || featureSet.containsAll(Arrays.asList(features))) { 551 result.add(account); 552 } 553 } 554 return result.toArray(new Account[result.size()]); 555 } 556 }); 557 } 558 559 private <T extends BaseRoboAccountManagerFuture> T start(T future) { 560 future.start(); 561 return future; 562 } 563 564 @Implementation(minSdk = JELLY_BEAN_MR2) 565 protected Account[] getAccountsByTypeForPackage(String type, String packageName) { 566 List<Account> result = new ArrayList<>(); 567 568 Account[] accountsByType = getAccountsByType(type); 569 for (Account account : accountsByType) { 570 if (packageVisibileAccounts.containsKey(account) && packageVisibileAccounts.get(account).contains(packageName)) { 571 result.add(account); 572 } 573 } 574 575 return result.toArray(new Account[result.size()]); 576 } 577 578 /** 579 * Sets authenticator exception, which will be thrown by {@link #getAccountsByTypeAndFeatures}. 580 * 581 * @param authenticationErrorOnNextResponse to set flag that exception will be thrown on next 582 * response. 583 */ 584 public void setAuthenticationErrorOnNextResponse(boolean authenticationErrorOnNextResponse) { 585 this.authenticationErrorOnNextResponse = authenticationErrorOnNextResponse; 586 } 587 588 private abstract class BaseRoboAccountManagerFuture<T> implements AccountManagerFuture<T> { 589 protected final AccountManagerCallback<T> callback; 590 private final Handler handler; 591 protected T result; 592 private Exception exception; 593 private boolean started = false; 594 595 BaseRoboAccountManagerFuture(AccountManagerCallback<T> callback, Handler handler) { 596 this.callback = callback; 597 this.handler = handler == null ? mainHandler : handler; 598 } 599 600 void start() { 601 if (started) return; 602 started = true; 603 604 try { 605 result = doWork(); 606 } catch (OperationCanceledException | IOException | AuthenticatorException e) { 607 exception = e; 608 } 609 610 if (callback != null) { 611 handler.post( 612 new Runnable() { 613 @Override 614 public void run() { 615 callback.run(BaseRoboAccountManagerFuture.this); 616 } 617 }); 618 } 619 } 620 621 @Override 622 public boolean cancel(boolean mayInterruptIfRunning) { 623 return false; 624 } 625 626 @Override 627 public boolean isCancelled() { 628 return false; 629 } 630 631 @Override 632 public boolean isDone() { 633 return result != null || exception != null || isCancelled(); 634 } 635 636 @Override 637 public T getResult() throws OperationCanceledException, IOException, AuthenticatorException { 638 start(); 639 640 if (exception instanceof OperationCanceledException) { 641 throw new OperationCanceledException(exception); 642 } else if (exception instanceof IOException) { 643 throw new IOException(exception); 644 } else if (exception instanceof AuthenticatorException) { 645 throw new AuthenticatorException(exception); 646 } 647 return result; 648 } 649 650 @Override 651 public T getResult(long timeout, TimeUnit unit) throws OperationCanceledException, IOException, AuthenticatorException { 652 return getResult(); 653 } 654 655 public abstract T doWork() throws OperationCanceledException, IOException, AuthenticatorException; 656 } 657 } 658