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