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