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