1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.accounts.cts;
18 
19 import android.accounts.AbstractAccountAuthenticator;
20 import android.accounts.Account;
21 import android.accounts.AccountAuthenticatorResponse;
22 import android.accounts.AccountManager;
23 import android.accounts.NetworkErrorException;
24 import android.accounts.cts.common.Fixtures;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.os.Bundle;
28 import android.util.Log;
29 
30 import java.util.ArrayList;
31 import java.util.concurrent.atomic.AtomicBoolean;
32 import java.util.concurrent.atomic.AtomicInteger;
33 
34 /**
35  * A simple Mock Account Authenticator
36  */
37 public class MockAccountAuthenticator extends AbstractAccountAuthenticator {
38     private static String TAG = "AccountManagerTest";
39 
40     public static String KEY_ACCOUNT_INFO = "key_account_info";
41     public static String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = "key_account_authenticator_response";
42     public static String ACCOUNT_NAME_FOR_NEW_REMOVE_API = "call new removeAccount api";
43     public static String ACCOUNT_NAME_FOR_DEFAULT_IMPL = "call super api";
44     // Key for triggering return intent flow
45     public static String KEY_RETURN_INTENT = "return an intent";
46     public static String ACCOUNT_NAME_FOR_NEW_REMOVE_API1 = "call new removeAccount api";
47 
48     private final Context mContext;
49     private final AtomicInteger mTokenCounter  = new AtomicInteger(0);
50     private final AtomicBoolean mIsRecentlyCalled = new AtomicBoolean(false);
51 
52     AccountAuthenticatorResponse mResponse;
53     String mAccountType;
54     String mAuthTokenType;
55     String[] mRequiredFeatures;
56     public Bundle mOptionsUpdateCredentials;
57     public Bundle mOptionsConfirmCredentials;
58     public Bundle mOptionsAddAccount;
59     public Bundle mOptionsGetAuthToken;
60     public Bundle mOptionsStartAddAccountSession;
61     public Bundle mOptionsStartUpdateCredentialsSession;
62     public Bundle mOptionsFinishSession;
63     Account mAccount;
64     String[] mFeatures;
65     String mStatusToken;
66 
67     final ArrayList<String> mockFeatureList = new ArrayList<String>();
68     private final long mTokenDurationMillis = 1000; // 1 second
69 
MockAccountAuthenticator(Context context)70     public MockAccountAuthenticator(Context context) {
71         super(context);
72         mContext = context;
73 
74         // Create some mock features
75         mockFeatureList.add(AccountManagerTest.FEATURE_1);
76         mockFeatureList.add(AccountManagerTest.FEATURE_2);
77     }
78 
getTokenDurationMillis()79     public long getTokenDurationMillis() {
80         return mTokenDurationMillis;
81     }
82 
isRecentlyCalled()83     public boolean isRecentlyCalled() {
84         return mIsRecentlyCalled.getAndSet(false);
85     }
86 
getLastTokenServed()87     public String getLastTokenServed() {
88         return Integer.toString(mTokenCounter.get());
89     }
90 
getResponse()91     public AccountAuthenticatorResponse getResponse() {
92         return mResponse;
93     }
94 
getAccountType()95     public String getAccountType() {
96         return mAccountType;
97     }
98 
getAuthTokenType()99     public String getAuthTokenType() {
100         return mAuthTokenType;
101     }
102 
getRequiredFeatures()103     public String[] getRequiredFeatures() {
104         return mRequiredFeatures;
105     }
106 
getAccount()107     public Account getAccount() {
108         return mAccount;
109     }
110 
getFeatures()111     public String[] getFeatures() {
112         return mFeatures;
113     }
114 
getStatusToken()115     public String getStatusToken() {
116         return mStatusToken;
117     }
118 
clearData()119     public void clearData() {
120         mResponse = null;
121         mAccountType = null;
122         mAuthTokenType = null;
123         mRequiredFeatures = null;
124         mOptionsUpdateCredentials = null;
125         mOptionsAddAccount = null;
126         mOptionsGetAuthToken = null;
127         mOptionsConfirmCredentials = null;
128         mOptionsStartAddAccountSession = null;
129         mOptionsStartUpdateCredentialsSession = null;
130         mOptionsFinishSession = null;
131         mAccount = null;
132         mFeatures = null;
133         mStatusToken = null;
134     }
135 
callAccountAuthenticated()136     public void callAccountAuthenticated() {
137         AccountManager am = AccountManager.get(mContext);
138         am.notifyAccountAuthenticated(mAccount);
139     }
140 
callSetPassword()141     public void callSetPassword() {
142         AccountManager am = AccountManager.get(mContext);
143         am.setPassword(mAccount, "password");
144     }
145 
createResultBundle()146     private Bundle createResultBundle() {
147         Bundle result = new Bundle();
148         result.putString(AccountManager.KEY_ACCOUNT_NAME, AccountManagerTest.ACCOUNT_NAME);
149         result.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountManagerTest.ACCOUNT_TYPE);
150         result.putString(
151                 AccountManager.KEY_AUTHTOKEN,
152                 Integer.toString(mTokenCounter.incrementAndGet()));
153         return result;
154     }
155 
156     /**
157      * Adds an account of the specified accountType.
158      */
159     @Override
addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)160     public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
161             String authTokenType, String[] requiredFeatures, Bundle options)
162             throws NetworkErrorException {
163         Log.i(TAG, "MockAuth - addAccount");
164         this.mResponse = response;
165         this.mAccountType = accountType;
166         this.mAuthTokenType = authTokenType;
167         this.mRequiredFeatures = requiredFeatures;
168         this.mOptionsAddAccount = options;
169         AccountManager am = AccountManager.get(mContext);
170         am.addAccountExplicitly(AccountManagerTest.ACCOUNT, "fakePassword", null);
171         return createResultBundle();
172     }
173 
174     /**
175      * Update the locally stored credentials for an account.
176      */
177     @Override
updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)178     public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
179             String authTokenType, Bundle options) throws NetworkErrorException {
180         this.mResponse = response;
181         this.mAccount = account;
182         this.mAuthTokenType = authTokenType;
183         this.mOptionsUpdateCredentials = options;
184         return createResultBundle();
185     }
186 
187     /**
188      * Returns a Bundle that contains the Intent of the activity that can be used to edit the
189      * properties. In order to indicate success the activity should call response.setResult()
190      * with a non-null Bundle.
191      */
192     @Override
editProperties(AccountAuthenticatorResponse response, String accountType)193     public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
194         this.mResponse = response;
195         this.mAccountType = accountType;
196         return createResultBundle();
197     }
198 
199     /**
200      * Checks that the user knows the credentials of an account.
201      */
202     @Override
confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)203     public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
204             Bundle options) throws NetworkErrorException {
205         this.mResponse = response;
206         this.mAccount = account;
207         this.mOptionsConfirmCredentials = options;
208         Bundle result = new Bundle();
209         if (options.containsKey(KEY_RETURN_INTENT)) {
210             Intent intent = new Intent();
211             intent.setClassName("android.accounts.cts", "android.accounts.cts.AccountDummyActivity");
212             result.putParcelable(AccountManager.KEY_INTENT, intent);
213         } else {
214             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
215         }
216 
217         return result;
218     }
219 
220     /**
221      * Gets the authtoken for an account.
222      */
223     @Override
getAuthToken( AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)224     public Bundle getAuthToken(
225             AccountAuthenticatorResponse response,
226             Account account,
227             String authTokenType,
228             Bundle options) throws NetworkErrorException {
229         Log.i(TAG, "MockAuth - getAuthToken@" + System.currentTimeMillis());
230         mIsRecentlyCalled.set(true);
231         this.mResponse = response;
232         this.mAccount = account;
233         this.mAuthTokenType = authTokenType;
234         this.mOptionsGetAuthToken = options;
235         Bundle result = new Bundle();
236 
237         result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
238         result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
239         String token;
240         if (AccountManagerTest.AUTH_EXPIRING_TOKEN_TYPE.equals(authTokenType)) {
241             /*
242              * The resultant token should simply be the expiration timestamp. E.g. the time after
243              * which getting a new AUTH_EXPIRING_TOKEN_TYPE typed token will return a different
244              * value.
245              */
246             long expiry = System.currentTimeMillis() + mTokenDurationMillis;
247             result.putLong(AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, expiry);
248         }
249         result.putString(
250                 AccountManager.KEY_AUTHTOKEN,
251                 Integer.toString(mTokenCounter.incrementAndGet()));
252         return result;
253     }
254 
255     /**
256      * Ask the authenticator for a localized label for the given authTokenType.
257      */
258     @Override
getAuthTokenLabel(String authTokenType)259     public String getAuthTokenLabel(String authTokenType) {
260         this.mAuthTokenType = authTokenType;
261         return AccountManagerTest.AUTH_TOKEN_LABEL;
262     }
263 
264     /**
265      * Checks if the account supports all the specified authenticator specific features.
266      */
267     @Override
hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features)268     public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
269             String[] features) throws NetworkErrorException {
270         Log.i(TAG, "MockAuth - hasFeatures");
271         this.mResponse = response;
272         this.mAccount = account;
273         this.mFeatures = features;
274 
275         Bundle result = new Bundle();
276         if (null == features) {
277             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
278         }
279         else {
280             boolean booleanResult = true;
281             for (String feature: features) {
282                 if (!mockFeatureList.contains(feature)) {
283                     booleanResult = false;
284                     break;
285                 }
286             }
287             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, booleanResult);
288         }
289         return result;
290     }
291 
292     @Override
getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account)293     public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
294             Account account) throws NetworkErrorException {
295         Log.i(TAG, "MockAuth - getAccountRemovalAllowed");
296         final Bundle result = new Bundle();
297         if (ACCOUNT_NAME_FOR_NEW_REMOVE_API.equals(account.name)) {
298             Intent intent = AccountRemovalDummyActivity.createIntent(mContext);
299             // Pass in the authenticator response, so that account removal can
300             // be
301             // completed
302             intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
303             intent.putExtra(KEY_ACCOUNT_INFO, account);
304             result.putParcelable(AccountManager.KEY_INTENT, intent);
305             // Adding this following line to reject account installation
306             // requests
307             // coming from old removeAccount API.
308             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
309         } else if (ACCOUNT_NAME_FOR_DEFAULT_IMPL.equals(account.name)) {
310             return super.getAccountRemovalAllowed(response, account);
311         } else {
312             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
313         }
314         return result;
315     }
316 
317     @Override
addAccountFromCredentials(final AccountAuthenticatorResponse response, Account account, Bundle accountCredentials)318     public Bundle addAccountFromCredentials(final AccountAuthenticatorResponse response,
319             Account account,
320             Bundle accountCredentials) throws NetworkErrorException {
321         return super.addAccountFromCredentials(response, account, accountCredentials);
322     }
323 
324     @Override
getAccountCredentialsForCloning(final AccountAuthenticatorResponse response, final Account account)325     public Bundle getAccountCredentialsForCloning(final AccountAuthenticatorResponse response,
326             final Account account) throws NetworkErrorException {
327         return super.getAccountCredentialsForCloning(response, account);
328     }
329 
330 
331     /**
332      * Start add account flow of the specified accountType to authenticate user.
333      */
334     @Override
startAddAccountSession(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)335     public Bundle startAddAccountSession(AccountAuthenticatorResponse response,
336             String accountType,
337             String authTokenType,
338             String[] requiredFeatures,
339             Bundle options) throws NetworkErrorException {
340         this.mResponse = response;
341         this.mAccountType = accountType;
342         this.mAuthTokenType = authTokenType;
343         this.mRequiredFeatures = requiredFeatures;
344         this.mOptionsStartAddAccountSession = options;
345 
346         String accountName = null;
347         Bundle sessionBundle = null;
348         if (options != null) {
349             accountName = options.getString(Fixtures.KEY_ACCOUNT_NAME);
350             sessionBundle = options.getBundle(Fixtures.KEY_ACCOUNT_SESSION_BUNDLE);
351         }
352 
353         Bundle result = new Bundle();
354         if (accountName.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
355             // fill bundle with a success result.
356             result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
357             result.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
358                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
359             result.putString(AccountManager.KEY_PASSWORD, AccountManagerTest.ACCOUNT_PASSWORD);
360             result.putString(AccountManager.KEY_AUTHTOKEN,
361                     Integer.toString(mTokenCounter.incrementAndGet()));
362         } else if (accountName.startsWith(Fixtures.PREFIX_NAME_INTERVENE)) {
363             // Specify data to be returned by the eventual activity.
364             Intent eventualActivityResultData = new Intent();
365             eventualActivityResultData.putExtra(AccountManager.KEY_AUTHTOKEN,
366                     Integer.toString(mTokenCounter.incrementAndGet()));
367             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
368                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
369             eventualActivityResultData.putExtra(AccountManager.KEY_PASSWORD,
370                     AccountManagerTest.ACCOUNT_PASSWORD);
371             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE,
372                     sessionBundle);
373             // Fill result with Intent.
374             Intent intent = new Intent(mContext, AccountAuthenticatorDummyActivity.class);
375             intent.putExtra(Fixtures.KEY_RESULT, eventualActivityResultData);
376             intent.putExtra(Fixtures.KEY_CALLBACK, response);
377 
378             result.putParcelable(AccountManager.KEY_INTENT, intent);
379         } else {
380             // fill with error
381             fillResultWithError(result, options);
382         }
383         return result;
384     }
385 
386     /**
387      * Start update credentials flow to re-auth user without updating locally stored credentials
388      * for an account.
389      */
390     @Override
startUpdateCredentialsSession(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)391     public Bundle startUpdateCredentialsSession(AccountAuthenticatorResponse response,
392             Account account,
393             String authTokenType,
394             Bundle options) throws NetworkErrorException {
395         Log.i(TAG, "MockAuth - startUpdateCredentialsSession");
396         mResponse = response;
397         mAccount = account;
398         mAuthTokenType = authTokenType;
399         mOptionsStartUpdateCredentialsSession = options;
400 
401         String accountName = null;
402         Bundle sessionBundle = null;
403         if (options != null) {
404             accountName = options.getString(Fixtures.KEY_ACCOUNT_NAME);
405             sessionBundle = options.getBundle(Fixtures.KEY_ACCOUNT_SESSION_BUNDLE);
406         }
407 
408         Bundle result = new Bundle();
409         if (accountName.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
410             // fill bundle with a success result.
411             result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
412             result.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
413                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
414             result.putString(AccountManager.KEY_PASSWORD, AccountManagerTest.ACCOUNT_PASSWORD);
415             result.putString(AccountManager.KEY_AUTHTOKEN,
416                     Integer.toString(mTokenCounter.incrementAndGet()));
417         } else if (accountName.startsWith(Fixtures.PREFIX_NAME_INTERVENE)) {
418             // Specify data to be returned by the eventual activity.
419             Intent eventualActivityResultData = new Intent();
420             eventualActivityResultData.putExtra(AccountManager.KEY_AUTHTOKEN,
421                     Integer.toString(mTokenCounter.incrementAndGet()));
422             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
423                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
424             eventualActivityResultData.putExtra(AccountManager.KEY_PASSWORD,
425                     AccountManagerTest.ACCOUNT_PASSWORD);
426             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE,
427                     sessionBundle);
428             // Fill result with Intent.
429             Intent intent = new Intent(mContext, AccountAuthenticatorDummyActivity.class);
430             intent.putExtra(Fixtures.KEY_RESULT, eventualActivityResultData);
431             intent.putExtra(Fixtures.KEY_CALLBACK, response);
432 
433             result.putParcelable(AccountManager.KEY_INTENT, intent);
434         } else {
435             // fill with error
436             fillResultWithError(result, options);
437         }
438         return result;
439     }
440 
441     /**
442      * Finishes account session started by adding the account to device or updating the local
443      * credentials.
444      */
445     @Override
finishSession(AccountAuthenticatorResponse response, String accountType, Bundle sessionBundle)446     public Bundle finishSession(AccountAuthenticatorResponse response,
447             String accountType,
448             Bundle sessionBundle) throws NetworkErrorException {
449         this.mResponse = response;
450         this.mAccountType = accountType;
451         this.mOptionsFinishSession = sessionBundle;
452 
453         String accountName = null;
454         if (sessionBundle != null) {
455             accountName = sessionBundle.getString(Fixtures.KEY_ACCOUNT_NAME);
456         }
457 
458         Bundle result = new Bundle();
459         if (accountName.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
460             // fill bundle with a success result.
461             result.putString(AccountManager.KEY_ACCOUNT_NAME, AccountManagerTest.ACCOUNT_NAME);
462             result.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountManagerTest.ACCOUNT_TYPE);
463             result.putString(AccountManager.KEY_AUTHTOKEN,
464                     Integer.toString(mTokenCounter.incrementAndGet()));
465         } else if (accountName.startsWith(Fixtures.PREFIX_NAME_INTERVENE)) {
466             // Specify data to be returned by the eventual activity.
467             Intent eventualActivityResultData = new Intent();
468             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_NAME,
469                     AccountManagerTest.ACCOUNT_NAME);
470             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_TYPE,
471                     AccountManagerTest.ACCOUNT_TYPE);
472             eventualActivityResultData.putExtra(AccountManager.KEY_AUTHTOKEN,
473                     Integer.toString(mTokenCounter.incrementAndGet()));
474 
475             // Fill result with Intent.
476             Intent intent = new Intent(mContext, AccountAuthenticatorDummyActivity.class);
477             intent.putExtra(Fixtures.KEY_RESULT, eventualActivityResultData);
478             intent.putExtra(Fixtures.KEY_CALLBACK, response);
479 
480             result.putParcelable(AccountManager.KEY_INTENT, intent);
481         } else {
482             // fill with error
483             fillResultWithError(result, sessionBundle);
484         }
485         return result;
486     }
487 
fillResultWithError(Bundle result, Bundle options)488     private void fillResultWithError(Bundle result, Bundle options) {
489         int errorCode = AccountManager.ERROR_CODE_INVALID_RESPONSE;
490         String errorMsg = "Default Error Message";
491         if (options != null) {
492             errorCode = options.getInt(AccountManager.KEY_ERROR_CODE);
493             errorMsg = options.getString(AccountManager.KEY_ERROR_MESSAGE);
494         }
495         result.putInt(AccountManager.KEY_ERROR_CODE, errorCode);
496         result.putString(AccountManager.KEY_ERROR_MESSAGE, errorMsg);
497     }
498 
499     /**
500      * Checks if the credentials of the account should be updated.
501      */
502     @Override
isCredentialsUpdateSuggested( final AccountAuthenticatorResponse response, Account account, String statusToken)503     public Bundle isCredentialsUpdateSuggested(
504             final AccountAuthenticatorResponse response,
505             Account account,
506             String statusToken) throws NetworkErrorException {
507         this.mResponse = response;
508         this.mAccount = account;
509         this.mStatusToken = statusToken;
510 
511         Bundle result = new Bundle();
512         if (account.name.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
513             // fill bundle with a success result.
514             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
515         } else {
516             // fill with error
517             int errorCode = AccountManager.ERROR_CODE_INVALID_RESPONSE;
518             String errorMsg = "Default Error Message";
519             result.putInt(AccountManager.KEY_ERROR_CODE, errorCode);
520             result.putString(AccountManager.KEY_ERROR_MESSAGE, errorMsg);
521         }
522 
523         response.onResult(result);
524         return null;
525     }
526 }
527