1 package org.robolectric.shadows; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1; 4 import static android.os.Build.VERSION_CODES.O; 5 6 import android.accounts.Account; 7 import android.accounts.AccountManager; 8 import android.accounts.AccountManagerCallback; 9 import android.accounts.AccountManagerFuture; 10 import android.accounts.AuthenticatorDescription; 11 import android.accounts.AuthenticatorException; 12 import android.accounts.IAccountManager; 13 import android.accounts.OnAccountsUpdateListener; 14 import android.accounts.OperationCanceledException; 15 import android.app.Activity; 16 import android.content.Context; 17 import android.content.Intent; 18 import android.os.Bundle; 19 import android.os.Handler; 20 import java.io.IOException; 21 import java.util.ArrayList; 22 import java.util.Arrays; 23 import java.util.Collections; 24 import java.util.HashMap; 25 import java.util.HashSet; 26 import java.util.Iterator; 27 import java.util.LinkedHashMap; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Map.Entry; 31 import java.util.Set; 32 import java.util.concurrent.TimeUnit; 33 import javax.annotation.Nullable; 34 import org.robolectric.annotation.Implementation; 35 import org.robolectric.annotation.Implements; 36 import org.robolectric.annotation.Resetter; 37 import org.robolectric.util.Scheduler.IdleState; 38 39 @Implements(AccountManager.class) 40 public class ShadowAccountManager { 41 42 private List<Account> accounts = new ArrayList<>(); 43 private Map<Account, Map<String, String>> authTokens = new HashMap<>(); 44 private Map<String, AuthenticatorDescription> authenticators = new LinkedHashMap<>(); 45 46 /** 47 * Maps listeners to a set of account types. If null, the listener should be notified for changes 48 * to accounts of any type. Otherwise, the listener is only notified of changes to accounts of the 49 * given type. 50 */ 51 private Map<OnAccountsUpdateListener, Set<String>> listeners = new LinkedHashMap<>(); 52 53 private Map<Account, Map<String, String>> userData = new HashMap<>(); 54 private Map<Account, String> passwords = new HashMap<>(); 55 private Map<Account, Set<String>> accountFeatures = new HashMap<>(); 56 private Map<Account, Set<String>> packageVisibleAccounts = new HashMap<>(); 57 58 private List<Bundle> addAccountOptionsList = new ArrayList<>(); 59 private static Handler mainHandler; 60 private static RoboAccountManagerFuture pendingAddFuture; 61 private static boolean authenticationErrorOnNextResponse = false; 62 private static Intent removeAccountIntent; 63 64 @Resetter reset()65 public static void reset() { 66 if (mainHandler != null) { 67 mainHandler.removeCallbacksAndMessages(null); 68 mainHandler = null; 69 } 70 71 if (pendingAddFuture != null) { 72 pendingAddFuture.cancel(true); 73 pendingAddFuture = null; 74 } 75 authenticationErrorOnNextResponse = false; 76 removeAccountIntent = null; 77 } 78 79 @Implementation __constructor__(Context context, IAccountManager service)80 protected void __constructor__(Context context, IAccountManager service) { 81 mainHandler = new Handler(context.getMainLooper()); 82 } 83 84 @Implementation get(Context context)85 protected static AccountManager get(Context context) { 86 return (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE); 87 } 88 89 @Implementation getAccounts()90 protected Account[] getAccounts() { 91 return accounts.toArray(new Account[accounts.size()]); 92 } 93 94 @Implementation getAccountsByType(String type)95 protected Account[] getAccountsByType(String type) { 96 if (type == null) { 97 return getAccounts(); 98 } 99 List<Account> accountsByType = new ArrayList<>(); 100 101 for (Account a : accounts) { 102 if (type.equals(a.type)) { 103 accountsByType.add(a); 104 } 105 } 106 107 return accountsByType.toArray(new Account[accountsByType.size()]); 108 } 109 110 @Implementation setAuthToken(Account account, String tokenType, String authToken)111 protected synchronized void setAuthToken(Account account, String tokenType, String authToken) { 112 if (accounts.contains(account)) { 113 Map<String, String> tokenMap = authTokens.get(account); 114 if (tokenMap == null) { 115 tokenMap = new HashMap<>(); 116 authTokens.put(account, tokenMap); 117 } 118 tokenMap.put(tokenType, authToken); 119 } 120 } 121 122 @Implementation peekAuthToken(Account account, String tokenType)123 protected String peekAuthToken(Account account, String tokenType) { 124 Map<String, String> tokenMap = authTokens.get(account); 125 if (tokenMap != null) { 126 return tokenMap.get(tokenType); 127 } 128 return null; 129 } 130 131 @SuppressWarnings("InconsistentCapitalization") 132 @Implementation addAccountExplicitly(Account account, String password, Bundle userdata)133 protected boolean addAccountExplicitly(Account account, String password, Bundle userdata) { 134 if (account == null) { 135 throw new IllegalArgumentException("account is null"); 136 } 137 for (Account a : getAccountsByType(account.type)) { 138 if (a.name.equals(account.name)) { 139 return false; 140 } 141 } 142 143 if (!accounts.add(account)) { 144 return false; 145 } 146 147 setPassword(account, password); 148 149 if (userdata != null) { 150 for (String key : userdata.keySet()) { 151 setUserData(account, key, userdata.get(key).toString()); 152 } 153 } 154 155 notifyListeners(account); 156 157 return true; 158 } 159 160 @Implementation blockingGetAuthToken( Account account, String authTokenType, boolean notifyAuthFailure)161 protected String blockingGetAuthToken( 162 Account account, String authTokenType, boolean notifyAuthFailure) { 163 if (account == null) { 164 throw new IllegalArgumentException("account is null"); 165 } 166 if (authTokenType == null) { 167 throw new IllegalArgumentException("authTokenType is null"); 168 } 169 170 Map<String, String> tokensForAccount = authTokens.get(account); 171 if (tokensForAccount == null) { 172 return null; 173 } 174 return tokensForAccount.get(authTokenType); 175 } 176 177 /** 178 * The remove operation is posted to the given {@code handler}, and will be executed according to 179 * the {@link IdleState} of the corresponding {@link org.robolectric.util.Scheduler}. 180 */ 181 @Implementation removeAccount( final Account account, AccountManagerCallback<Boolean> callback, Handler handler)182 protected AccountManagerFuture<Boolean> removeAccount( 183 final Account account, AccountManagerCallback<Boolean> callback, Handler handler) { 184 if (account == null) { 185 throw new IllegalArgumentException("account is null"); 186 } 187 188 return start( 189 new BaseRoboAccountManagerFuture<Boolean>(callback, handler) { 190 @Override 191 public Boolean doWork() { 192 return removeAccountExplicitly(account); 193 } 194 }); 195 } 196 197 /** 198 * Removes the account unless {@link #setRemoveAccountIntent} has been set. If set, the future 199 * Bundle will include the Intent and {@link AccountManager#KEY_BOOLEAN_RESULT} will be false. 200 */ 201 @Implementation(minSdk = LOLLIPOP_MR1) 202 protected AccountManagerFuture<Bundle> removeAccount( 203 Account account, 204 Activity activity, 205 AccountManagerCallback<Bundle> callback, 206 Handler handler) { 207 if (account == null) { 208 throw new IllegalArgumentException("account is null"); 209 } 210 return start( 211 new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 212 @Override 213 public Bundle doWork() { 214 Bundle result = new Bundle(); 215 if (removeAccountIntent == null) { 216 result.putBoolean( 217 AccountManager.KEY_BOOLEAN_RESULT, removeAccountExplicitly(account)); 218 } else { 219 result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); 220 result.putParcelable(AccountManager.KEY_INTENT, removeAccountIntent); 221 } 222 return result; 223 } 224 }); 225 } 226 227 @Implementation(minSdk = LOLLIPOP_MR1) 228 protected boolean removeAccountExplicitly(Account account) { 229 passwords.remove(account); 230 userData.remove(account); 231 if (accounts.remove(account)) { 232 notifyListeners(account); 233 return true; 234 } 235 return false; 236 } 237 238 /** Removes all accounts that have been added. */ 239 public void removeAllAccounts() { 240 passwords.clear(); 241 userData.clear(); 242 accounts.clear(); 243 } 244 245 @Implementation 246 protected AuthenticatorDescription[] getAuthenticatorTypes() { 247 return authenticators.values().toArray(new AuthenticatorDescription[authenticators.size()]); 248 } 249 250 @Implementation 251 protected void addOnAccountsUpdatedListener( 252 final OnAccountsUpdateListener listener, Handler handler, boolean updateImmediately) { 253 addOnAccountsUpdatedListener(listener, handler, updateImmediately, /* accountTypes= */ null); 254 } 255 256 /** 257 * Based on {@link AccountManager#addOnAccountsUpdatedListener(OnAccountsUpdateListener, Handler, 258 * boolean, String[])}. {@link Handler} is ignored. 259 */ 260 @Implementation(minSdk = O) 261 protected void addOnAccountsUpdatedListener( 262 @Nullable final OnAccountsUpdateListener listener, 263 @Nullable Handler handler, 264 boolean updateImmediately, 265 @Nullable String[] accountTypes) { 266 // TODO: Match real method behavior by throwing IllegalStateException. 267 if (listeners.containsKey(listener)) { 268 return; 269 } 270 271 Set<String> types = null; 272 if (accountTypes != null) { 273 types = new HashSet<>(Arrays.asList(accountTypes)); 274 } 275 listeners.put(listener, types); 276 277 if (updateImmediately) { 278 notifyListener(listener, types, getAccounts()); 279 } 280 } 281 282 @Implementation 283 protected void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) { 284 listeners.remove(listener); 285 } 286 287 @Implementation 288 protected String getUserData(Account account, String key) { 289 if (account == null) { 290 throw new IllegalArgumentException("account is null"); 291 } 292 293 if (!userData.containsKey(account)) { 294 return null; 295 } 296 297 Map<String, String> userDataMap = userData.get(account); 298 if (userDataMap.containsKey(key)) { 299 return userDataMap.get(key); 300 } 301 302 return null; 303 } 304 305 @Implementation 306 protected void setUserData(Account account, String key, String value) { 307 if (account == null) { 308 throw new IllegalArgumentException("account is null"); 309 } 310 311 if (!userData.containsKey(account)) { 312 userData.put(account, new HashMap<String, String>()); 313 } 314 315 Map<String, String> userDataMap = userData.get(account); 316 317 if (value == null) { 318 userDataMap.remove(key); 319 } else { 320 userDataMap.put(key, value); 321 } 322 } 323 324 @Implementation 325 protected void setPassword(Account account, String password) { 326 if (account == null) { 327 throw new IllegalArgumentException("account is null"); 328 } 329 330 if (password == null) { 331 passwords.remove(account); 332 } else { 333 passwords.put(account, password); 334 } 335 } 336 337 @Implementation 338 protected String getPassword(Account account) { 339 if (account == null) { 340 throw new IllegalArgumentException("account is null"); 341 } 342 343 if (passwords.containsKey(account)) { 344 return passwords.get(account); 345 } else { 346 return null; 347 } 348 } 349 350 @Implementation 351 protected void invalidateAuthToken(final String accountType, final String authToken) { 352 Account[] accountsByType = getAccountsByType(accountType); 353 for (Account account : accountsByType) { 354 Map<String, String> tokenMap = authTokens.get(account); 355 if (tokenMap != null) { 356 Iterator<Entry<String, String>> it = tokenMap.entrySet().iterator(); 357 while (it.hasNext()) { 358 Map.Entry<String, String> map = it.next(); 359 if (map.getValue().equals(authToken)) { 360 it.remove(); 361 } 362 } 363 authTokens.put(account, tokenMap); 364 } 365 } 366 } 367 368 /** 369 * Returns a bundle that contains the account session bundle under {@link 370 * AccountManager#KEY_ACCOUNT_SESSION_BUNDLE} to later be passed on to {@link 371 * AccountManager#finishSession(Bundle,Activity,AccountManagerCallback<Bundle>,Handler)}. The 372 * session bundle simply propagates the given {@code accountType} so as not to be empty and is not 373 * encrypted as it would be in the real implementation. If an activity isn't provided, resulting 374 * bundle will only have a dummy {@link Intent} under {@link AccountManager#KEY_INTENT}. 375 * 376 * @param accountType An authenticator must exist for the accountType, or else {@link 377 * AuthenticatorException} is thrown. 378 * @param authTokenType is ignored. 379 * @param requiredFeatures is ignored. 380 * @param options is ignored. 381 * @param activity if null, only {@link AccountManager#KEY_INTENT} will be present in result. 382 * @param callback if not null, will be called with result bundle. 383 * @param handler is ignored. 384 * @return future for bundle containing {@link AccountManager#KEY_ACCOUNT_SESSION_BUNDLE} if 385 * activity is provided, or {@link AccountManager#KEY_INTENT} otherwise. 386 */ 387 @Implementation(minSdk = O) 388 protected AccountManagerFuture<Bundle> startAddAccountSession( 389 String accountType, 390 String authTokenType, 391 String[] requiredFeatures, 392 Bundle options, 393 Activity activity, 394 AccountManagerCallback<Bundle> callback, 395 Handler handler) { 396 397 return start( 398 new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 399 @Override 400 public Bundle doWork() throws AuthenticatorException { 401 if (!authenticators.containsKey(accountType)) { 402 throw new AuthenticatorException("No authenticator specified for " + accountType); 403 } 404 405 Bundle resultBundle = new Bundle(); 406 407 if (activity == null) { 408 Intent resultIntent = new Intent(); 409 resultBundle.putParcelable(AccountManager.KEY_INTENT, resultIntent); 410 } else { 411 // This would actually be an encrypted bundle. Account type is copied as is simply to 412 // make it non-empty. 413 Bundle accountSessionBundle = new Bundle(); 414 accountSessionBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType); 415 resultBundle.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, Bundle.EMPTY); 416 } 417 418 return resultBundle; 419 } 420 }); 421 } 422 423 /** 424 * Returns sessionBundle as the result of finishSession. 425 * 426 * @param sessionBundle is returned as the result bundle. 427 * @param activity is ignored. 428 * @param callback if not null, will be called with result bundle. 429 * @param handler is ignored. 430 */ 431 @Implementation(minSdk = O) 432 protected AccountManagerFuture<Bundle> finishSession( 433 Bundle sessionBundle, 434 Activity activity, 435 AccountManagerCallback<Bundle> callback, 436 Handler handler) { 437 438 return start( 439 new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 440 @Override 441 public Bundle doWork() { 442 // Just return sessionBundle as the result since it's not really used, allowing it to 443 // be easily controlled in tests. 444 return sessionBundle; 445 } 446 }); 447 } 448 449 /** 450 * Based off of private method postToHandler(Handler, OnAccountsUpdateListener, Account[]) in 451 * {@link AccountManager} 452 */ 453 private void notifyListener( 454 OnAccountsUpdateListener listener, 455 @Nullable Set<String> accountTypesToReportOn, 456 Account[] allAccounts) { 457 if (accountTypesToReportOn != null) { 458 ArrayList<Account> filtered = new ArrayList<>(); 459 for (Account account : allAccounts) { 460 if (accountTypesToReportOn.contains(account.type)) { 461 filtered.add(account); 462 } 463 } 464 listener.onAccountsUpdated(filtered.toArray(new Account[0])); 465 } else { 466 listener.onAccountsUpdated(allAccounts); 467 } 468 } 469 470 private void notifyListeners(Account changedAccount) { 471 Account[] accounts = getAccounts(); 472 for (Map.Entry<OnAccountsUpdateListener, Set<String>> entry : listeners.entrySet()) { 473 OnAccountsUpdateListener listener = entry.getKey(); 474 Set<String> types = entry.getValue(); 475 if (types == null || types.contains(changedAccount.type)) { 476 notifyListener(listener, types, accounts); 477 } 478 } 479 } 480 481 /** 482 * @param account User account. 483 */ 484 public void addAccount(Account account) { 485 accounts.add(account); 486 if (pendingAddFuture != null) { 487 pendingAddFuture.resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); 488 start(pendingAddFuture); 489 pendingAddFuture = null; 490 } 491 notifyListeners(account); 492 } 493 494 /** 495 * Adds an account to the AccountManager but when {@link 496 * AccountManager#getAccountsByTypeForPackage(String, String)} is called will be included if is in 497 * one of the #visibleToPackages 498 * 499 * @param account User account. 500 */ 501 public void addAccount(Account account, String... visibleToPackages) { 502 addAccount(account); 503 HashSet<String> value = new HashSet<>(); 504 Collections.addAll(value, visibleToPackages); 505 packageVisibleAccounts.put(account, value); 506 } 507 508 /** 509 * Consumes and returns the next {@code addAccountOptions} passed to {@link #addAccount}. 510 * 511 * @return the next {@code addAccountOptions} 512 */ 513 public Bundle getNextAddAccountOptions() { 514 if (addAccountOptionsList.isEmpty()) { 515 return null; 516 } else { 517 return addAccountOptionsList.remove(0); 518 } 519 } 520 521 /** 522 * Returns the next {@code addAccountOptions} passed to {@link #addAccount} without consuming it. 523 * 524 * @return the next {@code addAccountOptions} 525 */ 526 public Bundle peekNextAddAccountOptions() { 527 if (addAccountOptionsList.isEmpty()) { 528 return null; 529 } else { 530 return addAccountOptionsList.get(0); 531 } 532 } 533 534 private class RoboAccountManagerFuture extends BaseRoboAccountManagerFuture<Bundle> { 535 private final String accountType; 536 private final Activity activity; 537 private final Bundle resultBundle; 538 539 RoboAccountManagerFuture( 540 AccountManagerCallback<Bundle> callback, 541 Handler handler, 542 String accountType, 543 Activity activity) { 544 super(callback, handler); 545 546 this.accountType = accountType; 547 this.activity = activity; 548 this.resultBundle = new Bundle(); 549 } 550 551 @Override 552 public Bundle doWork() throws AuthenticatorException { 553 if (!authenticators.containsKey(accountType)) { 554 throw new AuthenticatorException("No authenticator specified for " + accountType); 555 } 556 557 resultBundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType); 558 559 if (activity == null) { 560 Intent resultIntent = new Intent(); 561 resultBundle.putParcelable(AccountManager.KEY_INTENT, resultIntent); 562 } else if (callback == null) { 563 resultBundle.putString(AccountManager.KEY_ACCOUNT_NAME, "some_user@gmail.com"); 564 } 565 566 return resultBundle; 567 } 568 } 569 570 @Implementation 571 protected AccountManagerFuture<Bundle> addAccount( 572 final String accountType, 573 String authTokenType, 574 String[] requiredFeatures, 575 Bundle addAccountOptions, 576 Activity activity, 577 AccountManagerCallback<Bundle> callback, 578 Handler handler) { 579 addAccountOptionsList.add(addAccountOptions); 580 if (activity == null) { 581 // Caller only wants to get the intent, so start the future immediately. 582 RoboAccountManagerFuture future = 583 new RoboAccountManagerFuture(callback, handler, accountType, null); 584 start(future); 585 return future; 586 } else { 587 // Caller wants to start the sign in flow and return the intent with the new account added. 588 // Account can be added via ShadowAccountManager#addAccount. 589 pendingAddFuture = new RoboAccountManagerFuture(callback, handler, accountType, activity); 590 return pendingAddFuture; 591 } 592 } 593 594 public void setFeatures(Account account, String[] accountFeatures) { 595 HashSet<String> featureSet = new HashSet<>(); 596 featureSet.addAll(Arrays.asList(accountFeatures)); 597 this.accountFeatures.put(account, featureSet); 598 } 599 600 /** 601 * @param authenticator System authenticator. 602 */ 603 public void addAuthenticator(AuthenticatorDescription authenticator) { 604 authenticators.put(authenticator.type, authenticator); 605 } 606 607 public void addAuthenticator(String type) { 608 addAuthenticator(AuthenticatorDescription.newKey(type)); 609 } 610 611 private Map<Account, String> previousNames = new HashMap<Account, String>(); 612 613 /** 614 * Sets the previous name for an account, which will be returned by {@link 615 * AccountManager#getPreviousName(Account)}. 616 * 617 * @param account User account. 618 * @param previousName Previous account name. 619 */ 620 public void setPreviousAccountName(Account account, String previousName) { 621 previousNames.put(account, previousName); 622 } 623 624 /** 625 * @see #setPreviousAccountName(Account, String) 626 */ 627 @Implementation 628 protected String getPreviousName(Account account) { 629 return previousNames.get(account); 630 } 631 632 @Implementation 633 protected AccountManagerFuture<Bundle> getAuthToken( 634 final Account account, 635 final String authTokenType, 636 final Bundle options, 637 final Activity activity, 638 final AccountManagerCallback<Bundle> callback, 639 Handler handler) { 640 641 return start( 642 new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 643 @Override 644 public Bundle doWork() throws AuthenticatorException { 645 return getAuthToken(account, authTokenType); 646 } 647 }); 648 } 649 650 @Implementation 651 protected AccountManagerFuture<Bundle> getAuthToken( 652 final Account account, 653 final String authTokenType, 654 final Bundle options, 655 final boolean notifyAuthFailure, 656 final AccountManagerCallback<Bundle> callback, 657 Handler handler) { 658 659 return start( 660 new BaseRoboAccountManagerFuture<Bundle>(callback, handler) { 661 @Override 662 public Bundle doWork() throws AuthenticatorException { 663 return getAuthToken(account, authTokenType); 664 } 665 }); 666 } 667 668 private Bundle getAuthToken(Account account, String authTokenType) throws AuthenticatorException { 669 Bundle result = new Bundle(); 670 671 String authToken = blockingGetAuthToken(account, authTokenType, false); 672 result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); 673 result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); 674 result.putString(AccountManager.KEY_AUTHTOKEN, authToken); 675 676 if (authToken != null) { 677 return result; 678 } 679 680 if (!authenticators.containsKey(account.type)) { 681 throw new AuthenticatorException("No authenticator specified for " + account.type); 682 } 683 684 Intent resultIntent = new Intent(); 685 result.putParcelable(AccountManager.KEY_INTENT, resultIntent); 686 687 return result; 688 } 689 690 @Implementation 691 protected AccountManagerFuture<Boolean> hasFeatures( 692 final Account account, 693 final String[] features, 694 AccountManagerCallback<Boolean> callback, 695 Handler handler) { 696 return start( 697 new BaseRoboAccountManagerFuture<Boolean>(callback, handler) { 698 @Override 699 public Boolean doWork() { 700 Set<String> availableFeatures = accountFeatures.get(account); 701 for (String feature : features) { 702 if (!availableFeatures.contains(feature)) { 703 return false; 704 } 705 } 706 return true; 707 } 708 }); 709 } 710 711 @Implementation 712 protected AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures( 713 final String type, 714 final String[] features, 715 AccountManagerCallback<Account[]> callback, 716 Handler handler) { 717 return start( 718 new BaseRoboAccountManagerFuture<Account[]>(callback, handler) { 719 @Override 720 public Account[] doWork() throws AuthenticatorException { 721 722 if (authenticationErrorOnNextResponse) { 723 setAuthenticationErrorOnNextResponse(false); 724 throw new AuthenticatorException(); 725 } 726 727 List<Account> result = new ArrayList<>(); 728 729 Account[] accountsByType = getAccountsByType(type); 730 for (Account account : accountsByType) { 731 Set<String> featureSet = accountFeatures.get(account); 732 if (features == null 733 || (featureSet != null && featureSet.containsAll(Arrays.asList(features)))) { 734 result.add(account); 735 } 736 } 737 return result.toArray(new Account[result.size()]); 738 } 739 }); 740 } 741 742 private <T extends BaseRoboAccountManagerFuture> T start(T future) { 743 future.start(); 744 return future; 745 } 746 747 @Implementation 748 protected Account[] getAccountsByTypeForPackage(String type, String packageName) { 749 List<Account> result = new ArrayList<>(); 750 751 Account[] accountsByType = getAccountsByType(type); 752 for (Account account : accountsByType) { 753 if (packageVisibleAccounts.containsKey(account) 754 && packageVisibleAccounts.get(account).contains(packageName)) { 755 result.add(account); 756 } 757 } 758 759 return result.toArray(new Account[result.size()]); 760 } 761 762 /** 763 * Sets authenticator exception, which will be thrown by {@link #getAccountsByTypeAndFeatures}. 764 * 765 * @param authenticationErrorOnNextResponse to set flag that exception will be thrown on next 766 * response. 767 */ 768 public void setAuthenticationErrorOnNextResponse(boolean authenticationErrorOnNextResponse) { 769 this.authenticationErrorOnNextResponse = authenticationErrorOnNextResponse; 770 } 771 772 /** 773 * Sets the intent to include in Bundle result from {@link #removeAccount} if Activity is given. 774 * 775 * @param removeAccountIntent the intent to surface as {@link AccountManager#KEY_INTENT}. 776 */ 777 public void setRemoveAccountIntent(Intent removeAccountIntent) { 778 this.removeAccountIntent = removeAccountIntent; 779 } 780 781 public Map<OnAccountsUpdateListener, Set<String>> getListeners() { 782 return listeners; 783 } 784 785 private abstract class BaseRoboAccountManagerFuture<T> implements AccountManagerFuture<T> { 786 protected final AccountManagerCallback<T> callback; 787 private final Handler handler; 788 protected T result; 789 private Exception exception; 790 private boolean started = false; 791 792 BaseRoboAccountManagerFuture(AccountManagerCallback<T> callback, Handler handler) { 793 this.callback = callback; 794 this.handler = handler == null ? mainHandler : handler; 795 } 796 797 void start() { 798 if (started) return; 799 started = true; 800 801 try { 802 result = doWork(); 803 } catch (OperationCanceledException | IOException | AuthenticatorException e) { 804 exception = e; 805 } 806 807 if (callback != null) { 808 handler.post( 809 new Runnable() { 810 @Override 811 public void run() { 812 callback.run(BaseRoboAccountManagerFuture.this); 813 } 814 }); 815 } 816 } 817 818 @Override 819 public boolean cancel(boolean mayInterruptIfRunning) { 820 return false; 821 } 822 823 @Override 824 public boolean isCancelled() { 825 return false; 826 } 827 828 @Override 829 public boolean isDone() { 830 return result != null || exception != null || isCancelled(); 831 } 832 833 @Override 834 public T getResult() throws OperationCanceledException, IOException, AuthenticatorException { 835 start(); 836 837 if (exception instanceof OperationCanceledException) { 838 throw new OperationCanceledException(exception); 839 } else if (exception instanceof IOException) { 840 throw new IOException(exception); 841 } else if (exception instanceof AuthenticatorException) { 842 throw new AuthenticatorException(exception); 843 } 844 return result; 845 } 846 847 @Override 848 public T getResult(long timeout, TimeUnit unit) 849 throws OperationCanceledException, IOException, AuthenticatorException { 850 return getResult(); 851 } 852 853 public abstract T doWork() 854 throws OperationCanceledException, IOException, AuthenticatorException; 855 } 856 } 857