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