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