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         this.mResponse = response;
164         this.mAccountType = accountType;
165         this.mAuthTokenType = authTokenType;
166         this.mRequiredFeatures = requiredFeatures;
167         this.mOptionsAddAccount = options;
168         AccountManager am = AccountManager.get(mContext);
169         am.addAccountExplicitly(AccountManagerTest.ACCOUNT, "fakePassword", null);
170         return createResultBundle();
171     }
172 
173     /**
174      * Update the locally stored credentials for an account.
175      */
176     @Override
updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)177     public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
178             String authTokenType, Bundle options) throws NetworkErrorException {
179         this.mResponse = response;
180         this.mAccount = account;
181         this.mAuthTokenType = authTokenType;
182         this.mOptionsUpdateCredentials = options;
183         return createResultBundle();
184     }
185 
186     /**
187      * Returns a Bundle that contains the Intent of the activity that can be used to edit the
188      * properties. In order to indicate success the activity should call response.setResult()
189      * with a non-null Bundle.
190      */
191     @Override
editProperties(AccountAuthenticatorResponse response, String accountType)192     public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
193         this.mResponse = response;
194         this.mAccountType = accountType;
195         return createResultBundle();
196     }
197 
198     /**
199      * Checks that the user knows the credentials of an account.
200      */
201     @Override
confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options)202     public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
203             Bundle options) throws NetworkErrorException {
204         this.mResponse = response;
205         this.mAccount = account;
206         this.mOptionsConfirmCredentials = options;
207         Bundle result = new Bundle();
208         if (options.containsKey(KEY_RETURN_INTENT)) {
209             Intent intent = new Intent();
210             intent.setClassName("android.accounts.cts", "android.accounts.cts.AccountDummyActivity");
211             result.putParcelable(AccountManager.KEY_INTENT, intent);
212         } else {
213             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
214         }
215 
216         return result;
217     }
218 
219     /**
220      * Gets the authtoken for an account.
221      */
222     @Override
getAuthToken( AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)223     public Bundle getAuthToken(
224             AccountAuthenticatorResponse response,
225             Account account,
226             String authTokenType,
227             Bundle options) throws NetworkErrorException {
228         Log.w(TAG, "MockAuth - getAuthToken@" + System.currentTimeMillis());
229         mIsRecentlyCalled.set(true);
230         this.mResponse = response;
231         this.mAccount = account;
232         this.mAuthTokenType = authTokenType;
233         this.mOptionsGetAuthToken = options;
234         Bundle result = new Bundle();
235 
236         result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
237         result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
238         String token;
239         if (AccountManagerTest.AUTH_EXPIRING_TOKEN_TYPE.equals(authTokenType)) {
240             /*
241              * The resultant token should simply be the expiration timestamp. E.g. the time after
242              * which getting a new AUTH_EXPIRING_TOKEN_TYPE typed token will return a different
243              * value.
244              */
245             long expiry = System.currentTimeMillis() + mTokenDurationMillis;
246             result.putLong(AbstractAccountAuthenticator.KEY_CUSTOM_TOKEN_EXPIRY, expiry);
247         }
248         result.putString(
249                 AccountManager.KEY_AUTHTOKEN,
250                 Integer.toString(mTokenCounter.incrementAndGet()));
251         return result;
252     }
253 
254     /**
255      * Ask the authenticator for a localized label for the given authTokenType.
256      */
257     @Override
getAuthTokenLabel(String authTokenType)258     public String getAuthTokenLabel(String authTokenType) {
259         this.mAuthTokenType = authTokenType;
260         return AccountManagerTest.AUTH_TOKEN_LABEL;
261     }
262 
263     /**
264      * Checks if the account supports all the specified authenticator specific features.
265      */
266     @Override
hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features)267     public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
268             String[] features) throws NetworkErrorException {
269 
270         this.mResponse = response;
271         this.mAccount = account;
272         this.mFeatures = features;
273 
274         Bundle result = new Bundle();
275         if (null == features) {
276             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
277         }
278         else {
279             boolean booleanResult = true;
280             for (String feature: features) {
281                 if (!mockFeatureList.contains(feature)) {
282                     booleanResult = false;
283                     break;
284                 }
285             }
286             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, booleanResult);
287         }
288         return result;
289     }
290 
291     @Override
getAccountRemovalAllowed(AccountAuthenticatorResponse response, Account account)292     public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
293             Account account) throws NetworkErrorException {
294         final Bundle result = new Bundle();
295         if (ACCOUNT_NAME_FOR_NEW_REMOVE_API.equals(account.name)) {
296             Intent intent = AccountRemovalDummyActivity.createIntent(mContext);
297             // Pass in the authenticator response, so that account removal can
298             // be
299             // completed
300             intent.putExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
301             intent.putExtra(KEY_ACCOUNT_INFO, account);
302             result.putParcelable(AccountManager.KEY_INTENT, intent);
303             // Adding this following line to reject account installation
304             // requests
305             // coming from old removeAccount API.
306             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
307         } else if (ACCOUNT_NAME_FOR_DEFAULT_IMPL.equals(account.name)) {
308             return super.getAccountRemovalAllowed(response, account);
309         } else {
310             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
311         }
312         return result;
313     }
314 
315     @Override
addAccountFromCredentials(final AccountAuthenticatorResponse response, Account account, Bundle accountCredentials)316     public Bundle addAccountFromCredentials(final AccountAuthenticatorResponse response,
317             Account account,
318             Bundle accountCredentials) throws NetworkErrorException {
319         return super.addAccountFromCredentials(response, account, accountCredentials);
320     }
321 
322     @Override
getAccountCredentialsForCloning(final AccountAuthenticatorResponse response, final Account account)323     public Bundle getAccountCredentialsForCloning(final AccountAuthenticatorResponse response,
324             final Account account) throws NetworkErrorException {
325         return super.getAccountCredentialsForCloning(response, account);
326     }
327 
328 
329     /**
330      * Start add account flow of the specified accountType to authenticate user.
331      */
332     @Override
startAddAccountSession(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)333     public Bundle startAddAccountSession(AccountAuthenticatorResponse response,
334             String accountType,
335             String authTokenType,
336             String[] requiredFeatures,
337             Bundle options) throws NetworkErrorException {
338         this.mResponse = response;
339         this.mAccountType = accountType;
340         this.mAuthTokenType = authTokenType;
341         this.mRequiredFeatures = requiredFeatures;
342         this.mOptionsStartAddAccountSession = options;
343 
344         String accountName = null;
345         Bundle sessionBundle = null;
346         if (options != null) {
347             accountName = options.getString(Fixtures.KEY_ACCOUNT_NAME);
348             sessionBundle = options.getBundle(Fixtures.KEY_ACCOUNT_SESSION_BUNDLE);
349         }
350 
351         Bundle result = new Bundle();
352         if (accountName.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
353             // fill bundle with a success result.
354             result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
355             result.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
356                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
357             result.putString(AccountManager.KEY_PASSWORD, AccountManagerTest.ACCOUNT_PASSWORD);
358             result.putString(AccountManager.KEY_AUTHTOKEN,
359                     Integer.toString(mTokenCounter.incrementAndGet()));
360         } else if (accountName.startsWith(Fixtures.PREFIX_NAME_INTERVENE)) {
361             // Specify data to be returned by the eventual activity.
362             Intent eventualActivityResultData = new Intent();
363             eventualActivityResultData.putExtra(AccountManager.KEY_AUTHTOKEN,
364                     Integer.toString(mTokenCounter.incrementAndGet()));
365             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
366                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
367             eventualActivityResultData.putExtra(AccountManager.KEY_PASSWORD,
368                     AccountManagerTest.ACCOUNT_PASSWORD);
369             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE,
370                     sessionBundle);
371             // Fill result with Intent.
372             Intent intent = new Intent(mContext, AccountAuthenticatorDummyActivity.class);
373             intent.putExtra(Fixtures.KEY_RESULT, eventualActivityResultData);
374             intent.putExtra(Fixtures.KEY_CALLBACK, response);
375 
376             result.putParcelable(AccountManager.KEY_INTENT, intent);
377         } else {
378             // fill with error
379             fillResultWithError(result, options);
380         }
381         return result;
382     }
383 
384     /**
385      * Start update credentials flow to re-auth user without updating locally stored credentials
386      * for an account.
387      */
388     @Override
startUpdateCredentialsSession(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options)389     public Bundle startUpdateCredentialsSession(AccountAuthenticatorResponse response,
390             Account account,
391             String authTokenType,
392             Bundle options) throws NetworkErrorException {
393         mResponse = response;
394         mAccount = account;
395         mAuthTokenType = authTokenType;
396         mOptionsStartUpdateCredentialsSession = options;
397 
398         String accountName = null;
399         Bundle sessionBundle = null;
400         if (options != null) {
401             accountName = options.getString(Fixtures.KEY_ACCOUNT_NAME);
402             sessionBundle = options.getBundle(Fixtures.KEY_ACCOUNT_SESSION_BUNDLE);
403         }
404 
405         Bundle result = new Bundle();
406         if (accountName.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
407             // fill bundle with a success result.
408             result.putBundle(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE, sessionBundle);
409             result.putString(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
410                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
411             result.putString(AccountManager.KEY_PASSWORD, AccountManagerTest.ACCOUNT_PASSWORD);
412             result.putString(AccountManager.KEY_AUTHTOKEN,
413                     Integer.toString(mTokenCounter.incrementAndGet()));
414         } else if (accountName.startsWith(Fixtures.PREFIX_NAME_INTERVENE)) {
415             // Specify data to be returned by the eventual activity.
416             Intent eventualActivityResultData = new Intent();
417             eventualActivityResultData.putExtra(AccountManager.KEY_AUTHTOKEN,
418                     Integer.toString(mTokenCounter.incrementAndGet()));
419             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_STATUS_TOKEN,
420                     AccountManagerTest.ACCOUNT_STATUS_TOKEN);
421             eventualActivityResultData.putExtra(AccountManager.KEY_PASSWORD,
422                     AccountManagerTest.ACCOUNT_PASSWORD);
423             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_SESSION_BUNDLE,
424                     sessionBundle);
425             // Fill result with Intent.
426             Intent intent = new Intent(mContext, AccountAuthenticatorDummyActivity.class);
427             intent.putExtra(Fixtures.KEY_RESULT, eventualActivityResultData);
428             intent.putExtra(Fixtures.KEY_CALLBACK, response);
429 
430             result.putParcelable(AccountManager.KEY_INTENT, intent);
431         } else {
432             // fill with error
433             fillResultWithError(result, options);
434         }
435         return result;
436     }
437 
438     /**
439      * Finishes account session started by adding the account to device or updating the local
440      * credentials.
441      */
442     @Override
finishSession(AccountAuthenticatorResponse response, String accountType, Bundle sessionBundle)443     public Bundle finishSession(AccountAuthenticatorResponse response,
444             String accountType,
445             Bundle sessionBundle) throws NetworkErrorException {
446         this.mResponse = response;
447         this.mAccountType = accountType;
448         this.mOptionsFinishSession = sessionBundle;
449 
450         String accountName = null;
451         if (sessionBundle != null) {
452             accountName = sessionBundle.getString(Fixtures.KEY_ACCOUNT_NAME);
453         }
454 
455         Bundle result = new Bundle();
456         if (accountName.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
457             // fill bundle with a success result.
458             result.putString(AccountManager.KEY_ACCOUNT_NAME, AccountManagerTest.ACCOUNT_NAME);
459             result.putString(AccountManager.KEY_ACCOUNT_TYPE, AccountManagerTest.ACCOUNT_TYPE);
460             result.putString(AccountManager.KEY_AUTHTOKEN,
461                     Integer.toString(mTokenCounter.incrementAndGet()));
462         } else if (accountName.startsWith(Fixtures.PREFIX_NAME_INTERVENE)) {
463             // Specify data to be returned by the eventual activity.
464             Intent eventualActivityResultData = new Intent();
465             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_NAME,
466                     AccountManagerTest.ACCOUNT_NAME);
467             eventualActivityResultData.putExtra(AccountManager.KEY_ACCOUNT_TYPE,
468                     AccountManagerTest.ACCOUNT_TYPE);
469             eventualActivityResultData.putExtra(AccountManager.KEY_AUTHTOKEN,
470                     Integer.toString(mTokenCounter.incrementAndGet()));
471 
472             // Fill result with Intent.
473             Intent intent = new Intent(mContext, AccountAuthenticatorDummyActivity.class);
474             intent.putExtra(Fixtures.KEY_RESULT, eventualActivityResultData);
475             intent.putExtra(Fixtures.KEY_CALLBACK, response);
476 
477             result.putParcelable(AccountManager.KEY_INTENT, intent);
478         } else {
479             // fill with error
480             fillResultWithError(result, sessionBundle);
481         }
482         return result;
483     }
484 
fillResultWithError(Bundle result, Bundle options)485     private void fillResultWithError(Bundle result, Bundle options) {
486         int errorCode = AccountManager.ERROR_CODE_INVALID_RESPONSE;
487         String errorMsg = "Default Error Message";
488         if (options != null) {
489             errorCode = options.getInt(AccountManager.KEY_ERROR_CODE);
490             errorMsg = options.getString(AccountManager.KEY_ERROR_MESSAGE);
491         }
492         result.putInt(AccountManager.KEY_ERROR_CODE, errorCode);
493         result.putString(AccountManager.KEY_ERROR_MESSAGE, errorMsg);
494     }
495 
496     /**
497      * Checks if the credentials of the account should be updated.
498      */
499     @Override
isCredentialsUpdateSuggested( final AccountAuthenticatorResponse response, Account account, String statusToken)500     public Bundle isCredentialsUpdateSuggested(
501             final AccountAuthenticatorResponse response,
502             Account account,
503             String statusToken) throws NetworkErrorException {
504         this.mResponse = response;
505         this.mAccount = account;
506         this.mStatusToken = statusToken;
507 
508         Bundle result = new Bundle();
509         if (account.name.startsWith(Fixtures.PREFIX_NAME_SUCCESS)) {
510             // fill bundle with a success result.
511             result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true);
512         } else {
513             // fill with error
514             int errorCode = AccountManager.ERROR_CODE_INVALID_RESPONSE;
515             String errorMsg = "Default Error Message";
516             result.putInt(AccountManager.KEY_ERROR_CODE, errorCode);
517             result.putString(AccountManager.KEY_ERROR_MESSAGE, errorMsg);
518         }
519 
520         response.onResult(result);
521         return null;
522     }
523 }
524