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