• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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