AccountSettings.java revision 1bd4630d3d7e4ea309a126b05b00fb1ccab283b8
1/* 2 * Copyright (C) 2010 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 17package com.android.email.activity.setup; 18 19import android.app.ActionBar; 20import android.app.Activity; 21import android.app.AlertDialog; 22import android.app.Dialog; 23import android.app.DialogFragment; 24import android.app.Fragment; 25import android.content.Context; 26import android.content.DialogInterface; 27import android.content.Intent; 28import android.content.res.Resources; 29import android.database.ContentObserver; 30import android.database.Cursor; 31import android.net.Uri; 32import android.os.AsyncTask; 33import android.os.Bundle; 34import android.preference.PreferenceActivity; 35import android.text.SpannableString; 36import android.text.method.LinkMovementMethod; 37import android.text.util.Linkify; 38import android.util.Log; 39import android.util.TypedValue; 40import android.view.KeyEvent; 41import android.view.Menu; 42import android.view.MenuItem; 43import android.widget.TextView; 44 45import com.android.email.R; 46import com.android.email.activity.ActivityHelper; 47import com.android.email.mail.Sender; 48import com.android.email.provider.EmailProvider; 49import com.android.emailcommon.Logging; 50import com.android.emailcommon.provider.Account; 51import com.android.emailcommon.provider.EmailContent.AccountColumns; 52import com.android.emailcommon.utility.IntentUtilities; 53import com.android.emailcommon.utility.Utility; 54import com.android.mail.providers.Folder; 55import com.android.mail.providers.UIProvider.EditSettingsExtras; 56 57import java.util.List; 58 59/** 60 * Handles account preferences, using multi-pane arrangement when possible. 61 * 62 * This activity uses the following fragments: 63 * AccountSettingsFragment 64 * Account{Incoming/Outgoing}Fragment 65 * AccountCheckSettingsFragment 66 * GeneralPreferences 67 * DebugFragment 68 * 69 * TODO: Delete account - on single-pane view (phone UX) the account list doesn't update properly 70 * TODO: Handle dynamic changes to the account list (exit if necessary). It probably makes 71 * sense to use a loader for the accounts list, because it would provide better support for 72 * dealing with accounts being added/deleted and triggering the header reload. 73 */ 74public class AccountSettings extends PreferenceActivity { 75 /* 76 * Intent to open account settings for account=1 77 adb shell am start -a android.intent.action.EDIT \ 78 -d '"content://ui.email.android.com/settings?ACCOUNT_ID=1"' 79 */ 80 81 // Intent extras for our internal activity launch 82 private static final String EXTRA_ENABLE_DEBUG = "AccountSettings.enable_debug"; 83 private static final String EXTRA_LOGIN_WARNING_FOR_ACCOUNT = "AccountSettings.for_account"; 84 private static final String EXTRA_LOGIN_WARNING_REASON_FOR_ACCOUNT = 85 "AccountSettings.for_account_reason"; 86 private static final String EXTRA_TITLE = "AccountSettings.title"; 87 public static final String EXTRA_NO_ACCOUNTS = "AccountSettings.no_account"; 88 89 // Intent extras for launch directly from system account manager 90 // NOTE: This string must match the one in res/xml/account_preferences.xml 91 private static final String ACTION_ACCOUNT_MANAGER_ENTRY = 92 "com.android.email.activity.setup.ACCOUNT_MANAGER_ENTRY"; 93 // NOTE: This constant should eventually be defined in android.accounts.Constants 94 private static final String EXTRA_ACCOUNT_MANAGER_ACCOUNT = "account"; 95 96 // Key for arguments bundle for QuickResponse editing 97 private static final String QUICK_RESPONSE_ACCOUNT_KEY = "account"; 98 99 // Key codes used to open a debug settings fragment. 100 private static final int[] SECRET_KEY_CODES = { 101 KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_U, 102 KeyEvent.KEYCODE_G 103 }; 104 private int mSecretKeyCodeIndex = 0; 105 106 // Support for account-by-name lookup 107 private static final String SELECTION_ACCOUNT_EMAIL_ADDRESS = 108 AccountColumns.EMAIL_ADDRESS + "=?"; 109 110 // When the user taps "Email Preferences" 10 times in a row, we'll enable the debug settings. 111 private int mNumGeneralHeaderClicked = 0; 112 113 private long mRequestedAccountId; 114 private Header mRequestedAccountHeader; 115 private Header[] mAccountListHeaders; 116 private Header mAppPreferencesHeader; 117 /* package */ Fragment mCurrentFragment; 118 private long mDeletingAccountId = -1; 119 private boolean mShowDebugMenu; 120 private List<Header> mGeneratedHeaders; 121 122 // Async Tasks 123 private LoadAccountListTask mLoadAccountListTask; 124 private GetAccountIdFromAccountTask mGetAccountIdFromAccountTask; 125 private ContentObserver mAccountObserver; 126 127 // Specific callbacks used by settings fragments 128 private final AccountSettingsFragmentCallback mAccountSettingsFragmentCallback 129 = new AccountSettingsFragmentCallback(); 130 private final AccountServerSettingsFragmentCallback mAccountServerSettingsFragmentCallback 131 = new AccountServerSettingsFragmentCallback(); 132 133 /** 134 * Display (and edit) settings for a specific account, or -1 for any/all accounts 135 */ 136 public static void actionSettings(Activity fromActivity, long accountId) { 137 fromActivity.startActivity( 138 createAccountSettingsIntent(fromActivity, accountId, null, null)); 139 } 140 141 /** 142 * Create and return an intent to display (and edit) settings for a specific account, or -1 143 * for any/all accounts. If an account name string is provided, a warning dialog will be 144 * displayed as well. 145 */ 146 public static Intent createAccountSettingsIntent(Context context, long accountId, 147 String loginWarningAccountName, String loginWarningReason) { 148 final Uri.Builder b = IntentUtilities.createActivityIntentUrlBuilder("settings"); 149 IntentUtilities.setAccountId(b, accountId); 150 Intent i = new Intent(Intent.ACTION_EDIT, b.build()); 151 if (loginWarningAccountName != null) { 152 i.putExtra(EXTRA_LOGIN_WARNING_FOR_ACCOUNT, loginWarningAccountName); 153 } 154 if (loginWarningReason != null) { 155 i.putExtra(EXTRA_LOGIN_WARNING_REASON_FOR_ACCOUNT, loginWarningReason); 156 } 157 return i; 158 } 159 160 /** 161 * Launch generic settings and pre-enable the debug preferences 162 */ 163 public static void actionSettingsWithDebug(Context fromContext) { 164 Intent i = new Intent(fromContext, AccountSettings.class); 165 i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 166 i.putExtra(EXTRA_ENABLE_DEBUG, true); 167 fromContext.startActivity(i); 168 } 169 170 @Override 171 public void onCreate(Bundle savedInstanceState) { 172 super.onCreate(savedInstanceState); 173 ActivityHelper.debugSetWindowFlags(this); 174 175 final Intent i = getIntent(); 176 if (savedInstanceState == null) { 177 // If we are not restarting from a previous instance, we need to 178 // figure out the initial prefs to show. (Otherwise, we want to 179 // continue showing whatever the user last selected.) 180 if (ACTION_ACCOUNT_MANAGER_ENTRY.equals(i.getAction())) { 181 // This case occurs if we're changing account settings from Settings -> Accounts 182 mGetAccountIdFromAccountTask = 183 (GetAccountIdFromAccountTask) new GetAccountIdFromAccountTask() 184 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, i); 185 } else if (i.hasExtra(EditSettingsExtras.EXTRA_FOLDER)) { 186 launchMailboxSettings(i); 187 return; 188 } else if (i.hasExtra(EXTRA_NO_ACCOUNTS)) { 189 AccountSetupBasics.actionNewAccountWithResult(this); 190 finish(); 191 return; 192 } else { 193 // Otherwise, we're called from within the Email app and look for our extras 194 mRequestedAccountId = IntentUtilities.getAccountIdFromIntent(i); 195 String loginWarningAccount = i.getStringExtra(EXTRA_LOGIN_WARNING_FOR_ACCOUNT); 196 String loginWarningReason = 197 i.getStringExtra(EXTRA_LOGIN_WARNING_REASON_FOR_ACCOUNT); 198 if (loginWarningAccount != null) { 199 // Show dialog (first time only - don't re-show on a rotation) 200 LoginWarningDialog dialog = 201 LoginWarningDialog.newInstance(loginWarningAccount, loginWarningReason); 202 dialog.show(getFragmentManager(), "loginwarning"); 203 } 204 } 205 } 206 mShowDebugMenu = i.getBooleanExtra(EXTRA_ENABLE_DEBUG, false); 207 208 String title = i.getStringExtra(EXTRA_TITLE); 209 if (title != null) { 210 setTitle(title); 211 } 212 213 getActionBar().setDisplayOptions( 214 ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP); 215 216 mAccountObserver = new ContentObserver(Utility.getMainThreadHandler()) { 217 @Override 218 public void onChange(boolean selfChange) { 219 updateAccounts(); 220 } 221 }; 222 } 223 224 @Override 225 public void onResume() { 226 super.onResume(); 227 getContentResolver().registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver); 228 updateAccounts(); 229 } 230 231 @Override 232 public void onPause() { 233 super.onPause(); 234 getContentResolver().unregisterContentObserver(mAccountObserver); 235 } 236 237 @Override 238 protected void onDestroy() { 239 super.onDestroy(); 240 Utility.cancelTaskInterrupt(mLoadAccountListTask); 241 mLoadAccountListTask = null; 242 Utility.cancelTaskInterrupt(mGetAccountIdFromAccountTask); 243 mGetAccountIdFromAccountTask = null; 244 } 245 246 /** 247 * Listen for secret sequence and, if heard, enable debug menu 248 */ 249 @Override 250 public boolean onKeyDown(int keyCode, KeyEvent event) { 251 if (event.getKeyCode() == SECRET_KEY_CODES[mSecretKeyCodeIndex]) { 252 mSecretKeyCodeIndex++; 253 if (mSecretKeyCodeIndex == SECRET_KEY_CODES.length) { 254 mSecretKeyCodeIndex = 0; 255 enableDebugMenu(); 256 } 257 } else { 258 mSecretKeyCodeIndex = 0; 259 } 260 return super.onKeyDown(keyCode, event); 261 } 262 263 @Override 264 public boolean onCreateOptionsMenu(Menu menu) { 265 super.onCreateOptionsMenu(menu); 266 getMenuInflater().inflate(R.menu.account_settings_add_account_option, menu); 267 return true; 268 } 269 270 @Override 271 public boolean onOptionsItemSelected(MenuItem item) { 272 switch (item.getItemId()) { 273 case android.R.id.home: 274 // The app icon on the action bar is pressed. Just emulate a back press. 275 // TODO: this should navigate to the main screen, even if a sub-setting is open. 276 // But we shouldn't just finish(), as we want to show "discard changes?" dialog 277 // when necessary. 278 onBackPressed(); 279 break; 280 case R.id.add_new_account: 281 onAddNewAccount(); 282 break; 283 default: 284 return super.onOptionsItemSelected(item); 285 } 286 return true; 287 } 288 289 @Override 290 public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args, 291 int titleRes, int shortTitleRes) { 292 Intent result = super.onBuildStartFragmentIntent( 293 fragmentName, args, titleRes, shortTitleRes); 294 295 // When opening a sub-settings page (e.g. account specific page), see if we want to modify 296 // the activity title. 297 String title = AccountSettingsFragment.getTitleFromArgs(args); 298 if ((titleRes == 0) && (title != null)) { 299 result.putExtra(EXTRA_TITLE, title); 300 } 301 return result; 302 } 303 304 /** 305 * Any time we exit via this pathway, and we are showing a server settings fragment, 306 * we put up the exit-save-changes dialog. This will work for the following cases: 307 * Cancel button 308 * Back button 309 * Up arrow in application icon 310 * It will *not* apply in the following cases: 311 * Click the parent breadcrumb - need to find a hook for this 312 * Click in the header list (e.g. another account) - handled elsewhere 313 */ 314 @Override 315 public void onBackPressed() { 316 if (mCurrentFragment instanceof AccountServerBaseFragment) { 317 boolean changed = ((AccountServerBaseFragment) mCurrentFragment).haveSettingsChanged(); 318 if (changed) { 319 UnsavedChangesDialogFragment dialogFragment = 320 UnsavedChangesDialogFragment.newInstanceForBack(); 321 dialogFragment.show(getFragmentManager(), UnsavedChangesDialogFragment.TAG); 322 return; // Prevent "back" from being handled 323 } 324 } 325 super.onBackPressed(); 326 } 327 328 private void launchMailboxSettings(Intent intent) { 329 final Folder folder = (Folder)intent.getParcelableExtra(EditSettingsExtras.EXTRA_FOLDER); 330 331 // TODO: determine from the account if we should navigate to the mailbox settings. 332 // See bug 6242668 333 334 // Get the mailbox id from the folder 335 final long mailboxId = Long.parseLong(folder.uri.getPathSegments().get(1)); 336 337 MailboxSettings.start(this, mailboxId); 338 finish(); 339 } 340 341 342 private void enableDebugMenu() { 343 mShowDebugMenu = true; 344 invalidateHeaders(); 345 } 346 347 private void onAddNewAccount() { 348 AccountSetupBasics.actionNewAccount(this); 349 } 350 351 /** 352 * Start the async reload of the accounts list (if the headers are being displayed) 353 */ 354 private void updateAccounts() { 355 if (hasHeaders()) { 356 Utility.cancelTaskInterrupt(mLoadAccountListTask); 357 mLoadAccountListTask = (LoadAccountListTask) 358 new LoadAccountListTask().executeOnExecutor( 359 AsyncTask.THREAD_POOL_EXECUTOR, mDeletingAccountId); 360 } 361 } 362 363 /** 364 * Write the current header (accounts) array into the one provided by the PreferenceActivity. 365 * Skip any headers that match mDeletingAccountId (this is a quick-hide algorithm while a 366 * background thread works on deleting the account). Also sets mRequestedAccountHeader if 367 * we find the requested account (by id). 368 */ 369 @Override 370 public void onBuildHeaders(List<Header> target) { 371 // Assume the account is unspecified 372 mRequestedAccountHeader = null; 373 374 // Always add app preferences as first header 375 target.clear(); 376 target.add(getAppPreferencesHeader()); 377 378 // Then add zero or more account headers as necessary 379 if (mAccountListHeaders != null) { 380 int headerCount = mAccountListHeaders.length; 381 for (int index = 0; index < headerCount; index++) { 382 Header header = mAccountListHeaders[index]; 383 if (header != null && header.id != HEADER_ID_UNDEFINED) { 384 if (header.id != mDeletingAccountId) { 385 target.add(header); 386 if (header.id == mRequestedAccountId) { 387 mRequestedAccountHeader = header; 388 mRequestedAccountId = -1; 389 } 390 } 391 } 392 } 393 } 394 395 // finally, if debug header is enabled, show it 396 if (mShowDebugMenu) { 397 // setup lightweight header for debugging 398 Header debugHeader = new Header(); 399 debugHeader.title = getText(R.string.debug_title); 400 debugHeader.summary = null; 401 debugHeader.iconRes = 0; 402 debugHeader.fragment = DebugFragment.class.getCanonicalName(); 403 debugHeader.fragmentArguments = null; 404 target.add(debugHeader); 405 } 406 407 // Save for later use (see forceSwitch) 408 mGeneratedHeaders = target; 409 } 410 411 /** 412 * Generate and return the first header, for app preferences 413 */ 414 private Header getAppPreferencesHeader() { 415 // Set up fixed header for general settings 416 if (mAppPreferencesHeader == null) { 417 mAppPreferencesHeader = new Header(); 418 mAppPreferencesHeader.title = getText(R.string.header_label_general_preferences); 419 mAppPreferencesHeader.summary = null; 420 mAppPreferencesHeader.iconRes = 0; 421 mAppPreferencesHeader.fragment = GeneralPreferences.class.getCanonicalName(); 422 mAppPreferencesHeader.fragmentArguments = null; 423 } 424 return mAppPreferencesHeader; 425 } 426 427 /** 428 * This AsyncTask reads the accounts list and generates the headers. When the headers are 429 * ready, we'll trigger PreferenceActivity to refresh the account list with them. 430 * 431 * The array generated and stored in mAccountListHeaders may be sparse so any readers should 432 * check for and skip over null entries, and should not assume array length is # of accounts. 433 * 434 * TODO: Smaller projection 435 * TODO: Convert to Loader 436 * TODO: Write a test, including operation of deletingAccountId param 437 */ 438 private class LoadAccountListTask extends AsyncTask<Long, Void, Object[]> { 439 440 @Override 441 protected Object[] doInBackground(Long... params) { 442 Header[] result = null; 443 Boolean deletingAccountFound = false; 444 long deletingAccountId = params[0]; 445 446 Cursor c = getContentResolver().query( 447 Account.CONTENT_URI, 448 Account.CONTENT_PROJECTION, null, null, null); 449 try { 450 int index = 0; 451 int headerCount = c.getCount(); 452 result = new Header[headerCount]; 453 454 while (c.moveToNext()) { 455 long accountId = c.getLong(Account.CONTENT_ID_COLUMN); 456 if (accountId == deletingAccountId) { 457 deletingAccountFound = true; 458 continue; 459 } 460 String name = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN); 461 String email = c.getString(Account.CONTENT_EMAIL_ADDRESS_COLUMN); 462 Header newHeader = new Header(); 463 newHeader.id = accountId; 464 newHeader.title = name; 465 newHeader.summary = email; 466 newHeader.fragment = AccountSettingsFragment.class.getCanonicalName(); 467 newHeader.fragmentArguments = 468 AccountSettingsFragment.buildArguments(accountId, email); 469 470 result[index++] = newHeader; 471 } 472 } finally { 473 if (c != null) { 474 c.close(); 475 } 476 } 477 return new Object[] { result, deletingAccountFound }; 478 } 479 480 @Override 481 protected void onPostExecute(Object[] result) { 482 if (isCancelled() || result == null) return; 483 // Extract the results 484 Header[] headers = (Header[]) result[0]; 485 boolean deletingAccountFound = (Boolean) result[1]; 486 // report the settings 487 mAccountListHeaders = headers; 488 invalidateHeaders(); 489 if (!deletingAccountFound) { 490 mDeletingAccountId = -1; 491 } 492 } 493 } 494 495 /** 496 * Called when the user selects an item in the header list. Handles save-data cases as needed 497 * 498 * @param header The header that was selected. 499 * @param position The header's position in the list. 500 */ 501 @Override 502 public void onHeaderClick(Header header, int position) { 503 // special case when exiting the server settings fragments 504 if (mCurrentFragment instanceof AccountServerBaseFragment) { 505 boolean changed = ((AccountServerBaseFragment)mCurrentFragment).haveSettingsChanged(); 506 if (changed) { 507 UnsavedChangesDialogFragment dialogFragment = 508 UnsavedChangesDialogFragment.newInstanceForHeader(position); 509 dialogFragment.show(getFragmentManager(), UnsavedChangesDialogFragment.TAG); 510 return; 511 } 512 } 513 514 // Secret keys: Click 10x to enable debug settings 515 if (position == 0) { 516 mNumGeneralHeaderClicked++; 517 if (mNumGeneralHeaderClicked == 10) { 518 enableDebugMenu(); 519 } 520 } else { 521 mNumGeneralHeaderClicked = 0; 522 } 523 524 // Process header click normally 525 super.onHeaderClick(header, position); 526 } 527 528 /** 529 * Switch to a specific header without checking for server settings fragments as done 530 * in {@link #onHeaderClick(Header, int)}. Called after we interrupted a header switch 531 * with a dialog, and the user OK'd it. 532 */ 533 private void forceSwitchHeader(int position) { 534 // Clear the current fragment; we're navigating away 535 mCurrentFragment = null; 536 // Ensure the UI visually shows the correct header selected 537 setSelection(position); 538 Header header = mGeneratedHeaders.get(position); 539 switchToHeader(header); 540 } 541 542 /** 543 * Forcefully go backward in the stack. This may potentially discard unsaved settings. 544 */ 545 private void forceBack() { 546 // Clear the current fragment; we're navigating away 547 mCurrentFragment = null; 548 onBackPressed(); 549 } 550 551 @Override 552 public void onAttachFragment(Fragment f) { 553 super.onAttachFragment(f); 554 555 if (f instanceof AccountSettingsFragment) { 556 AccountSettingsFragment asf = (AccountSettingsFragment) f; 557 asf.setCallback(mAccountSettingsFragmentCallback); 558 } else if (f instanceof AccountServerBaseFragment) { 559 AccountServerBaseFragment asbf = (AccountServerBaseFragment) f; 560 asbf.setCallback(mAccountServerSettingsFragmentCallback); 561 } else { 562 // Possibly uninteresting fragment, such as a dialog. 563 return; 564 } 565 mCurrentFragment = f; 566 567 // When we're changing fragments, enable/disable the add account button 568 invalidateOptionsMenu(); 569 } 570 571 /** 572 * Callbacks for AccountSettingsFragment 573 */ 574 private class AccountSettingsFragmentCallback implements AccountSettingsFragment.Callback { 575 @Override 576 public void onSettingsChanged(Account account, String preference, Object value) { 577 AccountSettings.this.onSettingsChanged(account, preference, value); 578 } 579 @Override 580 public void onEditQuickResponses(Account account) { 581 AccountSettings.this.onEditQuickResponses(account); 582 } 583 @Override 584 public void onIncomingSettings(Account account) { 585 AccountSettings.this.onIncomingSettings(account); 586 } 587 @Override 588 public void onOutgoingSettings(Account account) { 589 AccountSettings.this.onOutgoingSettings(account); 590 } 591 @Override 592 public void abandonEdit() { 593 finish(); 594 } 595 } 596 597 /** 598 * Callbacks for AccountServerSettingsFragmentCallback 599 */ 600 private class AccountServerSettingsFragmentCallback 601 implements AccountServerBaseFragment.Callback { 602 @Override 603 public void onEnableProceedButtons(boolean enable) { 604 // This is not used - it's a callback for the legacy activities 605 } 606 607 @Override 608 public void onProceedNext(int checkMode, AccountServerBaseFragment target) { 609 AccountCheckSettingsFragment checkerFragment = 610 AccountCheckSettingsFragment.newInstance(checkMode, target); 611 startPreferenceFragment(checkerFragment, true); 612 } 613 614 /** 615 * After verifying a new server configuration as OK, we return here and continue. This 616 * simply does a "back" to exit the settings screen. 617 */ 618 @Override 619 public void onCheckSettingsComplete(int result, int setupMode) { 620 if (result == AccountCheckSettingsFragment.CHECK_SETTINGS_OK) { 621 // Settings checked & saved; clear current fragment 622 mCurrentFragment = null; 623 onBackPressed(); 624 } 625 } 626 } 627 628 /** 629 * Some of the settings have changed. Update internal state as necessary. 630 */ 631 public void onSettingsChanged(Account account, String preference, Object value) { 632 if (AccountSettingsFragment.PREFERENCE_DESCRIPTION.equals(preference)) { 633 for (Header header : mAccountListHeaders) { 634 if (header.id == account.mId) { 635 // Manually tweak the header title. We cannot rebuild the header list from 636 // an account cursor as the account database has not been saved yet. 637 header.title = value.toString(); 638 invalidateHeaders(); 639 break; 640 } 641 } 642 } 643 } 644 645 /** 646 * Dispatch to edit quick responses. 647 */ 648 public void onEditQuickResponses(Account account) { 649 try { 650 Bundle args = new Bundle(); 651 args.putParcelable(QUICK_RESPONSE_ACCOUNT_KEY, account); 652 startPreferencePanel(AccountSettingsEditQuickResponsesFragment.class.getName(), args, 653 R.string.account_settings_edit_quick_responses_label, null, null, 0); 654 } catch (Exception e) { 655 Log.d(Logging.LOG_TAG, "Error while trying to invoke edit quick responses.", e); 656 } 657 } 658 659 /** 660 * Dispatch to edit incoming settings. 661 */ 662 public void onIncomingSettings(Account account) { 663 try { 664 SetupData.init(SetupData.FLOW_MODE_EDIT, account); 665 startPreferencePanel(AccountSetupIncomingFragment.class.getName(), 666 AccountSetupIncomingFragment.getSettingsModeArgs(), 667 R.string.account_settings_incoming_label, null, null, 0); 668 } catch (Exception e) { 669 Log.d(Logging.LOG_TAG, "Error while trying to invoke store settings.", e); 670 } 671 } 672 673 /** 674 * Dispatch to edit outgoing settings. 675 * 676 * TODO: Make things less hardwired 677 */ 678 public void onOutgoingSettings(Account account) { 679 try { 680 Sender sender = Sender.getInstance(getApplication(), account); 681 if (sender != null) { 682 Class<? extends android.app.Activity> setting = sender.getSettingActivityClass(); 683 if (setting != null) { 684 SetupData.init(SetupData.FLOW_MODE_EDIT, account); 685 if (setting.equals(AccountSetupOutgoing.class)) { 686 startPreferencePanel(AccountSetupOutgoingFragment.class.getName(), 687 AccountSetupOutgoingFragment.getSettingsModeArgs(), 688 R.string.account_settings_outgoing_label, null, null, 0); 689 } 690 } 691 } 692 } catch (Exception e) { 693 Log.d(Logging.LOG_TAG, "Error while trying to invoke sender settings.", e); 694 } 695 } 696 697 /** 698 * Delete the selected account 699 */ 700 public void deleteAccount(final Account account) { 701 // Kick off the work to actually delete the account 702 new Thread(new Runnable() { 703 @Override 704 public void run() { 705 Uri uri = EmailProvider.uiUri("uiaccount", account.mId); 706 getContentResolver().delete(uri, null, null); 707 }}).start(); 708 709 // TODO: Remove ui glue for unified 710 // Then update the UI as appropriate: 711 // If single pane, return to the header list. If multi, rebuild header list 712 if (onIsMultiPane()) { 713 Header prefsHeader = getAppPreferencesHeader(); 714 this.switchToHeader(prefsHeader.fragment, prefsHeader.fragmentArguments); 715 mDeletingAccountId = account.mId; 716 updateAccounts(); 717 } else { 718 // We should only be calling this while showing AccountSettingsFragment, 719 // so a finish() should bring us back to headers. No point hiding the deleted account. 720 finish(); 721 } 722 } 723 724 /** 725 * This AsyncTask looks up an account based on its email address (which is what we get from 726 * the Account Manager). When the account id is determined, we refresh the header list, 727 * which will select the preferences for that account. 728 */ 729 private class GetAccountIdFromAccountTask extends AsyncTask<Intent, Void, Long> { 730 731 @Override 732 protected Long doInBackground(Intent... params) { 733 Intent intent = params[0]; 734 android.accounts.Account acct = 735 (android.accounts.Account) intent.getParcelableExtra(EXTRA_ACCOUNT_MANAGER_ACCOUNT); 736 return Utility.getFirstRowLong(AccountSettings.this, Account.CONTENT_URI, 737 Account.ID_PROJECTION, SELECTION_ACCOUNT_EMAIL_ADDRESS, 738 new String[] {acct.name}, null, Account.ID_PROJECTION_COLUMN, -1L); 739 } 740 741 @Override 742 protected void onPostExecute(Long accountId) { 743 if (accountId != -1 && !isCancelled()) { 744 mRequestedAccountId = accountId; 745 invalidateHeaders(); 746 } 747 } 748 } 749 750 /** 751 * Dialog fragment to show "exit with unsaved changes?" dialog 752 */ 753 /* package */ static class UnsavedChangesDialogFragment extends DialogFragment { 754 final static String TAG = "UnsavedChangesDialogFragment"; 755 756 // Argument bundle keys 757 private final static String BUNDLE_KEY_HEADER = "UnsavedChangesDialogFragment.Header"; 758 private final static String BUNDLE_KEY_BACK = "UnsavedChangesDialogFragment.Back"; 759 760 /** 761 * Creates a save changes dialog when the user selects a new header 762 * @param position The new header index to make active if the user accepts the dialog. This 763 * must be a valid header index although there is no error checking. 764 */ 765 public static UnsavedChangesDialogFragment newInstanceForHeader(int position) { 766 UnsavedChangesDialogFragment f = new UnsavedChangesDialogFragment(); 767 Bundle b = new Bundle(); 768 b.putInt(BUNDLE_KEY_HEADER, position); 769 f.setArguments(b); 770 return f; 771 } 772 773 /** 774 * Creates a save changes dialog when the user navigates "back". 775 * {@link #onBackPressed()} defines in which case this may be triggered. 776 */ 777 public static UnsavedChangesDialogFragment newInstanceForBack() { 778 UnsavedChangesDialogFragment f = new UnsavedChangesDialogFragment(); 779 Bundle b = new Bundle(); 780 b.putBoolean(BUNDLE_KEY_BACK, true); 781 f.setArguments(b); 782 return f; 783 } 784 785 // Force usage of newInstance() 786 private UnsavedChangesDialogFragment() { 787 } 788 789 @Override 790 public Dialog onCreateDialog(Bundle savedInstanceState) { 791 final AccountSettings activity = (AccountSettings) getActivity(); 792 final int position = getArguments().getInt(BUNDLE_KEY_HEADER); 793 final boolean isBack = getArguments().getBoolean(BUNDLE_KEY_BACK); 794 795 return new AlertDialog.Builder(activity) 796 .setIconAttribute(android.R.attr.alertDialogIcon) 797 .setTitle(android.R.string.dialog_alert_title) 798 .setMessage(R.string.account_settings_exit_server_settings) 799 .setPositiveButton( 800 R.string.okay_action, 801 new DialogInterface.OnClickListener() { 802 @Override 803 public void onClick(DialogInterface dialog, int which) { 804 if (isBack) { 805 activity.forceBack(); 806 } else { 807 activity.forceSwitchHeader(position); 808 } 809 dismiss(); 810 } 811 }) 812 .setNegativeButton( 813 activity.getString(R.string.cancel_action), null) 814 .create(); 815 } 816 } 817 818 /** 819 * Dialog briefly shown in some cases, to indicate the user that login failed. If the user 820 * clicks OK, we simply dismiss the dialog, leaving the user in the account settings for 821 * that account; If the user clicks "cancel", we exit account settings. 822 */ 823 public static class LoginWarningDialog extends DialogFragment 824 implements DialogInterface.OnClickListener { 825 private static final String BUNDLE_KEY_ACCOUNT_NAME = "account_name"; 826 private String mReason; 827 828 /** 829 * Create a new dialog. 830 */ 831 public static LoginWarningDialog newInstance(String accountName, String reason) { 832 final LoginWarningDialog dialog = new LoginWarningDialog(); 833 Bundle b = new Bundle(); 834 b.putString(BUNDLE_KEY_ACCOUNT_NAME, accountName); 835 dialog.setArguments(b); 836 dialog.mReason = reason; 837 return dialog; 838 } 839 840 @Override 841 public Dialog onCreateDialog(Bundle savedInstanceState) { 842 final String accountName = getArguments().getString(BUNDLE_KEY_ACCOUNT_NAME); 843 844 final Context context = getActivity(); 845 final Resources res = context.getResources(); 846 final AlertDialog.Builder b = new AlertDialog.Builder(context); 847 b.setTitle(R.string.account_settings_login_dialog_title); 848 b.setIconAttribute(android.R.attr.alertDialogIcon); 849 if (mReason != null) { 850 final TextView message = new TextView(context); 851 String alert = res.getString( 852 R.string.account_settings_login_dialog_reason_fmt, accountName, mReason); 853 SpannableString spannableAlertString = new SpannableString(alert); 854 Linkify.addLinks(spannableAlertString, Linkify.WEB_URLS); 855 message.setText(spannableAlertString); 856 // There must be a better way than specifying size/padding this way 857 // It does work and look right, though 858 int textSize = res.getDimensionPixelSize(R.dimen.dialog_text_size); 859 message.setTextSize(textSize); 860 int paddingLeft = res.getDimensionPixelSize(R.dimen.dialog_padding_left); 861 int paddingOther = res.getDimensionPixelSize(R.dimen.dialog_padding_other); 862 message.setPadding(paddingLeft, paddingOther, paddingOther, paddingOther); 863 message.setMovementMethod(LinkMovementMethod.getInstance()); 864 b.setView(message); 865 } else { 866 b.setMessage(res.getString(R.string.account_settings_login_dialog_content_fmt, 867 accountName)); 868 } 869 b.setPositiveButton(R.string.okay_action, this); 870 b.setNegativeButton(R.string.cancel_action, this); 871 return b.create(); 872 } 873 874 @Override 875 public void onClick(DialogInterface dialog, int which) { 876 dismiss(); 877 if (which == DialogInterface.BUTTON_NEGATIVE) { 878 getActivity().finish(); 879 } 880 } 881 } 882} 883