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