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 com.android.development;
18 
19 import android.accounts.Account;
20 import android.accounts.AccountManager;
21 import android.accounts.AccountManagerCallback;
22 import android.accounts.AccountManagerFuture;
23 import android.accounts.AuthenticatorDescription;
24 import android.accounts.AuthenticatorException;
25 import android.accounts.OnAccountsUpdateListener;
26 import android.accounts.OperationCanceledException;
27 import android.app.Activity;
28 import android.app.AlertDialog;
29 import android.app.Dialog;
30 import android.content.Context;
31 import android.content.DialogInterface;
32 import android.content.pm.PackageManager;
33 import android.content.res.Resources;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.os.Parcelable;
37 import android.text.TextUtils;
38 import android.util.Log;
39 import android.view.ContextMenu;
40 import android.view.LayoutInflater;
41 import android.view.MenuInflater;
42 import android.view.MenuItem;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.widget.AdapterView;
46 import android.widget.ArrayAdapter;
47 import android.widget.EditText;
48 import android.widget.ImageView;
49 import android.widget.ListView;
50 import android.widget.Spinner;
51 import android.widget.TextView;
52 
53 import java.io.IOException;
54 import java.util.ArrayList;
55 import java.util.List;
56 
57 public class AccountsTester extends Activity implements OnAccountsUpdateListener {
58     private static final String TAG = "AccountsTester";
59     private Spinner mAccountTypesSpinner;
60     private ListView mAccountsListView;
61     private AccountManager mAccountManager;
62     private Account mLongPressedAccount = null;
63     private AuthenticatorDescription[] mAuthenticatorDescs;
64 
65     private static final int GET_AUTH_TOKEN_DIALOG_ID = 1;
66     private static final int UPDATE_CREDENTIALS_DIALOG_ID = 2;
67     private static final int INVALIDATE_AUTH_TOKEN_DIALOG_ID = 3;
68     private static final int TEST_HAS_FEATURES_DIALOG_ID = 4;
69     private static final int MESSAGE_DIALOG_ID = 5;
70     private static final int GET_AUTH_TOKEN_BY_TYPE_AND_FEATURE_DIALOG_ID = 6;
71 
72     private EditText mDesiredAuthTokenTypeEditText;
73     private EditText mDesiredFeaturesEditText;
74     private volatile CharSequence mDialogMessage;
75 
76     @Override
onCreate(Bundle savedInstanceState)77     protected void onCreate(Bundle savedInstanceState) {
78         super.onCreate(savedInstanceState);
79         mAccountManager = AccountManager.get(this);
80         setContentView(R.layout.accounts_tester);
81         ButtonClickListener buttonClickListener = new ButtonClickListener();
82 
83         mAccountTypesSpinner = (Spinner) findViewById(R.id.accounts_tester_account_types_spinner);
84         mAccountsListView = (ListView) findViewById(R.id.accounts_tester_accounts_list);
85         registerForContextMenu(mAccountsListView);
86         initializeAuthenticatorsSpinner();
87         findViewById(R.id.accounts_tester_get_all_accounts).setOnClickListener(buttonClickListener);
88         findViewById(R.id.accounts_tester_get_accounts_by_type).setOnClickListener(
89                 buttonClickListener);
90         findViewById(R.id.accounts_tester_add_account).setOnClickListener(buttonClickListener);
91         findViewById(R.id.accounts_tester_edit_properties).setOnClickListener(buttonClickListener);
92         findViewById(R.id.accounts_tester_get_auth_token_by_type_and_feature).setOnClickListener(
93                 buttonClickListener);
94         mDesiredAuthTokenTypeEditText =
95                 (EditText) findViewById(R.id.accounts_tester_desired_authtokentype);
96         mDesiredFeaturesEditText = (EditText) findViewById(R.id.accounts_tester_desired_features);
97     }
98 
99     private class AccountArrayAdapter extends ArrayAdapter<Account> {
100         protected LayoutInflater mInflater;
101 
AccountArrayAdapter(Context context, Account[] accounts)102         public AccountArrayAdapter(Context context, Account[] accounts) {
103             super(context, R.layout.account_list_item, accounts);
104             mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
105         }
106 
107         class ViewHolder {
108             TextView name;
109             ImageView icon;
110             Account account;
111         }
112 
113         @Override
getView(int position, View convertView, ViewGroup parent)114         public View getView(int position, View convertView, ViewGroup parent) {
115             // A ViewHolder keeps references to children views to avoid unneccessary calls
116             // to findViewById() on each row.
117             ViewHolder holder;
118 
119             // When convertView is not null, we can reuse it directly, there is no need
120             // to reinflate it. We only inflate a new View when the convertView supplied
121             // by ListView is null.
122             if (convertView == null) {
123                 convertView = mInflater.inflate(R.layout.account_list_item, null);
124 
125                 // Creates a ViewHolder and store references to the two children views
126                 // we want to bind data to.
127                 holder = new ViewHolder();
128                 holder.name = (TextView) convertView.findViewById(
129                         R.id.accounts_tester_account_name);
130                 holder.icon = (ImageView) convertView.findViewById(
131                         R.id.accounts_tester_account_type_icon);
132 
133                 convertView.setTag(holder);
134             } else {
135                 // Get the ViewHolder back to get fast access to the TextView
136                 // and the ImageView.
137                 holder = (ViewHolder) convertView.getTag();
138             }
139 
140             final Account account = getItem(position);
141             holder.account = account;
142             holder.icon.setVisibility(View.INVISIBLE);
143             for (AuthenticatorDescription desc : mAuthenticatorDescs) {
144                 if (desc.type.equals(account.type)) {
145                     final String packageName = desc.packageName;
146                     try {
147                         final Context authContext = getContext().createPackageContext(packageName,
148                                 0);
149                         holder.icon.setImageDrawable(authContext.getResources().getDrawable(
150                                 desc.iconId));
151                         holder.icon.setVisibility(View.VISIBLE);
152                     } catch (PackageManager.NameNotFoundException e) {
153                         Log.d(TAG, "error getting the Package Context for " + packageName, e);
154                     }
155                 }
156             }
157 
158             // Set text field
159             holder.name.setText(account.name);
160             return convertView;
161         }
162     }
163 
initializeAuthenticatorsSpinner()164     private void initializeAuthenticatorsSpinner() {
165         mAuthenticatorDescs = mAccountManager.getAuthenticatorTypes();
166         List<String> names = new ArrayList(mAuthenticatorDescs.length);
167         for (int i = 0; i < mAuthenticatorDescs.length; i++) {
168             Context authContext;
169             try {
170                 authContext = createPackageContext(mAuthenticatorDescs[i].packageName, 0);
171             } catch (PackageManager.NameNotFoundException e) {
172                 continue;
173             }
174             try  {
175                 names.add(authContext.getString(mAuthenticatorDescs[i].labelId));
176             } catch (Resources.NotFoundException e) {
177                 continue;
178             }
179         }
180 
181         String[] namesArray = names.toArray(new String[names.size()]);
182         ArrayAdapter<String> adapter =
183                 new ArrayAdapter<String>(AccountsTester.this,
184                 android.R.layout.simple_spinner_item, namesArray);
185         mAccountTypesSpinner.setAdapter(adapter);
186     }
187 
onAccountsUpdated(Account[] accounts)188     public void onAccountsUpdated(Account[] accounts) {
189         Log.d(TAG, "onAccountsUpdated: \n  " + TextUtils.join("\n  ", accounts));
190         mAccountsListView.setAdapter(new AccountArrayAdapter(this, accounts));
191     }
192 
onStart()193     protected void onStart() {
194         super.onStart();
195         final Handler mainHandler = new Handler(getMainLooper());
196         mAccountManager.addOnAccountsUpdatedListener(this, mainHandler,
197                 true /* updateImmediately */);
198     }
199 
onStop()200     protected void onStop() {
201         super.onStop();
202         mAccountManager.removeOnAccountsUpdatedListener(this);
203     }
204 
205     class ButtonClickListener implements View.OnClickListener {
onClick(View v)206         public void onClick(View v) {
207             if (R.id.accounts_tester_get_all_accounts == v.getId()) {
208                 onAccountsUpdated(mAccountManager.getAccounts());
209             } else if (R.id.accounts_tester_get_accounts_by_type == v.getId()) {
210                 String type = getSelectedAuthenticator().type;
211                 onAccountsUpdated(mAccountManager.getAccountsByType(type));
212             } else if (R.id.accounts_tester_add_account == v.getId()) {
213                 String authTokenType = mDesiredAuthTokenTypeEditText.getText().toString();
214                 if (TextUtils.isEmpty(authTokenType)) {
215                     authTokenType = null;
216                 }
217                 String featureString = mDesiredFeaturesEditText.getText().toString();
218                 String[] requiredFeatures = TextUtils.split(featureString, " ");
219                 if (requiredFeatures.length == 0) {
220                     requiredFeatures = null;
221                 }
222                 mAccountManager.addAccount(getSelectedAuthenticator().type,
223                         authTokenType, requiredFeatures, null /* options */,
224                         AccountsTester.this,
225                         new CallbackToDialog(AccountsTester.this, "add account"),
226                         null /* handler */);
227             } else if (R.id.accounts_tester_edit_properties == v.getId()) {
228                 mAccountManager.editProperties(getSelectedAuthenticator().type,
229                         AccountsTester.this,
230                         new CallbackToDialog(AccountsTester.this, "edit properties"),
231                         null /* handler */);
232             } else if (R.id.accounts_tester_get_auth_token_by_type_and_feature == v.getId()) {
233                 showDialog(GET_AUTH_TOKEN_BY_TYPE_AND_FEATURE_DIALOG_ID);
234             } else {
235                 // unknown button
236             }
237         }
238     }
239 
getSelectedAuthenticator()240     private AuthenticatorDescription getSelectedAuthenticator() {
241         return mAuthenticatorDescs[mAccountTypesSpinner.getSelectedItemPosition()];
242     }
243 
244     @Override
onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo)245     public void onCreateContextMenu(ContextMenu menu, View v,
246             ContextMenu.ContextMenuInfo menuInfo) {
247         menu.setHeaderTitle(R.string.accounts_tester_account_context_menu_title);
248 
249         AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;
250 
251         MenuInflater inflater = getMenuInflater();
252         inflater.inflate(R.layout.account_list_context_menu, menu);
253         AccountArrayAdapter.ViewHolder holder =
254                 (AccountArrayAdapter.ViewHolder)info.targetView.getTag();
255         mLongPressedAccount = holder.account;
256     }
257 
onSaveInstanceState(Bundle outState)258     protected void onSaveInstanceState(Bundle outState) {
259         outState.putParcelable("account", mLongPressedAccount);
260     }
261 
onRestoreInstanceState(Bundle savedInstanceState)262     protected void onRestoreInstanceState(Bundle savedInstanceState) {
263         mLongPressedAccount = savedInstanceState.getParcelable("account");
264     }
265 
266     @Override
onContextItemSelected(MenuItem item)267     public boolean onContextItemSelected(MenuItem item) {
268         if (item.getItemId() == R.id.accounts_tester_remove_account) {
269             final Account account = mLongPressedAccount;
270             mAccountManager.removeAccount(account, new AccountManagerCallback<Boolean>() {
271                 public void run(AccountManagerFuture<Boolean> future) {
272                     try {
273                         Log.d(TAG, "removeAccount(" + account + ") = " + future.getResult());
274                     } catch (OperationCanceledException e) {
275                     } catch (IOException e) {
276                     } catch (AuthenticatorException e) {
277                     }
278                 }
279             }, null /* handler */);
280         } else if (item.getItemId() == R.id.accounts_tester_clear_password) {
281             final Account account = mLongPressedAccount;
282             mAccountManager.clearPassword(account);
283             showMessageDialog("cleared");
284         } else if (item.getItemId() == R.id.accounts_tester_get_auth_token) {
285             showDialog(GET_AUTH_TOKEN_DIALOG_ID);
286         } else if (item.getItemId() == R.id.accounts_tester_test_has_features) {
287             showDialog(TEST_HAS_FEATURES_DIALOG_ID);
288         } else if (item.getItemId() == R.id.accounts_tester_invalidate_auth_token) {
289             showDialog(INVALIDATE_AUTH_TOKEN_DIALOG_ID);
290         } else if (item.getItemId() == R.id.accounts_tester_update_credentials) {
291             showDialog(UPDATE_CREDENTIALS_DIALOG_ID);
292         } else if (item.getItemId() == R.id.accounts_tester_confirm_credentials) {
293             mAccountManager.confirmCredentials(mLongPressedAccount, null,
294                     AccountsTester.this, new CallbackToDialog(this, "confirm credentials"),
295                     null /* handler */);
296         }
297         return true;
298     }
299 
300     @Override
onCreateDialog(final int id)301     protected Dialog onCreateDialog(final int id) {
302         switch (id) {
303             case GET_AUTH_TOKEN_DIALOG_ID:
304             case INVALIDATE_AUTH_TOKEN_DIALOG_ID:
305             case UPDATE_CREDENTIALS_DIALOG_ID:
306             case TEST_HAS_FEATURES_DIALOG_ID: {
307                 final View view = LayoutInflater.from(this).inflate(R.layout.get_auth_token_view,
308                         null);
309                 AlertDialog.Builder builder = new AlertDialog.Builder(this);
310                 builder.setPositiveButton(R.string.accounts_tester_ok_button,
311                         new DialogInterface.OnClickListener() {
312                             public void onClick(DialogInterface dialog, int which) {
313                                 EditText value = (EditText) view.findViewById(
314                                         R.id.accounts_tester_auth_token_type);
315 
316                                 String authTokenType = value.getText().toString();
317                                 final Account account = mLongPressedAccount;
318                                 if (id == GET_AUTH_TOKEN_DIALOG_ID) {
319                                     mAccountManager.getAuthToken(account,
320                                             authTokenType,
321                                             null /* loginOptions */,
322                                             AccountsTester.this,
323                                             new CallbackToDialog(AccountsTester.this,
324                                                     "get auth token"),
325                                             null /* handler */);
326                                 } else if (id == INVALIDATE_AUTH_TOKEN_DIALOG_ID) {
327                                     mAccountManager.getAuthToken(account, authTokenType, false,
328                                             new GetAndInvalidateAuthTokenCallback(account), null);
329                                 } else if (id == TEST_HAS_FEATURES_DIALOG_ID) {
330                                     String[] features = TextUtils.split(authTokenType, ",");
331                                     mAccountManager.hasFeatures(account, features,
332                                             new TestHasFeaturesCallback(), null);
333                                 } else {
334                                     mAccountManager.updateCredentials(
335                                             account,
336                                             authTokenType, null /* loginOptions */,
337                                             AccountsTester.this,
338                                             new CallbackToDialog(AccountsTester.this, "update"),
339                                             null /* handler */);
340                                 }
341                             }
342                 });
343                 builder.setView(view);
344                 return builder.create();
345             }
346 
347             case GET_AUTH_TOKEN_BY_TYPE_AND_FEATURE_DIALOG_ID: {
348                 final View view = LayoutInflater.from(this).inflate(R.layout.get_features_view,
349                         null);
350                 AlertDialog.Builder builder = new AlertDialog.Builder(this);
351                 builder.setPositiveButton(R.string.accounts_tester_ok_button,
352                         new DialogInterface.OnClickListener() {
353                             public void onClick(DialogInterface dialog, int which) {
354                                 EditText value = (EditText) view.findViewById(
355                                         R.id.accounts_tester_auth_token_type);
356 
357                                 String authTokenType = value.getText().toString();
358 
359                                 value = (EditText) view.findViewById(
360                                         R.id.accounts_tester_features);
361 
362                                 String features = value.getText().toString();
363 
364                                 final Account account = mLongPressedAccount;
365                                 mAccountManager.getAuthTokenByFeatures(
366                                         getSelectedAuthenticator().type,
367                                         authTokenType,
368                                         TextUtils.isEmpty(features) ? null : features.split(" "),
369                                         AccountsTester.this,
370                                         null /* addAccountOptions */,
371                                         null /* getAuthTokenOptions */,
372                                         new CallbackToDialog(AccountsTester.this,
373                                                 "get auth token by features"),
374                                         null /* handler */);
375                             }
376                 });
377                 builder.setView(view);
378                 return builder.create();
379             }
380 
381             case MESSAGE_DIALOG_ID: {
382                 AlertDialog.Builder builder = new AlertDialog.Builder(this);
383                 builder.setMessage(mDialogMessage);
384                 return builder.create();
385             }
386         }
387 
388         return super.onCreateDialog(id);
389     }
390 
newAccountsCallback(String type, String[] features)391     AccountManagerCallback<Bundle> newAccountsCallback(String type, String[] features) {
392         return new GetAccountsCallback(type, features);
393     }
394 
395     class GetAccountsCallback implements AccountManagerCallback<Bundle> {
396         final String[] mFeatures;
397         final String mAccountType;
398 
GetAccountsCallback(String type, String[] features)399         public GetAccountsCallback(String type, String[] features) {
400             mFeatures = features;
401             mAccountType = type;
402         }
403 
run(AccountManagerFuture<Bundle> future)404         public void run(AccountManagerFuture<Bundle> future) {
405             Log.d(TAG, "GetAccountsCallback: type " + mAccountType
406                     + ", features "
407                     + (mFeatures == null ? "none" : TextUtils.join(",", mFeatures)));
408             try {
409                 Bundle result = future.getResult();
410                 Parcelable[] accounts = result.getParcelableArray(AccountManager.KEY_ACCOUNTS);
411                 Log.d(TAG, "found " + accounts.length + " accounts");
412                 for (Parcelable account : accounts) {
413                     Log.d(TAG, "  " + account);
414                 }
415             } catch (OperationCanceledException e) {
416                 Log.d(TAG, "failure", e);
417             } catch (IOException e) {
418                 Log.d(TAG, "failure", e);
419             } catch (AuthenticatorException e) {
420                 Log.d(TAG, "failure", e);
421             }
422         }
423     }
424 
newAuthTokensCallback(String type, String authTokenType, String[] features)425     AccountManagerCallback<Bundle> newAuthTokensCallback(String type, String authTokenType,
426             String[] features) {
427         return new GetAuthTokenCallback(type, authTokenType, features);
428     }
429 
430     class GetAuthTokenCallback implements AccountManagerCallback<Bundle> {
431         final String[] mFeatures;
432         final String mAccountType;
433         final String mAuthTokenType;
434 
GetAuthTokenCallback(String type, String authTokenType, String[] features)435         public GetAuthTokenCallback(String type, String authTokenType, String[] features) {
436             mFeatures = features;
437             mAccountType = type;
438             mAuthTokenType = authTokenType;
439         }
440 
run(AccountManagerFuture<Bundle> future)441         public void run(AccountManagerFuture<Bundle> future) {
442             Log.d(TAG, "GetAuthTokenCallback: type " + mAccountType
443                     + ", features "
444                     + (mFeatures == null ? "none" : TextUtils.join(",", mFeatures)));
445             getAndLogResult(future, "get auth token");
446         }
447     }
448 
449     private class GetAndInvalidateAuthTokenCallback implements AccountManagerCallback<Bundle> {
450         private final Account mAccount;
451 
GetAndInvalidateAuthTokenCallback(Account account)452         private GetAndInvalidateAuthTokenCallback(Account account) {
453             mAccount = account;
454         }
455 
run(AccountManagerFuture<Bundle> future)456         public void run(AccountManagerFuture<Bundle> future) {
457             Bundle result = getAndLogResult(future, "get and invalidate");
458             if (result != null) {
459                 String authToken = result.getString(AccountManager.KEY_AUTHTOKEN);
460                 mAccountManager.invalidateAuthToken(mAccount.type, authToken);
461             }
462         }
463     }
464 
showMessageDialog(String message)465     private void showMessageDialog(String message) {
466         mDialogMessage = message;
467         removeDialog(MESSAGE_DIALOG_ID);
468         showDialog(MESSAGE_DIALOG_ID);
469     }
470 
471     private class TestHasFeaturesCallback implements AccountManagerCallback<Boolean> {
run(AccountManagerFuture<Boolean> future)472         public void run(AccountManagerFuture<Boolean> future) {
473             try {
474                 Boolean hasFeatures = future.getResult();
475                 Log.d(TAG, "hasFeatures: " + hasFeatures);
476                 showMessageDialog("hasFeatures: " + hasFeatures);
477             } catch (OperationCanceledException e) {
478                 Log.d(TAG, "interrupted");
479                 showMessageDialog("operation was canceled");
480             } catch (IOException e) {
481                 Log.d(TAG, "error", e);
482                 showMessageDialog("operation got an IOException");
483             } catch (AuthenticatorException e) {
484                 Log.d(TAG, "error", e);
485                 showMessageDialog("operation got an AuthenticationException");
486             }
487         }
488     }
489 
490     private static class CallbackToDialog implements AccountManagerCallback<Bundle> {
491         private final AccountsTester mActivity;
492         private final String mLabel;
493 
CallbackToDialog(AccountsTester activity, String label)494         private CallbackToDialog(AccountsTester activity, String label) {
495             mActivity = activity;
496             mLabel = label;
497         }
498 
run(AccountManagerFuture<Bundle> future)499         public void run(AccountManagerFuture<Bundle> future) {
500             mActivity.getAndLogResult(future, mLabel);
501         }
502     }
503 
getAndLogResult(AccountManagerFuture<Bundle> future, String label)504     private Bundle getAndLogResult(AccountManagerFuture<Bundle> future, String label) {
505         try {
506             Bundle result = future.getResult();
507             result.keySet();
508             Log.d(TAG, label + ": " + result);
509             StringBuffer sb = new StringBuffer();
510             sb.append(label).append(" result:");
511             for (String key : result.keySet()) {
512                 Object value = result.get(key);
513                 if (AccountManager.KEY_AUTHTOKEN.equals(key)) {
514                     value = "<redacted>";
515                 }
516                 sb.append("\n  ").append(key).append(" -> ").append(value);
517             }
518             showMessageDialog(sb.toString());
519             return result;
520         } catch (OperationCanceledException e) {
521             Log.d(TAG, label + " failed", e);
522             showMessageDialog(label + " was canceled");
523             return null;
524         } catch (IOException e) {
525             Log.d(TAG, label + " failed", e);
526             showMessageDialog(label + " failed with IOException: " + e.getMessage());
527             return null;
528         } catch (AuthenticatorException e) {
529             Log.d(TAG, label + " failed", e);
530             showMessageDialog(label + " failed with an AuthenticatorException: " + e.getMessage());
531             return null;
532         }
533     }
534 }
535