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