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