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 MessagingException mProgressException;
83
84    // Support for AsyncTask and account checking
85    AccountCheckTask mAccountCheckTask;
86
87    // Result codes returned by onCheckSettingsComplete.
88    /** Check settings returned successfully */
89    public final static int CHECK_SETTINGS_OK = 0;
90    /** Check settings failed due to connection, authentication, or other server error */
91    public final static int CHECK_SETTINGS_SERVER_ERROR = 1;
92    /** Check settings failed due to user refusing to accept security requirements */
93    public final static int CHECK_SETTINGS_SECURITY_USER_DENY = 2;
94    /** Check settings failed due to certificate being required - user needs to pick immediately. */
95    public final static int CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED = 3;
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                    .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
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, mProgressException);
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     */
204    private void reportProgress(int newState, MessagingException ex) {
205        mState = newState;
206        mProgressException = ex;
207
208        // If we are attached, create, recover, and/or update the dialog
209        if (mAttached) {
210            FragmentManager fm = getFragmentManager();
211
212            switch (newState) {
213                case STATE_CHECK_OK:
214                    // immediately terminate, clean up, and report back
215                    // 1. get rid of progress dialog (if any)
216                    recoverAndDismissCheckingDialog();
217                    // 2. exit self
218                    fm.popBackStack();
219                    // 3. report OK back to target fragment or activity
220                    getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_OK);
221                    break;
222                case STATE_CHECK_SHOW_SECURITY:
223                    // 1. get rid of progress dialog (if any)
224                    recoverAndDismissCheckingDialog();
225                    // 2. launch the error dialog, if needed
226                    if (fm.findFragmentByTag(SecurityRequiredDialog.TAG) == null) {
227                        String message = ex.getMessage();
228                        if (message != null) {
229                            message = message.trim();
230                        }
231                        SecurityRequiredDialog securityRequiredDialog =
232                                SecurityRequiredDialog.newInstance(this, message);
233                        fm.beginTransaction()
234                                .add(securityRequiredDialog, SecurityRequiredDialog.TAG)
235                                .commit();
236                    }
237                    break;
238                case STATE_CHECK_ERROR:
239                case STATE_AUTODISCOVER_AUTH_DIALOG:
240                    // 1. get rid of progress dialog (if any)
241                    recoverAndDismissCheckingDialog();
242                    // 2. launch the error dialog, if needed
243                    if (fm.findFragmentByTag(ErrorDialog.TAG) == null) {
244                        ErrorDialog errorDialog = ErrorDialog.newInstance(
245                                getActivity(), this, mProgressException);
246                        fm.beginTransaction()
247                                .add(errorDialog, ErrorDialog.TAG)
248                                .commit();
249                    }
250                    break;
251                case STATE_AUTODISCOVER_RESULT:
252                    HostAuth autoDiscoverResult = ((AutoDiscoverResults) ex).mHostAuth;
253                    // 1. get rid of progress dialog (if any)
254                    recoverAndDismissCheckingDialog();
255                    // 2. exit self
256                    fm.popBackStack();
257                    // 3. report back to target fragment or activity
258                    getCallbackTarget().onAutoDiscoverComplete(
259                            (autoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA,
260                            autoDiscoverResult);
261                    break;
262                default:
263                    // Display a normal progress message
264                    mCheckingDialog = (CheckingDialog) fm.findFragmentByTag(CheckingDialog.TAG);
265
266                    if (mCheckingDialog == null) {
267                        mCheckingDialog = CheckingDialog.newInstance(this, mState);
268                        fm.beginTransaction()
269                                .add(mCheckingDialog, CheckingDialog.TAG)
270                                .commit();
271                    } else {
272                        mCheckingDialog.updateProgress(mState);
273                    }
274                    break;
275            }
276        }
277    }
278
279    /**
280     * Find the callback target, either a target fragment or the activity
281     */
282    private Callbacks getCallbackTarget() {
283        Fragment target = getTargetFragment();
284        if (target instanceof Callbacks) {
285            return (Callbacks) target;
286        }
287        Activity activity = getActivity();
288        if (activity instanceof Callbacks) {
289            return (Callbacks) activity;
290        }
291        throw new IllegalStateException();
292    }
293
294    /**
295     * Recover and dismiss the progress dialog fragment
296     */
297    private void recoverAndDismissCheckingDialog() {
298        if (mCheckingDialog == null) {
299            mCheckingDialog = (CheckingDialog)
300                    getFragmentManager().findFragmentByTag(CheckingDialog.TAG);
301        }
302        if (mCheckingDialog != null) {
303            mCheckingDialog.dismissAllowingStateLoss();
304            mCheckingDialog = null;
305        }
306    }
307
308    /**
309     * This is called when the user clicks "cancel" on the progress dialog.  Shuts everything
310     * down and dismisses everything.
311     * This should cause us to remain in the current screen (not accepting the settings)
312     */
313    private void onCheckingDialogCancel() {
314        // 1. kill the checker
315        Utility.cancelTaskInterrupt(mAccountCheckTask);
316        mAccountCheckTask = null;
317        // 2. kill self with no report - this is "cancel"
318        finish();
319    }
320
321    private void onEditCertificateOk() {
322        Callbacks callbackTarget = getCallbackTarget();
323        getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED);
324        finish();
325    }
326
327    /**
328     * This is called when the user clicks "edit" from the error dialog.  The error dialog
329     * should have already dismissed itself.
330     * Depending on the context, the target will remain in the current activity (e.g. editing
331     * settings) or return to its own parent (e.g. enter new credentials).
332     */
333    private void onErrorDialogEditButton() {
334        // 1. handle "edit" - notify callback that we had a problem with the test
335        Callbacks callbackTarget = getCallbackTarget();
336        if (mState == STATE_AUTODISCOVER_AUTH_DIALOG) {
337            // report auth error to target fragment or activity
338            callbackTarget.onAutoDiscoverComplete(AUTODISCOVER_AUTHENTICATION, null);
339        } else {
340            // report check settings failure to target fragment or activity
341            callbackTarget.onCheckSettingsComplete(CHECK_SETTINGS_SERVER_ERROR);
342        }
343        finish();
344    }
345
346    /** Kill self if not already killed. */
347    private void finish() {
348        FragmentManager fm = getFragmentManager();
349        if (fm != null) {
350            fm.popBackStack();
351        }
352    }
353
354    /**
355     * This is called when the user clicks "ok" or "cancel" on the "security required" dialog.
356     * Shuts everything down and dismisses everything, and reports the result appropriately.
357     */
358    private void onSecurityRequiredDialogResultOk(boolean okPressed) {
359        // 1. handle OK/cancel - notify that security is OK and we can proceed
360        Callbacks callbackTarget = getCallbackTarget();
361        callbackTarget.onCheckSettingsComplete(
362                okPressed ? CHECK_SETTINGS_OK : CHECK_SETTINGS_SECURITY_USER_DENY);
363
364        // 2. kill self if not already killed by callback
365        FragmentManager fm = getFragmentManager();
366        if (fm != null) {
367            fm.popBackStack();
368        }
369    }
370
371    /**
372     * This exception class is used to report autodiscover results via the reporting mechanism.
373     */
374    public static class AutoDiscoverResults extends MessagingException {
375        public final HostAuth mHostAuth;
376
377        /**
378         * @param authenticationError true if auth failure, false for result (or no response)
379         * @param hostAuth null for "no autodiscover", non-null for server info to return
380         */
381        public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) {
382            super(null);
383            if (authenticationError) {
384                mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED;
385            } else {
386                mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT;
387            }
388            mHostAuth = hostAuth;
389        }
390    }
391
392    /**
393     * This AsyncTask does the actual account checking
394     *
395     * TODO: It would be better to remove the UI complete from here (the exception->string
396     * conversions).
397     */
398    private class AccountCheckTask extends AsyncTask<Void, Integer, MessagingException> {
399
400        final Context mContext;
401        final int mMode;
402        final Account mAccount;
403        final String mStoreHost;
404        final String mCheckEmail;
405        final String mCheckPassword;
406
407        /**
408         * Create task and parameterize it
409         * @param mode bits request operations
410         * @param checkAccount account holding values to be checked
411         */
412        public AccountCheckTask(int mode, Account checkAccount) {
413            mContext = getActivity().getApplicationContext();
414            mMode = mode;
415            mAccount = checkAccount;
416            mStoreHost = checkAccount.mHostAuthRecv.mAddress;
417            mCheckEmail = checkAccount.mEmailAddress;
418            mCheckPassword = checkAccount.mHostAuthRecv.mPassword;
419        }
420
421        @Override
422        protected MessagingException doInBackground(Void... params) {
423            if (DEBUG_FAKE_CHECK_CYCLE) {
424                return fakeChecker();
425            }
426
427            try {
428                if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) {
429                    if (isCancelled()) return null;
430                    publishProgress(STATE_CHECK_AUTODISCOVER);
431                    Log.d(Logging.LOG_TAG, "Begin auto-discover for " + mCheckEmail);
432                    Store store = Store.getInstance(mAccount, mContext);
433                    Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
434                    // Result will be one of:
435                    //  null: remote exception - proceed to manual setup
436                    //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
437                    //  Other error: proceed to manual setup
438                    //  No error: return autodiscover results
439                    if (result == null) {
440                        return new AutoDiscoverResults(false, null);
441                    }
442                    int errorCode =
443                            result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
444                    if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) {
445                        return new AutoDiscoverResults(true, null);
446                    } else if (errorCode != MessagingException.NO_ERROR) {
447                        return new AutoDiscoverResults(false, null);
448                    } else {
449                        HostAuth serverInfo = (HostAuth)
450                            result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
451                        return new AutoDiscoverResults(false, serverInfo);
452                    }
453                }
454
455                // Check Incoming Settings
456                if ((mMode & SetupData.CHECK_INCOMING) != 0) {
457                    if (isCancelled()) return null;
458                    Log.d(Logging.LOG_TAG, "Begin check of incoming email settings");
459                    publishProgress(STATE_CHECK_INCOMING);
460                    Store store = Store.getInstance(mAccount, mContext);
461                    Bundle bundle = store.checkSettings();
462                    int resultCode = MessagingException.UNSPECIFIED_EXCEPTION;
463                    if (bundle != null) {
464                        resultCode = bundle.getInt(
465                                EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
466                    }
467                    if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
468                        SetupData.setPolicy((Policy)bundle.getParcelable(
469                                EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
470                        return new MessagingException(resultCode, mStoreHost);
471                    } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
472                        String[] data = bundle.getStringArray(
473                                EmailServiceProxy.VALIDATE_BUNDLE_UNSUPPORTED_POLICIES);
474                        return new MessagingException(resultCode, mStoreHost, data);
475                    } else if (resultCode != MessagingException.NO_ERROR) {
476                        String errorMessage =
477                            bundle.getString(EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
478                        return new MessagingException(resultCode, errorMessage);
479                    }
480                }
481
482                // Check Outgoing Settings
483                if ((mMode & SetupData.CHECK_OUTGOING) != 0) {
484                    if (isCancelled()) return null;
485                    Log.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
486                    publishProgress(STATE_CHECK_OUTGOING);
487                    Sender sender = Sender.getInstance(mContext, mAccount);
488                    sender.close();
489                    sender.open();
490                    sender.close();
491                }
492
493                // If we reached the end, we completed the check(s) successfully
494                return null;
495            } catch (final MessagingException me) {
496                // Some of the legacy account checkers return errors by throwing MessagingException,
497                // which we catch and return here.
498                return me;
499            }
500        }
501
502        /**
503         * Dummy background worker, for testing UI only.
504         */
505        private MessagingException fakeChecker() {
506            // Dummy:  Publish a series of progress setups, 2 sec delays between them;
507            // then return "ok" (null)
508            final int DELAY = 2*1000;
509            if (isCancelled()) return null;
510            if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) {
511                publishProgress(STATE_CHECK_AUTODISCOVER);
512                try {
513                    Thread.sleep(DELAY);
514                } catch (InterruptedException e) { }
515                if (DEBUG_FAKE_CHECK_ERR) {
516                    return new MessagingException(MessagingException.AUTHENTICATION_FAILED);
517                }
518                // Return "real" AD results
519                HostAuth auth = new HostAuth();
520                auth.setLogin("user", "password");
521                auth.setConnection(HostAuth.SCHEME_EAS, "testserver.com", 0);
522                return new AutoDiscoverResults(false, auth);
523            }
524            if (isCancelled()) return null;
525            if ((mMode & SetupData.CHECK_INCOMING) != 0) {
526                publishProgress(STATE_CHECK_INCOMING);
527                try {
528                    Thread.sleep(DELAY);
529                } catch (InterruptedException e) { }
530                if (DEBUG_FAKE_CHECK_ERR) {
531                    return new MessagingException(MessagingException.IOERROR);
532                } else if (DEBUG_FORCE_SECURITY_REQUIRED) {
533                    return new MessagingException(MessagingException.SECURITY_POLICIES_REQUIRED);
534                }
535            }
536            if (isCancelled()) return null;
537            if ((mMode & SetupData.CHECK_OUTGOING) != 0) {
538                publishProgress(STATE_CHECK_OUTGOING);
539                try {
540                    Thread.sleep(DELAY);
541                } catch (InterruptedException e) { }
542                if (DEBUG_FAKE_CHECK_ERR) {
543                    return new MessagingException(MessagingException.TLS_REQUIRED);
544                }
545            }
546            return null;
547        }
548
549        /**
550         * Progress reports (runs in UI thread).  This should be used for real progress only
551         * (not for errors).
552         */
553        @Override
554        protected void onProgressUpdate(Integer... progress) {
555            if (isCancelled()) return;
556            reportProgress(progress[0], null);
557        }
558
559        /**
560         * Result handler (runs in UI thread).
561         *
562         * AutoDiscover authentication errors are handled a bit differently than the
563         * other errors;  If encountered, we display the error dialog, but we return with
564         * a different callback used only for AutoDiscover.
565         *
566         * @param result null for a successful check;  exception for various errors
567         */
568        @Override
569        protected void onPostExecute(MessagingException result) {
570            if (isCancelled()) return;
571            if (result == null) {
572                reportProgress(STATE_CHECK_OK, null);
573            } else {
574                int progressState = STATE_CHECK_ERROR;
575                int exceptionType = result.getExceptionType();
576
577                switch (exceptionType) {
578                    // NOTE: AutoDiscover reports have their own reporting state, handle differently
579                    // from the other exception types
580                    case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
581                        progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
582                        break;
583                    case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
584                        progressState = STATE_AUTODISCOVER_RESULT;
585                        break;
586                    // NOTE: Security policies required has its own report state, handle it a bit
587                    // differently from the other exception types.
588                    case MessagingException.SECURITY_POLICIES_REQUIRED:
589                        progressState = STATE_CHECK_SHOW_SECURITY;
590                        break;
591                }
592                reportProgress(progressState, result);
593            }
594        }
595    }
596
597    private static String getErrorString(Context context, MessagingException ex) {
598        int id;
599        String message = ex.getMessage();
600        if (message != null) {
601            message = message.trim();
602        }
603        switch (ex.getExceptionType()) {
604            // The remaining exception types are handled by setting the state to
605            // STATE_CHECK_ERROR (above, default) and conversion to specific error strings.
606            case MessagingException.CERTIFICATE_VALIDATION_ERROR:
607                id = TextUtils.isEmpty(message)
608                        ? R.string.account_setup_failed_dlg_certificate_message
609                        : R.string.account_setup_failed_dlg_certificate_message_fmt;
610                break;
611            case MessagingException.AUTHENTICATION_FAILED:
612            case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
613                id = TextUtils.isEmpty(message)
614                        ? R.string.account_setup_failed_dlg_auth_message
615                        : R.string.account_setup_failed_dlg_auth_message_fmt;
616                break;
617            case MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR:
618                id = R.string.account_setup_failed_check_credentials_message;
619                break;
620            case MessagingException.IOERROR:
621                id = R.string.account_setup_failed_ioerror;
622                break;
623            case MessagingException.TLS_REQUIRED:
624                id = R.string.account_setup_failed_tls_required;
625                break;
626            case MessagingException.AUTH_REQUIRED:
627                id = R.string.account_setup_failed_auth_required;
628                break;
629            case MessagingException.SECURITY_POLICIES_UNSUPPORTED:
630                id = R.string.account_setup_failed_security_policies_unsupported;
631                // Belt and suspenders here; there should always be a non-empty array here
632                String[] unsupportedPolicies = (String[]) ex.getExceptionData();
633                if (unsupportedPolicies == null) {
634                    Log.w(TAG, "No data for unsupported policies?");
635                    break;
636                }
637                // Build a string, concatenating policies we don't support
638                StringBuilder sb = new StringBuilder();
639                boolean first = true;
640                for (String policyName: unsupportedPolicies) {
641                    if (first) {
642                        first = false;
643                    } else {
644                        sb.append(", ");
645                    }
646                    sb.append(policyName);
647                }
648                message = sb.toString();
649                break;
650            case MessagingException.ACCESS_DENIED:
651                id = R.string.account_setup_failed_access_denied;
652                break;
653            case MessagingException.PROTOCOL_VERSION_UNSUPPORTED:
654                id = R.string.account_setup_failed_protocol_unsupported;
655                break;
656            case MessagingException.GENERAL_SECURITY:
657                id = R.string.account_setup_failed_security;
658                break;
659            case MessagingException.CLIENT_CERTIFICATE_REQUIRED:
660                id = R.string.account_setup_failed_certificate_required;
661                break;
662            case MessagingException.CLIENT_CERTIFICATE_ERROR:
663                id = R.string.account_setup_failed_certificate_inaccessible;
664                break;
665            default:
666                id = TextUtils.isEmpty(message)
667                        ? R.string.account_setup_failed_dlg_server_message
668                        : R.string.account_setup_failed_dlg_server_message_fmt;
669                break;
670        }
671        return TextUtils.isEmpty(message)
672                ? context.getString(id)
673                : context.getString(id, message);
674    }
675
676    /**
677     * Simple dialog that shows progress as we work through the settings checks.
678     * This is stateless except for its UI (e.g. current strings) and can be torn down or
679     * recreated at any time without affecting the account checking progress.
680     */
681    public static class CheckingDialog extends DialogFragment {
682        @SuppressWarnings("hiding")
683        public final static String TAG = "CheckProgressDialog";
684
685        // Extras for saved instance state
686        private final String EXTRA_PROGRESS_STRING = "CheckProgressDialog.Progress";
687
688        // UI
689        private String mProgressString;
690
691        /**
692         * Create a dialog that reports progress
693         * @param progress initial progress indication
694         */
695        public static CheckingDialog newInstance(AccountCheckSettingsFragment parentFragment,
696                int progress) {
697            CheckingDialog f = new CheckingDialog();
698            f.setTargetFragment(parentFragment, progress);
699            return f;
700        }
701
702        /**
703         * Update the progress of an existing dialog
704         * @param progress latest progress to be displayed
705         */
706        public void updateProgress(int progress) {
707            mProgressString = getProgressString(progress);
708            AlertDialog dialog = (AlertDialog) getDialog();
709            dialog.setMessage(mProgressString);
710        }
711
712        @Override
713        public Dialog onCreateDialog(Bundle savedInstanceState) {
714            Context context = getActivity();
715            if (savedInstanceState != null) {
716                mProgressString = savedInstanceState.getString(EXTRA_PROGRESS_STRING);
717            }
718            if (mProgressString == null) {
719                mProgressString = getProgressString(getTargetRequestCode());
720            }
721            final AccountCheckSettingsFragment target =
722                (AccountCheckSettingsFragment) getTargetFragment();
723
724            ProgressDialog dialog = new ProgressDialog(context);
725            dialog.setIndeterminate(true);
726            dialog.setMessage(mProgressString);
727            dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
728                    context.getString(R.string.cancel_action),
729                    new DialogInterface.OnClickListener() {
730                        public void onClick(DialogInterface dialog, int which) {
731                            dismiss();
732                            target.onCheckingDialogCancel();
733                        }
734                    });
735            return dialog;
736        }
737
738        /**
739         * Listen for cancellation, which can happen from places other than the
740         * negative button (e.g. touching outside the dialog), and stop the checker
741         */
742        @Override
743        public void onCancel(DialogInterface dialog) {
744            AccountCheckSettingsFragment target =
745                (AccountCheckSettingsFragment) getTargetFragment();
746            target.onCheckingDialogCancel();
747            super.onCancel(dialog);
748        }
749
750        @Override
751        public void onSaveInstanceState(Bundle outState) {
752            super.onSaveInstanceState(outState);
753            outState.putString(EXTRA_PROGRESS_STRING, mProgressString);
754        }
755
756        /**
757         * Convert progress to message
758         */
759        private String getProgressString(int progress) {
760            int stringId = 0;
761            switch (progress) {
762                case STATE_CHECK_AUTODISCOVER:
763                    stringId = R.string.account_setup_check_settings_retr_info_msg;
764                    break;
765                case STATE_CHECK_INCOMING:
766                    stringId = R.string.account_setup_check_settings_check_incoming_msg;
767                    break;
768                case STATE_CHECK_OUTGOING:
769                    stringId = R.string.account_setup_check_settings_check_outgoing_msg;
770                    break;
771            }
772            return getActivity().getString(stringId);
773        }
774    }
775
776    /**
777     * The standard error dialog.  Calls back to onErrorDialogButton().
778     */
779    public static class ErrorDialog extends DialogFragment {
780        @SuppressWarnings("hiding")
781        public final static String TAG = "ErrorDialog";
782
783        // Bundle keys for arguments
784        private final static String ARGS_MESSAGE = "ErrorDialog.Message";
785        private final static String ARGS_EXCEPTION_ID = "ErrorDialog.ExceptionId";
786
787        /**
788         * Use {@link #newInstance} This public constructor is still required so
789         * that DialogFragment state can be automatically restored by the
790         * framework.
791         */
792        public ErrorDialog() {
793        }
794
795        public static ErrorDialog newInstance(Context context, AccountCheckSettingsFragment target,
796                MessagingException ex) {
797            ErrorDialog fragment = new ErrorDialog();
798            Bundle arguments = new Bundle();
799            arguments.putString(ARGS_MESSAGE, getErrorString(context, ex));
800            arguments.putInt(ARGS_EXCEPTION_ID, ex.getExceptionType());
801            fragment.setArguments(arguments);
802            fragment.setTargetFragment(target, 0);
803            return fragment;
804        }
805
806        @Override
807        public Dialog onCreateDialog(Bundle savedInstanceState) {
808            final Context context = getActivity();
809            final Bundle arguments = getArguments();
810            final String message = arguments.getString(ARGS_MESSAGE);
811            final int exceptionId = arguments.getInt(ARGS_EXCEPTION_ID);
812            final AccountCheckSettingsFragment target =
813                    (AccountCheckSettingsFragment) getTargetFragment();
814
815            AlertDialog.Builder builder = new AlertDialog.Builder(context)
816                .setIconAttribute(android.R.attr.alertDialogIcon)
817                .setTitle(context.getString(R.string.account_setup_failed_dlg_title))
818                .setMessage(message)
819                .setCancelable(true);
820
821            if (exceptionId == MessagingException.CLIENT_CERTIFICATE_REQUIRED) {
822                // Certificate error - show two buttons so the host fragment can auto pop
823                // into the appropriate flow.
824                builder.setPositiveButton(
825                        context.getString(android.R.string.ok),
826                        new DialogInterface.OnClickListener() {
827                            public void onClick(DialogInterface dialog, int which) {
828                                dismiss();
829                                target.onEditCertificateOk();
830                            }
831                        });
832                builder.setNegativeButton(
833                        context.getString(android.R.string.cancel),
834                        new DialogInterface.OnClickListener() {
835                            public void onClick(DialogInterface dialog, int which) {
836                                dismiss();
837                                target.onErrorDialogEditButton();
838                            }
839                        });
840
841            } else {
842                // "Normal" error - just use a single "Edit details" button.
843                builder.setPositiveButton(
844                        context.getString(R.string.account_setup_failed_dlg_edit_details_action),
845                        new DialogInterface.OnClickListener() {
846                            public void onClick(DialogInterface dialog, int which) {
847                                dismiss();
848                                target.onErrorDialogEditButton();
849                            }
850                        });
851            }
852
853            return builder.create();
854        }
855
856    }
857
858    /**
859     * The "security required" error dialog.  This is presented whenever an exchange account
860     * reports that it will require security policy control, and provide the user with the
861     * opportunity to accept or deny this.
862     *
863     * If the user clicks OK, calls onSecurityRequiredDialogResultOk(true) which reports back
864     * to the target as if the settings check was "ok".  If the user clicks "cancel", calls
865     * onSecurityRequiredDialogResultOk(false) which simply closes the checker (this is the
866     * same as any other failed check.)
867     */
868    public static class SecurityRequiredDialog extends DialogFragment {
869        @SuppressWarnings("hiding")
870        public final static String TAG = "SecurityRequiredDialog";
871
872        // Bundle keys for arguments
873        private final static String ARGS_HOST_NAME = "SecurityRequiredDialog.HostName";
874
875        public static SecurityRequiredDialog newInstance(AccountCheckSettingsFragment target,
876                String hostName) {
877            SecurityRequiredDialog fragment = new SecurityRequiredDialog();
878            Bundle arguments = new Bundle();
879            arguments.putString(ARGS_HOST_NAME, hostName);
880            fragment.setArguments(arguments);
881            fragment.setTargetFragment(target, 0);
882            return fragment;
883        }
884
885        @Override
886        public Dialog onCreateDialog(Bundle savedInstanceState) {
887            final Context context = getActivity();
888            final Bundle arguments = getArguments();
889            final String hostName = arguments.getString(ARGS_HOST_NAME);
890            final AccountCheckSettingsFragment target =
891                    (AccountCheckSettingsFragment) getTargetFragment();
892
893            return new AlertDialog.Builder(context)
894                .setIconAttribute(android.R.attr.alertDialogIcon)
895                .setTitle(context.getString(R.string.account_setup_security_required_title))
896                .setMessage(context.getString(
897                        R.string.account_setup_security_policies_required_fmt, hostName))
898                .setCancelable(true)
899                .setPositiveButton(
900                        context.getString(R.string.okay_action),
901                        new DialogInterface.OnClickListener() {
902                            public void onClick(DialogInterface dialog, int which) {
903                                dismiss();
904                                target.onSecurityRequiredDialogResultOk(true);
905                            }
906                        })
907                .setNegativeButton(
908                        context.getString(R.string.cancel_action),
909                        new DialogInterface.OnClickListener() {
910                            public void onClick(DialogInterface dialog, int which) {
911                                dismiss();
912                                target.onSecurityRequiredDialogResultOk(false);
913                            }
914                        })
915                 .create();
916        }
917
918    }
919
920}
921