AccountCheckSettingsFragment.java revision 35b0e95ca795e17b6dc8dd98c7ab847d65d9aa0c
1/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.activity.setup;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.DialogFragment;
23import android.app.Fragment;
24import android.app.FragmentManager;
25import android.app.ProgressDialog;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.os.AsyncTask;
29import android.os.Bundle;
30import android.text.TextUtils;
31import android.util.Log;
32
33import com.android.email.R;
34import com.android.email.mail.Sender;
35import com.android.email.mail.Store;
36import com.android.emailcommon.Logging;
37import com.android.emailcommon.mail.MessagingException;
38import com.android.emailcommon.provider.Account;
39import com.android.emailcommon.provider.HostAuth;
40import com.android.emailcommon.provider.Policy;
41import com.android.emailcommon.service.EmailServiceProxy;
42import com.android.emailcommon.utility.Utility;
43
44/**
45 * Check incoming or outgoing settings, or perform autodiscovery.
46 *
47 * There are three components that work together.  1. This fragment is retained and non-displayed,
48 * and controls the overall process.  2. An AsyncTask that works with the stores/services to
49 * check the accounts settings.  3. A stateless progress dialog (which will be recreated on
50 * orientation changes).
51 *
52 * There are also two lightweight error dialogs which are used for notification of terminal
53 * conditions.
54 */
55public class AccountCheckSettingsFragment extends Fragment {
56
57    public final static String TAG = "AccountCheckSettingsFragment";
58
59    // Debugging flags - for debugging the UI
60    // If true, use a "fake" account check cycle
61    private static final boolean DEBUG_FAKE_CHECK_CYCLE = false;            // DO NOT CHECK IN TRUE
62    // If true, use fake check cycle, return failure
63    private static final boolean DEBUG_FAKE_CHECK_ERR = false;              // DO NOT CHECK IN TRUE
64    // If true, use fake check cycle, return "security required"
65    private static final boolean DEBUG_FORCE_SECURITY_REQUIRED = false;     // DO NOT CHECK IN TRUE
66
67    // State
68    private final static int STATE_START = 0;
69    private final static int STATE_CHECK_AUTODISCOVER = 1;
70    private final static int STATE_CHECK_INCOMING = 2;
71    private final static int STATE_CHECK_OUTGOING = 3;
72    private final static int STATE_CHECK_OK = 4;                    // terminal
73    private final static int STATE_CHECK_SHOW_SECURITY = 5;         // terminal
74    private final static int STATE_CHECK_ERROR = 6;                 // terminal
75    private final static int STATE_AUTODISCOVER_AUTH_DIALOG = 7;    // terminal
76    private final static int STATE_AUTODISCOVER_RESULT = 8;         // terminal
77    private int mState = STATE_START;
78
79    // Support for UI
80    private boolean mAttached;
81    private CheckingDialog mCheckingDialog;
82    private int mErrorStringId;
83    private String mErrorMessage;
84    private HostAuth mAutoDiscoverResult;
85
86    // Support for AsyncTask and account checking
87    AccountCheckTask mAccountCheckTask;
88
89    // Result codes returned by onCheckSettingsComplete.
90    /** Check settings returned successfully */
91    public final static int CHECK_SETTINGS_OK = 0;
92    /** Check settings failed due to connection, authentication, or other server error */
93    public final static int CHECK_SETTINGS_SERVER_ERROR = 1;
94    /** Check settings failed due to user refusing to accept security requirements */
95    public final static int CHECK_SETTINGS_SECURITY_USER_DENY = 2;
96
97    // Result codes returned by onAutoDiscoverComplete.
98    /** AutoDiscover completed successfully with server setup data */
99    public final static int AUTODISCOVER_OK = 0;
100    /** AutoDiscover completed with no data (no server or AD not supported) */
101    public final static int AUTODISCOVER_NO_DATA = 1;
102    /** AutoDiscover reported authentication error */
103    public final static int AUTODISCOVER_AUTHENTICATION = 2;
104
105    /**
106     * Callback interface for any target or activity doing account check settings
107     */
108    public interface Callbacks {
109        /**
110         * Called when CheckSettings completed
111         * @param result check settings result code - success is CHECK_SETTINGS_OK
112         */
113        public void onCheckSettingsComplete(int result);
114
115        /**
116         * Called when autodiscovery completes.
117         * @param result autodiscovery result code - success is AUTODISCOVER_OK
118         * @param hostAuth configuration data returned by AD server, or null if no data available
119         */
120        public void onAutoDiscoverComplete(int result, HostAuth hostAuth);
121    }
122
123    /**
124     * Create a retained, invisible fragment that checks accounts
125     *
126     * @param mode incoming or outgoing
127     */
128    public static AccountCheckSettingsFragment newInstance(int mode, Fragment parentFragment) {
129        AccountCheckSettingsFragment f = new AccountCheckSettingsFragment();
130        f.setTargetFragment(parentFragment, mode);
131        return f;
132    }
133
134    /**
135     * Fragment initialization.  Because we never implement onCreateView, and call
136     * setRetainInstance here, this creates an invisible, persistent, "worker" fragment.
137     */
138    @Override
139    public void onCreate(Bundle savedInstanceState) {
140        super.onCreate(savedInstanceState);
141        setRetainInstance(true);
142    }
143
144    /**
145     * This is called when the Fragment's Activity is ready to go, after
146     * its content view has been installed; it is called both after
147     * the initial fragment creation and after the fragment is re-attached
148     * to a new activity.
149     */
150    @Override
151    public void onActivityCreated(Bundle savedInstanceState) {
152        super.onActivityCreated(savedInstanceState);
153        mAttached = true;
154
155        // If this is the first time, start the AsyncTask
156        if (mAccountCheckTask == null) {
157            int checkMode = getTargetRequestCode();
158            Account checkAccount = SetupData.getAccount();
159            mAccountCheckTask = (AccountCheckTask)
160                    new AccountCheckTask(checkMode, checkAccount)
161                    .execute();
162        }
163    }
164
165    /**
166     * When resuming, restart the progress/error UI if necessary by re-reporting previous values
167     */
168    @Override
169    public void onResume() {
170        super.onResume();
171
172        if (mState != STATE_START) {
173            reportProgress(mState, mErrorStringId, mErrorMessage, mAutoDiscoverResult);
174        }
175    }
176
177    /**
178     * This is called when the fragment is going away.  It is NOT called
179     * when the fragment is being propagated between activity instances.
180     */
181    @Override
182    public void onDestroy() {
183        super.onDestroy();
184        Utility.cancelTaskInterrupt(mAccountCheckTask);
185        mAccountCheckTask = null;
186    }
187
188    /**
189     * This is called right before the fragment is detached from its current activity instance.
190     * All reporting and callbacks are halted until we reattach.
191     */
192    @Override
193    public void onDetach() {
194        super.onDetach();
195        mAttached = false;
196    }
197
198    /**
199     * The worker (AsyncTask) will call this (in the UI thread) to report progress.  If we are
200     * attached to an activity, update the progress immediately;  If not, simply hold the
201     * progress for later.
202     * @param newState The new progress state being reported
203     * @param errorStringId Resource Id of an error string to display
204     * @param errorMessage Additional string to insert if the resource string takes a parameter.
205     * @param autoDiscoverResult If doing autodiscovery, the setup info returned from AD server
206     */
207    public void reportProgress(int newState, int errorStringId, String errorMessage,
208            HostAuth autoDiscoverResult) {
209        mState = newState;
210        mErrorStringId = errorStringId;
211        mErrorMessage = errorMessage;
212        mAutoDiscoverResult = autoDiscoverResult;
213
214        // If we are attached, create, recover, and/or update the dialog
215        if (mAttached) {
216            FragmentManager fm = getFragmentManager();
217
218            switch (newState) {
219                case STATE_CHECK_OK:
220                    // immediately terminate, clean up, and report back
221                    // 1. get rid of progress dialog (if any)
222                    recoverAndDismissCheckingDialog();
223                    // 2. exit self
224                    fm.popBackStack();
225                    // 3. report OK back to target fragment or activity
226                    getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_OK);
227                    break;
228                case STATE_CHECK_SHOW_SECURITY:
229                    // 1. get rid of progress dialog (if any)
230                    recoverAndDismissCheckingDialog();
231                    // 2. launch the error dialog, if needed
232                    if (fm.findFragmentByTag(SecurityRequiredDialog.TAG) == null) {
233                        SecurityRequiredDialog securityRequiredDialog =
234                                SecurityRequiredDialog.newInstance(this, mErrorMessage);
235                        fm.beginTransaction()
236                                .add(securityRequiredDialog, SecurityRequiredDialog.TAG)
237                                .commit();
238                    }
239                    break;
240                case STATE_CHECK_ERROR:
241                case STATE_AUTODISCOVER_AUTH_DIALOG:
242                    // 1. get rid of progress dialog (if any)
243                    recoverAndDismissCheckingDialog();
244                    // 2. launch the error dialog, if needed
245                    if (fm.findFragmentByTag(ErrorDialog.TAG) == null) {
246                        ErrorDialog errorDialog =
247                                ErrorDialog.newInstance(this, mErrorStringId, mErrorMessage);
248                        fm.beginTransaction()
249                                .add(errorDialog, ErrorDialog.TAG)
250                                .commit();
251                    }
252                    break;
253                case STATE_AUTODISCOVER_RESULT:
254                    // 1. get rid of progress dialog (if any)
255                    recoverAndDismissCheckingDialog();
256                    // 2. exit self
257                    fm.popBackStack();
258                    // 3. report back to target fragment or activity
259                    getCallbackTarget().onAutoDiscoverComplete(
260                            (mAutoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA,
261                                    mAutoDiscoverResult);
262                    break;
263                default:
264                    // Display a normal progress message
265                    mCheckingDialog = (CheckingDialog) fm.findFragmentByTag(CheckingDialog.TAG);
266
267                    if (mCheckingDialog == null) {
268                        mCheckingDialog = CheckingDialog.newInstance(this, mState);
269                        fm.beginTransaction()
270                                .add(mCheckingDialog, CheckingDialog.TAG)
271                                .commit();
272                    } else {
273                        mCheckingDialog.updateProgress(mState);
274                    }
275                    break;
276            }
277        }
278    }
279
280    /**
281     * Find the callback target, either a target fragment or the activity
282     */
283    private Callbacks getCallbackTarget() {
284        Fragment target = getTargetFragment();
285        if (target instanceof Callbacks) {
286            return (Callbacks) target;
287        }
288        Activity activity = getActivity();
289        if (activity instanceof Callbacks) {
290            return (Callbacks) activity;
291        }
292        throw new IllegalStateException();
293    }
294
295    /**
296     * Recover and dismiss the progress dialog fragment
297     */
298    private void recoverAndDismissCheckingDialog() {
299        if (mCheckingDialog == null) {
300            mCheckingDialog = (CheckingDialog)
301                    getFragmentManager().findFragmentByTag(CheckingDialog.TAG);
302        }
303        if (mCheckingDialog != null) {
304            mCheckingDialog.dismissAllowingStateLoss();
305            mCheckingDialog = null;
306        }
307    }
308
309    /**
310     * This is called when the user clicks "cancel" on the progress dialog.  Shuts everything
311     * down and dismisses everything.
312     * This should cause us to remain in the current screen (not accepting the settings)
313     */
314    private void onCheckingDialogCancel() {
315        // 1. kill the checker
316        Utility.cancelTaskInterrupt(mAccountCheckTask);
317        mAccountCheckTask = null;
318        // 2. kill self with no report - this is "cancel"
319        getFragmentManager().popBackStack();
320    }
321
322    /**
323     * This is called when the user clicks "edit" from the error dialog.  The error dialog
324     * should have already dismissed itself.
325     * Depending on the context, the target will remain in the current activity (e.g. editing
326     * settings) or return to its own parent (e.g. enter new credentials).
327     */
328    private void onErrorDialogEditButton() {
329        // 1. handle "edit" - notify callback that we had a problem with the test
330        Callbacks callbackTarget = getCallbackTarget();
331        if (mState == STATE_AUTODISCOVER_AUTH_DIALOG) {
332            // report auth error to target fragment or activity
333            callbackTarget.onAutoDiscoverComplete(AUTODISCOVER_AUTHENTICATION, null);
334        } else {
335            // report check settings failure to target fragment or activity
336            callbackTarget.onCheckSettingsComplete(CHECK_SETTINGS_SERVER_ERROR);
337        }
338        // 2. kill self if not already killed by callback
339        FragmentManager fm = getFragmentManager();
340        if (fm != null) {
341            fm.popBackStack();
342        }
343    }
344
345    /**
346     * This is called when the user clicks "ok" or "cancel" on the "security required" dialog.
347     * Shuts everything down and dismisses everything, and reports the result appropriately.
348     */
349    private void onSecurityRequiredDialogResultOk(boolean okPressed) {
350        // 1. handle OK/cancel - notify that security is OK and we can proceed
351        Callbacks callbackTarget = getCallbackTarget();
352        callbackTarget.onCheckSettingsComplete(
353                okPressed ? CHECK_SETTINGS_OK : CHECK_SETTINGS_SECURITY_USER_DENY);
354
355        // 2. kill self if not already killed by callback
356        FragmentManager fm = getFragmentManager();
357        if (fm != null) {
358            fm.popBackStack();
359        }
360    }
361
362    /**
363     * This exception class is used to report autodiscover results via the reporting mechanism.
364     */
365    public static class AutoDiscoverResults extends MessagingException {
366        public final HostAuth mHostAuth;
367
368        /**
369         * @param authenticationError true if auth failure, false for result (or no response)
370         * @param hostAuth null for "no autodiscover", non-null for server info to return
371         */
372        public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) {
373            super(null);
374            if (authenticationError) {
375                mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED;
376            } else {
377                mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT;
378            }
379            mHostAuth = hostAuth;
380        }
381    }
382
383    /**
384     * This AsyncTask does the actual account checking
385     *
386     * TODO: It would be better to remove the UI complete from here (the exception->string
387     * conversions).
388     */
389    private class AccountCheckTask extends AsyncTask<Void, Integer, MessagingException> {
390
391        final Context mContext;
392        final int mMode;
393        final Account mAccount;
394        final String mStoreHost;
395        final String mCheckEmail;
396        final String mCheckPassword;
397
398        /**
399         * Create task and parameterize it
400         * @param mode bits request operations
401         * @param checkAccount account holding values to be checked
402         */
403        public AccountCheckTask(int mode, Account checkAccount) {
404            mContext = getActivity().getApplicationContext();
405            mMode = mode;
406            mAccount = checkAccount;
407            mStoreHost = checkAccount.mHostAuthRecv.mAddress;
408            mCheckEmail = checkAccount.mEmailAddress;
409            mCheckPassword = checkAccount.mHostAuthRecv.mPassword;
410        }
411
412        @Override
413        protected MessagingException doInBackground(Void... params) {
414            if (DEBUG_FAKE_CHECK_CYCLE) {
415                return fakeChecker();
416            }
417
418            try {
419                if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) {
420                    if (isCancelled()) return null;
421                    publishProgress(STATE_CHECK_AUTODISCOVER);
422                    Log.d(Logging.LOG_TAG, "Begin auto-discover for " + mCheckEmail);
423                    Store store = Store.getInstance(mAccount, mContext);
424                    Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
425                    // Result will be one of:
426                    //  null: remote exception - proceed to manual setup
427                    //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
428                    //  Other error: proceed to manual setup
429                    //  No error: return autodiscover results
430                    if (result == null) {
431                        return new AutoDiscoverResults(false, null);
432                    }
433                    int errorCode =
434                            result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
435                    if (errorCode == MessagingException.AUTHENTICATION_FAILED) {
436                        return new AutoDiscoverResults(true, null);
437                    } else if (errorCode != MessagingException.NO_ERROR) {
438                        return new AutoDiscoverResults(false, null);
439                    } else {
440                        HostAuth serverInfo = (HostAuth)
441                            result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
442                        return new AutoDiscoverResults(false, serverInfo);
443                    }
444                }
445
446                // Check Incoming Settings
447                if ((mMode & SetupData.CHECK_INCOMING) != 0) {
448                    if (isCancelled()) return null;
449                    Log.d(Logging.LOG_TAG, "Begin check of incoming email settings");
450                    publishProgress(STATE_CHECK_INCOMING);
451                    Store store = Store.getInstance(mAccount, mContext);
452                    Bundle bundle = store.checkSettings();
453                    int resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
454                    if (bundle != null) {
455                        resultCode = bundle.getInt(
456                                EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
457                    }
458                    if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
459                        SetupData.setPolicy((Policy)bundle.getParcelable(
460                                EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
461                        return new MessagingException(resultCode, mStoreHost);
462                    } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
463                        String[] data = bundle.getStringArray(
464                                EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES);
465                        return new MessagingException(resultCode, mStoreHost, data);
466                    } else if (resultCode != MessagingException.NO_ERROR) {
467                        String errorMessage =
468                            bundle.getString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
469                        return new MessagingException(resultCode, errorMessage);
470                    }
471                }
472
473                // Check Outgoing Settings
474                if ((mMode & SetupData.CHECK_OUTGOING) != 0) {
475                    if (isCancelled()) return null;
476                    Log.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
477                    publishProgress(STATE_CHECK_OUTGOING);
478                    Sender sender = Sender.getInstance(mContext, mAccount);
479                    sender.close();
480                    sender.open();
481                    sender.close();
482                }
483
484                // If we reached the end, we completed the check(s) successfully
485                return null;
486            } catch (final MessagingException me) {
487                // Some of the legacy account checkers return errors by throwing MessagingException,
488                // which we catch and return here.
489                return me;
490            }
491        }
492
493        /**
494         * Dummy background worker, for testing UI only.
495         */
496        private MessagingException fakeChecker() {
497            // Dummy:  Publish a series of progress setups, 2 sec delays between them;
498            // then return "ok" (null)
499            final int DELAY = 2*1000;
500            if (isCancelled()) return null;
501            if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) {
502                publishProgress(STATE_CHECK_AUTODISCOVER);
503                try {
504                    Thread.sleep(DELAY);
505                } catch (InterruptedException e) { }
506                if (DEBUG_FAKE_CHECK_ERR) {
507                    return new MessagingException(MessagingException.AUTHENTICATION_FAILED);
508                }
509                // Return "real" AD results
510                HostAuth auth = new HostAuth();
511                auth.setLogin("user", "password");
512                auth.setConnection(Store.STORE_SCHEME_EAS, "testserver.com", 0);
513                return new AutoDiscoverResults(false, auth);
514            }
515            if (isCancelled()) return null;
516            if ((mMode & SetupData.CHECK_INCOMING) != 0) {
517                publishProgress(STATE_CHECK_INCOMING);
518                try {
519                    Thread.sleep(DELAY);
520                } catch (InterruptedException e) { }
521                if (DEBUG_FAKE_CHECK_ERR) {
522                    return new MessagingException(MessagingException.IOERROR);
523                } else if (DEBUG_FORCE_SECURITY_REQUIRED) {
524                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
525                }
526            }
527            if (isCancelled()) return null;
528            if ((mMode & SetupData.CHECK_OUTGOING) != 0) {
529                publishProgress(STATE_CHECK_OUTGOING);
530                try {
531                    Thread.sleep(DELAY);
532                } catch (InterruptedException e) { }
533                if (DEBUG_FAKE_CHECK_ERR) {
534                    return new MessagingException(MessagingException.TLS_REQUIRED);
535                }
536            }
537            return null;
538        }
539
540        /**
541         * Progress reports (runs in UI thread).  This should be used for real progress only
542         * (not for errors).
543         */
544        @Override
545        protected void onProgressUpdate(Integer... progress) {
546            if (isCancelled()) return;
547            reportProgress(progress[0], 0, null, null);
548        }
549
550        /**
551         * Result handler (runs in UI thread).
552         *
553         * AutoDiscover authentication errors are handled a bit differently than the
554         * other errors;  If encountered, we display the error dialog, but we return with
555         * a different callback used only for AutoDiscover.
556         *
557         * @param result null for a successful check;  exception for various errors
558         */
559        @Override
560        protected void onPostExecute(MessagingException result) {
561            if (isCancelled()) return;
562            if (result == null) {
563                reportProgress(STATE_CHECK_OK, 0, null, null);
564            } else {
565                int progressState = STATE_CHECK_ERROR;
566                int exceptionType = result.getExceptionType();
567                String message = result.getMessage();
568                if (message != null) {
569                    message = message.trim();
570                }
571                HostAuth hostAuth = null;
572                int id = 0;
573
574                switch (exceptionType) {
575                    // NOTE: AutoDiscover reports have their own reporting state, handle differently
576                    // from the other exception types
577                    case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
578                        id = TextUtils.isEmpty(message)
579                            ? R.string.account_setup_failed_dlg_auth_message
580                            : R.string.account_setup_failed_dlg_auth_message_fmt;
581                        progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
582                        break;
583                    case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
584                        hostAuth = ((AutoDiscoverResults)result).mHostAuth;
585                        progressState = STATE_AUTODISCOVER_RESULT;
586                        break;
587
588                    // NOTE: Security policies required has its own report state, handle it a bit
589                    // differently from the other exception types.
590                    case MessagingException.SECURITY_POLICIES_REQUIRED:
591                        progressState = STATE_CHECK_SHOW_SECURITY;
592                        break;
593
594                    // The remaining exception types are handled by setting the state to
595                    // STATE_CHECK_ERROR (above, default) and conversion to specific error strings.
596                    case MessagingException.CERTIFICATE_VALIDATION_ERROR:
597                        id = TextUtils.isEmpty(message)
598                            ? R.string.account_setup_failed_dlg_certificate_message
599                            : R.string.account_setup_failed_dlg_certificate_message_fmt;
600                        break;
601                    case MessagingException.AUTHENTICATION_FAILED:
602                        id = TextUtils.isEmpty(message)
603                            ? R.string.account_setup_failed_dlg_auth_message
604                            : R.string.account_setup_failed_dlg_auth_message_fmt;
605                        break;
606                    case MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR:
607                        id = R.string.account_setup_failed_check_credentials_message;
608                        break;
609                    case MessagingException.IOERROR:
610                        id = R.string.account_setup_failed_ioerror;
611                        break;
612                    case MessagingException.TLS_REQUIRED:
613                        id = R.string.account_setup_failed_tls_required;
614                        break;
615                    case MessagingException.AUTH_REQUIRED:
616                        id = R.string.account_setup_failed_auth_required;
617                        break;
618                    case MessagingException.SECURITY_POLICIES_UNSUPPORTED:
619                        id = R.string.account_setup_failed_security_policies_unsupported;
620                        // Belt and suspenders here; there should always be a non-empty array here
621                        String[] unsupportedPolicies = (String[])result.getExceptionData();
622                        if (unsupportedPolicies == null) {
623                            Log.w(TAG, "No data for unsupported policies?");
624                            break;
625                        }
626                        // Build a string, concatenating policies we don't support
627                        StringBuilder sb = new StringBuilder();
628                        boolean first = true;
629                        for (String policyName: unsupportedPolicies) {
630                            if (first) {
631                                first = false;
632                            } else {
633                                sb.append(", ");
634                            }
635                            sb.append(policyName);
636                        }
637                        message = sb.toString();
638                        break;
639                    case MessagingException.ACCESS_DENIED:
640                        id = R.string.account_setup_failed_access_denied;
641                        break;
642                    case MessagingException.PROTOCOL_VERSION_UNSUPPORTED:
643                        id = R.string.account_setup_failed_protocol_unsupported;
644                        break;
645                    case MessagingException.GENERAL_SECURITY:
646                        id = R.string.account_setup_failed_security;
647                        break;
648                    case MessagingException.CLIENT_CERTIFICATE_REQUIRED:
649                        id = R.string.account_setup_failed_certificate_required;
650                        break;
651                    case MessagingException.CLIENT_CERTIFICATE_ERROR:
652                        id = R.string.account_setup_failed_certificate_inaccessible;
653                        break;
654                    default:
655                        id = TextUtils.isEmpty(message)
656                                ? R.string.account_setup_failed_dlg_server_message
657                                : R.string.account_setup_failed_dlg_server_message_fmt;
658                        break;
659                }
660                reportProgress(progressState, id, message, hostAuth);
661            }
662        }
663    }
664
665    /**
666     * Simple dialog that shows progress as we work through the settings checks.
667     * This is stateless except for its UI (e.g. current strings) and can be torn down or
668     * recreated at any time without affecting the account checking progress.
669     */
670    public static class CheckingDialog extends DialogFragment {
671        @SuppressWarnings("hiding")
672        public final static String TAG = "CheckProgressDialog";
673
674        // Extras for saved instance state
675        private final String EXTRA_PROGRESS_STRING = "CheckProgressDialog.Progress";
676
677        // UI
678        private String mProgressString;
679
680        /**
681         * Create a dialog that reports progress
682         * @param progress initial progress indication
683         */
684        public static CheckingDialog newInstance(AccountCheckSettingsFragment parentFragment,
685                int progress) {
686            CheckingDialog f = new CheckingDialog();
687            f.setTargetFragment(parentFragment, progress);
688            return f;
689        }
690
691        /**
692         * Update the progress of an existing dialog
693         * @param progress latest progress to be displayed
694         */
695        public void updateProgress(int progress) {
696            mProgressString = getProgressString(progress);
697            AlertDialog dialog = (AlertDialog) getDialog();
698            dialog.setMessage(mProgressString);
699        }
700
701        @Override
702        public Dialog onCreateDialog(Bundle savedInstanceState) {
703            Context context = getActivity();
704            if (savedInstanceState != null) {
705                mProgressString = savedInstanceState.getString(EXTRA_PROGRESS_STRING);
706            }
707            if (mProgressString == null) {
708                mProgressString = getProgressString(getTargetRequestCode());
709            }
710            final AccountCheckSettingsFragment target =
711                (AccountCheckSettingsFragment) getTargetFragment();
712
713            ProgressDialog dialog = new ProgressDialog(context);
714            dialog.setIndeterminate(true);
715            dialog.setMessage(mProgressString);
716            dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
717                    context.getString(R.string.cancel_action),
718                    new DialogInterface.OnClickListener() {
719                        public void onClick(DialogInterface dialog, int which) {
720                            dismiss();
721                            target.onCheckingDialogCancel();
722                        }
723                    });
724            return dialog;
725        }
726
727        /**
728         * Listen for cancellation, which can happen from places other than the
729         * negative button (e.g. touching outside the dialog), and stop the checker
730         */
731        @Override
732        public void onCancel(DialogInterface dialog) {
733            AccountCheckSettingsFragment target =
734                (AccountCheckSettingsFragment) getTargetFragment();
735            target.onCheckingDialogCancel();
736            super.onCancel(dialog);
737        }
738
739        @Override
740        public void onSaveInstanceState(Bundle outState) {
741            super.onSaveInstanceState(outState);
742            outState.putString(EXTRA_PROGRESS_STRING, mProgressString);
743        }
744
745        /**
746         * Convert progress to message
747         */
748        private String getProgressString(int progress) {
749            int stringId = 0;
750            switch (progress) {
751                case STATE_CHECK_AUTODISCOVER:
752                    stringId = R.string.account_setup_check_settings_retr_info_msg;
753                    break;
754                case STATE_CHECK_INCOMING:
755                    stringId = R.string.account_setup_check_settings_check_incoming_msg;
756                    break;
757                case STATE_CHECK_OUTGOING:
758                    stringId = R.string.account_setup_check_settings_check_outgoing_msg;
759                    break;
760            }
761            return getActivity().getString(stringId);
762        }
763    }
764
765    /**
766     * The standard error dialog.  Calls back to onErrorDialogButton().
767     */
768    public static class ErrorDialog extends DialogFragment {
769        @SuppressWarnings("hiding")
770        public final static String TAG = "ErrorDialog";
771
772        // Bundle keys for arguments
773        private final static String ARGS_MESSAGE_ID = "ErrorDialog.Message.Id";
774        private final static String ARGS_MESSAGE_ARGS = "ErrorDialog.Message.Args";
775
776        public static ErrorDialog newInstance(AccountCheckSettingsFragment target,
777                int messageId, String... messageArguments) {
778            ErrorDialog fragment = new ErrorDialog();
779            Bundle arguments = new Bundle();
780            arguments.putInt(ARGS_MESSAGE_ID, messageId);
781            arguments.putStringArray(ARGS_MESSAGE_ARGS, messageArguments);
782            fragment.setArguments(arguments);
783            fragment.setTargetFragment(target, 0);
784            return fragment;
785        }
786
787        @Override
788        public Dialog onCreateDialog(Bundle savedInstanceState) {
789            final Context context = getActivity();
790            final Bundle arguments = getArguments();
791            final int messageId = arguments.getInt(ARGS_MESSAGE_ID);
792            final Object[] messageArguments = arguments.getStringArray(ARGS_MESSAGE_ARGS);
793            final AccountCheckSettingsFragment target =
794                    (AccountCheckSettingsFragment) getTargetFragment();
795
796            return new AlertDialog.Builder(context)
797                .setIconAttribute(android.R.attr.alertDialogIcon)
798                .setTitle(context.getString(R.string.account_setup_failed_dlg_title))
799                .setMessage(context.getString(messageId, messageArguments))
800                .setCancelable(true)
801                .setPositiveButton(
802                        context.getString(R.string.account_setup_failed_dlg_edit_details_action),
803                        new DialogInterface.OnClickListener() {
804                            public void onClick(DialogInterface dialog, int which) {
805                                dismiss();
806                                target.onErrorDialogEditButton();
807                            }
808                        })
809                 .create();
810        }
811
812    }
813
814    /**
815     * The "security required" error dialog.  This is presented whenever an exchange account
816     * reports that it will require security policy control, and provide the user with the
817     * opportunity to accept or deny this.
818     *
819     * If the user clicks OK, calls onSecurityRequiredDialogResultOk(true) which reports back
820     * to the target as if the settings check was "ok".  If the user clicks "cancel", calls
821     * onSecurityRequiredDialogResultOk(false) which simply closes the checker (this is the
822     * same as any other failed check.)
823     */
824    public static class SecurityRequiredDialog extends DialogFragment {
825        @SuppressWarnings("hiding")
826        public final static String TAG = "SecurityRequiredDialog";
827
828        // Bundle keys for arguments
829        private final static String ARGS_HOST_NAME = "SecurityRequiredDialog.HostName";
830
831        public static SecurityRequiredDialog newInstance(AccountCheckSettingsFragment target,
832                String hostName) {
833            SecurityRequiredDialog fragment = new SecurityRequiredDialog();
834            Bundle arguments = new Bundle();
835            arguments.putString(ARGS_HOST_NAME, hostName);
836            fragment.setArguments(arguments);
837            fragment.setTargetFragment(target, 0);
838            return fragment;
839        }
840
841        @Override
842        public Dialog onCreateDialog(Bundle savedInstanceState) {
843            final Context context = getActivity();
844            final Bundle arguments = getArguments();
845            final String hostName = arguments.getString(ARGS_HOST_NAME);
846            final AccountCheckSettingsFragment target =
847                    (AccountCheckSettingsFragment) getTargetFragment();
848
849            return new AlertDialog.Builder(context)
850                .setIconAttribute(android.R.attr.alertDialogIcon)
851                .setTitle(context.getString(R.string.account_setup_security_required_title))
852                .setMessage(context.getString(
853                        R.string.account_setup_security_policies_required_fmt, hostName))
854                .setCancelable(true)
855                .setPositiveButton(
856                        context.getString(R.string.okay_action),
857                        new DialogInterface.OnClickListener() {
858                            public void onClick(DialogInterface dialog, int which) {
859                                dismiss();
860                                target.onSecurityRequiredDialogResultOk(true);
861                            }
862                        })
863                .setNegativeButton(
864                        context.getString(R.string.cancel_action),
865                        new DialogInterface.OnClickListener() {
866                            public void onClick(DialogInterface dialog, int which) {
867                                dismiss();
868                                target.onSecurityRequiredDialogResultOk(false);
869                            }
870                        })
871                 .create();
872        }
873
874    }
875
876}
877