ChooseTypeAndAccountActivity.java revision 8d16778efdf46fa80a309456b722b333ec3e6756
1/*
2 * Copyright (C) 2011 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 */
16package android.accounts;
17
18import com.google.android.collect.Sets;
19
20import android.app.Activity;
21import android.content.Intent;
22import android.os.Bundle;
23import android.os.Parcelable;
24import android.text.TextUtils;
25import android.util.Log;
26import android.view.View;
27import android.widget.AdapterView;
28import android.widget.ArrayAdapter;
29import android.widget.Button;
30import android.widget.ListView;
31import android.widget.TextView;
32
33import com.android.internal.R;
34
35import java.io.IOException;
36import java.util.ArrayList;
37import java.util.HashMap;
38import java.util.HashSet;
39import java.util.Set;
40
41/**
42 * @hide
43 */
44public class ChooseTypeAndAccountActivity extends Activity
45        implements AccountManagerCallback<Bundle> {
46    private static final String TAG = "AccountChooser";
47
48    /**
49     * A Parcelable ArrayList of Account objects that limits the choosable accounts to those
50     * in this list, if this parameter is supplied.
51     */
52    public static final String EXTRA_ALLOWABLE_ACCOUNTS_ARRAYLIST = "allowableAccounts";
53
54    /**
55     * A Parcelable ArrayList of String objects that limits the accounts to choose to those
56     * that match the types in this list, if this parameter is supplied. This list is also
57     * used to filter the allowable account types if add account is selected.
58     */
59    public static final String EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY = "allowableAccountTypes";
60
61    /**
62     * This is passed as the addAccountOptions parameter in AccountManager.addAccount()
63     * if it is called.
64     */
65    public static final String EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE = "addAccountOptions";
66
67    /**
68     * This is passed as the requiredFeatures parameter in AccountManager.addAccount()
69     * if it is called.
70     */
71    public static final String EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY =
72            "addAccountRequiredFeatures";
73
74    /**
75     * This is passed as the authTokenType string in AccountManager.addAccount()
76     * if it is called.
77     */
78    public static final String EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING = "authTokenType";
79
80    /**
81     * If set then the specified account is already "selected".
82     */
83    public static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
84
85    /**
86     * If true then display the account selection list even if there is just
87     * one account to choose from. boolean.
88     */
89    public static final String EXTRA_ALWAYS_PROMPT_FOR_ACCOUNT =
90            "alwaysPromptForAccount";
91
92    /**
93     * If set then this string willb e used as the description rather than
94     * the default.
95     */
96    public static final String EXTRA_DESCRIPTION_TEXT_OVERRIDE =
97            "descriptionTextOverride";
98
99    public static final int REQUEST_NULL = 0;
100    public static final int REQUEST_CHOOSE_TYPE = 1;
101    public static final int REQUEST_ADD_ACCOUNT = 2;
102
103    private static final String KEY_INSTANCE_STATE_PENDING_REQUEST = "pendingRequest";
104    private static final String KEY_INSTANCE_STATE_EXISTING_ACCOUNTS = "existingAccounts";
105    private static final String KEY_INSTANCE_STATE_SELECTED_ACCOUNT_NAME = "selectedAccountName";
106    private static final String KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT = "selectedAddAccount";
107
108    private static final int SELECTED_ITEM_NONE = -1;
109
110    private Set<Account> mSetOfAllowableAccounts;
111    private Set<String> mSetOfRelevantAccountTypes;
112    private String mSelectedAccountName = null;
113    private boolean mSelectedAddNewAccount = false;
114    private boolean mAlwaysPromptForAccount = false;
115    private String mDescriptionOverride;
116
117    private ArrayList<Account> mAccounts;
118    private int mPendingRequest = REQUEST_NULL;
119    private Parcelable[] mExistingAccounts = null;
120    private int mSelectedItemIndex;
121    private Button mOkButton;
122
123    @Override
124    public void onCreate(Bundle savedInstanceState) {
125        super.onCreate(savedInstanceState);
126        if (Log.isLoggable(TAG, Log.VERBOSE)) {
127            Log.v(TAG, "ChooseTypeAndAccountActivity.onCreate(savedInstanceState="
128                    + savedInstanceState + ")");
129        }
130
131        // save some items we use frequently
132        final Intent intent = getIntent();
133
134        if (savedInstanceState != null) {
135            mPendingRequest = savedInstanceState.getInt(KEY_INSTANCE_STATE_PENDING_REQUEST);
136            mExistingAccounts =
137                    savedInstanceState.getParcelableArray(KEY_INSTANCE_STATE_EXISTING_ACCOUNTS);
138
139            // Makes sure that any user selection is preserved across orientation changes.
140            mSelectedAccountName = savedInstanceState.getString(
141                    KEY_INSTANCE_STATE_SELECTED_ACCOUNT_NAME);
142
143            mSelectedAddNewAccount = savedInstanceState.getBoolean(
144                    KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT, false);
145        } else {
146            mPendingRequest = REQUEST_NULL;
147            mExistingAccounts = null;
148            // If the selected account as specified in the intent matches one in the list we will
149            // show is as pre-selected.
150            Account selectedAccount = (Account) intent.getParcelableExtra(EXTRA_SELECTED_ACCOUNT);
151            if (selectedAccount != null) {
152                mSelectedAccountName = selectedAccount.name;
153            }
154        }
155
156        if (Log.isLoggable(TAG, Log.VERBOSE)) {
157            Log.v(TAG, "selected account name is " + mSelectedAccountName);
158        }
159
160
161        mSetOfAllowableAccounts = getAllowableAccountSet(intent);
162        mSetOfRelevantAccountTypes = getReleventAccountTypes(intent);
163        mAlwaysPromptForAccount = intent.getBooleanExtra(EXTRA_ALWAYS_PROMPT_FOR_ACCOUNT, false);
164        mDescriptionOverride = intent.getStringExtra(EXTRA_DESCRIPTION_TEXT_OVERRIDE);
165    }
166
167    @Override
168    protected void onResume() {
169        super.onResume();
170        final AccountManager accountManager = AccountManager.get(this);
171
172        mAccounts = getAcceptableAccountChoices(accountManager);
173
174        // In cases where the activity does not need to show an account picker, cut the chase
175        // and return the result directly. Eg:
176        // Single account -> select it directly
177        // No account -> launch add account activity directly
178        if (mPendingRequest == REQUEST_NULL) {
179            // If there are no relevant accounts and only one relevant account type go directly to
180            // add account. Otherwise let the user choose.
181            if (mAccounts.isEmpty()) {
182                if (mSetOfRelevantAccountTypes.size() == 1) {
183                    runAddAccountForAuthenticator(mSetOfRelevantAccountTypes.iterator().next());
184                } else {
185                    startChooseAccountTypeActivity();
186                }
187                return;
188            }
189
190            // if there is only one allowable account return it
191            if (!mAlwaysPromptForAccount && mAccounts.size() == 1) {
192                Account account = mAccounts.get(0);
193                setResultAndFinish(account.name, account.type);
194                return;
195            }
196        }
197
198        String[] listItems = getListOfDisplayableOptions(mAccounts);
199        mSelectedItemIndex = getItemIndexToSelect(
200            mAccounts, mSelectedAccountName, mSelectedAddNewAccount);
201
202        // Cannot set content view until we know that mPendingRequest is not null, otherwise
203        // would cause screen flicker.
204        setContentView(R.layout.choose_type_and_account);
205        overrideDescriptionIfSupplied(mDescriptionOverride);
206        populateUIAccountList(listItems);
207
208        // Only enable "OK" button if something has been selected.
209        mOkButton = (Button) findViewById(android.R.id.button2);
210        mOkButton.setEnabled(mSelectedItemIndex != SELECTED_ITEM_NONE);
211    }
212
213    @Override
214    protected void onDestroy() {
215        if (Log.isLoggable(TAG, Log.VERBOSE)) {
216            Log.v(TAG, "ChooseTypeAndAccountActivity.onDestroy()");
217        }
218        super.onDestroy();
219    }
220
221    @Override
222    protected void onSaveInstanceState(final Bundle outState) {
223        super.onSaveInstanceState(outState);
224        outState.putInt(KEY_INSTANCE_STATE_PENDING_REQUEST, mPendingRequest);
225        if (mPendingRequest == REQUEST_ADD_ACCOUNT) {
226            outState.putParcelableArray(KEY_INSTANCE_STATE_EXISTING_ACCOUNTS, mExistingAccounts);
227        }
228        if (mSelectedItemIndex != SELECTED_ITEM_NONE) {
229            if (mSelectedItemIndex == mAccounts.size()) {
230                outState.putBoolean(KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT, true);
231            } else {
232                outState.putBoolean(KEY_INSTANCE_STATE_SELECTED_ADD_ACCOUNT, false);
233                outState.putString(KEY_INSTANCE_STATE_SELECTED_ACCOUNT_NAME,
234                        mAccounts.get(mSelectedItemIndex).name);
235            }
236        }
237    }
238
239    public void onCancelButtonClicked(View view) {
240        onBackPressed();
241    }
242
243    public void onOkButtonClicked(View view) {
244        if (mSelectedItemIndex == mAccounts.size()) {
245            // Selected "Add New Account" option
246            startChooseAccountTypeActivity();
247        } else if (mSelectedItemIndex != SELECTED_ITEM_NONE) {
248            onAccountSelected(mAccounts.get(mSelectedItemIndex));
249        }
250    }
251
252    // Called when the choose account type activity (for adding an account) returns.
253    // If it was a success read the account and set it in the result. In all cases
254    // return the result and finish this activity.
255    @Override
256    protected void onActivityResult(final int requestCode, final int resultCode,
257            final Intent data) {
258        if (Log.isLoggable(TAG, Log.VERBOSE)) {
259            if (data != null && data.getExtras() != null) data.getExtras().keySet();
260            Bundle extras = data != null ? data.getExtras() : null;
261            Log.v(TAG, "ChooseTypeAndAccountActivity.onActivityResult(reqCode=" + requestCode
262                    + ", resCode=" + resultCode + ", extras=" + extras + ")");
263        }
264
265        // we got our result, so clear the fact that we had a pending request
266        mPendingRequest = REQUEST_NULL;
267
268        if (resultCode == RESULT_CANCELED) {
269            // if canceling out of addAccount and the original state caused us to skip this,
270            // finish this activity
271            if (mAccounts.isEmpty()) {
272                setResult(Activity.RESULT_CANCELED);
273                finish();
274            }
275            return;
276        }
277
278        if (resultCode == RESULT_OK) {
279            if (requestCode == REQUEST_CHOOSE_TYPE) {
280                if (data != null) {
281                    String accountType = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
282                    if (accountType != null) {
283                        runAddAccountForAuthenticator(accountType);
284                        return;
285                    }
286                }
287                Log.d(TAG, "ChooseTypeAndAccountActivity.onActivityResult: unable to find account "
288                        + "type, pretending the request was canceled");
289            } else if (requestCode == REQUEST_ADD_ACCOUNT) {
290                String accountName = null;
291                String accountType = null;
292
293                if (data != null) {
294                    accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
295                    accountType = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
296                }
297
298                if (accountName == null || accountType == null) {
299                    Account[] currentAccounts = AccountManager.get(this).getAccounts();
300                    Set<Account> preExistingAccounts = new HashSet<Account>();
301                    for (Parcelable accountParcel : mExistingAccounts) {
302                        preExistingAccounts.add((Account) accountParcel);
303                    }
304                    for (Account account : currentAccounts) {
305                        if (!preExistingAccounts.contains(account)) {
306                            accountName = account.name;
307                            accountType = account.type;
308                            break;
309                        }
310                    }
311                }
312
313                if (accountName != null || accountType != null) {
314                    setResultAndFinish(accountName, accountType);
315                    return;
316                }
317            }
318            Log.d(TAG, "ChooseTypeAndAccountActivity.onActivityResult: unable to find added "
319                    + "account, pretending the request was canceled");
320        }
321        if (Log.isLoggable(TAG, Log.VERBOSE)) {
322            Log.v(TAG, "ChooseTypeAndAccountActivity.onActivityResult: canceled");
323        }
324        setResult(Activity.RESULT_CANCELED);
325        finish();
326    }
327
328    protected void runAddAccountForAuthenticator(String type) {
329        if (Log.isLoggable(TAG, Log.VERBOSE)) {
330            Log.v(TAG, "runAddAccountForAuthenticator: " + type);
331        }
332        final Bundle options = getIntent().getBundleExtra(
333                ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE);
334        final String[] requiredFeatures = getIntent().getStringArrayExtra(
335                ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY);
336        final String authTokenType = getIntent().getStringExtra(
337                ChooseTypeAndAccountActivity.EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING);
338        AccountManager.get(this).addAccount(type, authTokenType, requiredFeatures,
339                options, null /* activity */, this /* callback */, null /* Handler */);
340    }
341
342    @Override
343    public void run(final AccountManagerFuture<Bundle> accountManagerFuture) {
344        try {
345            final Bundle accountManagerResult = accountManagerFuture.getResult();
346            final Intent intent = (Intent)accountManagerResult.getParcelable(
347                    AccountManager.KEY_INTENT);
348            if (intent != null) {
349                mPendingRequest = REQUEST_ADD_ACCOUNT;
350                mExistingAccounts = AccountManager.get(this).getAccounts();
351                intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
352                startActivityForResult(intent, REQUEST_ADD_ACCOUNT);
353                return;
354            }
355        } catch (OperationCanceledException e) {
356            setResult(Activity.RESULT_CANCELED);
357            finish();
358            return;
359        } catch (IOException e) {
360        } catch (AuthenticatorException e) {
361        }
362        Bundle bundle = new Bundle();
363        bundle.putString(AccountManager.KEY_ERROR_MESSAGE, "error communicating with server");
364        setResult(Activity.RESULT_OK, new Intent().putExtras(bundle));
365        finish();
366    }
367
368    private void onAccountSelected(Account account) {
369      Log.d(TAG, "selected account " + account);
370      setResultAndFinish(account.name, account.type);
371    }
372
373    private void setResultAndFinish(final String accountName, final String accountType) {
374        Bundle bundle = new Bundle();
375        bundle.putString(AccountManager.KEY_ACCOUNT_NAME, accountName);
376        bundle.putString(AccountManager.KEY_ACCOUNT_TYPE, accountType);
377        setResult(Activity.RESULT_OK, new Intent().putExtras(bundle));
378        if (Log.isLoggable(TAG, Log.VERBOSE)) {
379            Log.v(TAG, "ChooseTypeAndAccountActivity.setResultAndFinish: "
380                    + "selected account " + accountName + ", " + accountType);
381        }
382        finish();
383    }
384
385    private void startChooseAccountTypeActivity() {
386        if (Log.isLoggable(TAG, Log.VERBOSE)) {
387            Log.v(TAG, "ChooseAccountTypeActivity.startChooseAccountTypeActivity()");
388        }
389        final Intent intent = new Intent(this, ChooseAccountTypeActivity.class);
390        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
391        intent.putExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY,
392                getIntent().getStringArrayExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY));
393        intent.putExtra(EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE,
394                getIntent().getBundleExtra(EXTRA_ADD_ACCOUNT_OPTIONS_BUNDLE));
395        intent.putExtra(EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY,
396                getIntent().getStringArrayExtra(EXTRA_ADD_ACCOUNT_REQUIRED_FEATURES_STRING_ARRAY));
397        intent.putExtra(EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING,
398                getIntent().getStringExtra(EXTRA_ADD_ACCOUNT_AUTH_TOKEN_TYPE_STRING));
399        startActivityForResult(intent, REQUEST_CHOOSE_TYPE);
400        mPendingRequest = REQUEST_CHOOSE_TYPE;
401    }
402
403    /**
404     * @return a value between 0 (inclusive) and accounts.size() (inclusive) or SELECTED_ITEM_NONE.
405     *      An index value of accounts.size() indicates 'Add account' option.
406     */
407    private int getItemIndexToSelect(ArrayList<Account> accounts, String selectedAccountName,
408        boolean selectedAddNewAccount) {
409      // If "Add account" option was previously selected by user, preserve it across
410      // orientation changes.
411      if (selectedAddNewAccount) {
412          return accounts.size();
413      }
414      // search for the selected account name if present
415      for (int i = 0; i < accounts.size(); i++) {
416        if (accounts.get(i).name.equals(selectedAccountName)) {
417          return i;
418        }
419      }
420      // no account selected.
421      return SELECTED_ITEM_NONE;
422    }
423
424    private String[] getListOfDisplayableOptions(ArrayList<Account> accounts) {
425      // List of options includes all accounts found together with "Add new account" as the
426      // last item in the list.
427      String[] listItems = new String[accounts.size() + 1];
428      for (int i = 0; i < accounts.size(); i++) {
429          listItems[i] = accounts.get(i).name;
430      }
431      listItems[accounts.size()] = getResources().getString(
432              R.string.add_account_button_label);
433      return listItems;
434    }
435
436    /**
437     * Create a list of Account objects for each account that is acceptable. Filter out
438     * accounts that don't match the allowable types, if provided, or that don't match the
439     * allowable accounts, if provided.
440     */
441    private ArrayList<Account> getAcceptableAccountChoices(AccountManager accountManager) {
442      final Account[] accounts = accountManager.getAccounts();
443      ArrayList<Account> accountsToPopulate = new ArrayList<Account>(accounts.length);
444      for (Account account : accounts) {
445          if (mSetOfAllowableAccounts != null
446                  && !mSetOfAllowableAccounts.contains(account)) {
447              continue;
448          }
449          if (mSetOfRelevantAccountTypes != null
450                  && !mSetOfRelevantAccountTypes.contains(account.type)) {
451              continue;
452          }
453          accountsToPopulate.add(account);
454      }
455      return accountsToPopulate;
456    }
457
458    /**
459     * Return a set of account types speficied by the intent as well as supported by the
460     * AccountManager.
461     */
462    private Set<String> getReleventAccountTypes(final Intent intent) {
463      // An account type is relevant iff it is allowed by the caller and supported by the account
464      // manager.
465      Set<String> setOfRelevantAccountTypes = null;
466      final String[] allowedAccountTypes =
467              intent.getStringArrayExtra(EXTRA_ALLOWABLE_ACCOUNT_TYPES_STRING_ARRAY);
468      if (allowedAccountTypes != null) {
469          setOfRelevantAccountTypes = Sets.newHashSet(allowedAccountTypes);
470          AuthenticatorDescription[] descs = AccountManager.get(this).getAuthenticatorTypes();
471          Set<String> supportedAccountTypes = new HashSet<String>(descs.length);
472          for (AuthenticatorDescription desc : descs) {
473              supportedAccountTypes.add(desc.type);
474          }
475          setOfRelevantAccountTypes.retainAll(supportedAccountTypes);
476      }
477      return setOfRelevantAccountTypes;
478    }
479
480    /**
481     * Returns a set of whitelisted accounts given by the intent or null if none specified by the
482     * intent.
483     */
484    private Set<Account> getAllowableAccountSet(final Intent intent) {
485      Set<Account> setOfAllowableAccounts = null;
486      final ArrayList<Parcelable> validAccounts =
487              intent.getParcelableArrayListExtra(EXTRA_ALLOWABLE_ACCOUNTS_ARRAYLIST);
488      if (validAccounts != null) {
489          setOfAllowableAccounts = new HashSet<Account>(validAccounts.size());
490          for (Parcelable parcelable : validAccounts) {
491              setOfAllowableAccounts.add((Account)parcelable);
492          }
493      }
494      return setOfAllowableAccounts;
495    }
496
497    /**
498     * Overrides the description text view for the picker activity if specified by the intent.
499     * If not specified then makes the description invisible.
500     */
501    private void overrideDescriptionIfSupplied(String descriptionOverride) {
502      TextView descriptionView = (TextView) findViewById(R.id.description);
503      if (!TextUtils.isEmpty(descriptionOverride)) {
504          descriptionView.setText(descriptionOverride);
505      } else {
506          descriptionView.setVisibility(View.GONE);
507      }
508    }
509
510    /**
511     * Populates the UI ListView with the given list of items and selects an item
512     * based on {@code mSelectedItemIndex} member variable.
513     */
514    private final void populateUIAccountList(String[] listItems) {
515      ListView list = (ListView) findViewById(android.R.id.list);
516      list.setAdapter(new ArrayAdapter<String>(this,
517              android.R.layout.simple_list_item_single_choice, listItems));
518      list.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
519      list.setItemsCanFocus(false);
520      list.setOnItemClickListener(
521              new AdapterView.OnItemClickListener() {
522                  @Override
523                  public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
524                      mSelectedItemIndex = position;
525                      mOkButton.setEnabled(true);
526                  }
527              });
528      if (mSelectedItemIndex != SELECTED_ITEM_NONE) {
529          list.setItemChecked(mSelectedItemIndex, true);
530          if (Log.isLoggable(TAG, Log.VERBOSE)) {
531              Log.v(TAG, "List item " + mSelectedItemIndex + " should be selected");
532          }
533      }
534    }
535}
536