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