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