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