AccountCheckSettingsFragment.java revision 7683a431173d242e55afced34175e1178764e757
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.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                id = TextUtils.isEmpty(message)
613                        ? R.string.account_setup_failed_dlg_auth_message
614                        : R.string.account_setup_failed_dlg_auth_message_fmt;
615                break;
616            case MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR:
617                id = R.string.account_setup_failed_check_credentials_message;
618                break;
619            case MessagingException.IOERROR:
620                id = R.string.account_setup_failed_ioerror;
621                break;
622            case MessagingException.TLS_REQUIRED:
623                id = R.string.account_setup_failed_tls_required;
624                break;
625            case MessagingException.AUTH_REQUIRED:
626                id = R.string.account_setup_failed_auth_required;
627                break;
628            case MessagingException.SECURITY_POLICIES_UNSUPPORTED:
629                id = R.string.account_setup_failed_security_policies_unsupported;
630                // Belt and suspenders here; there should always be a non-empty array here
631                String[] unsupportedPolicies = (String[]) ex.getExceptionData();
632                if (unsupportedPolicies == null) {
633                    Log.w(TAG, "No data for unsupported policies?");
634                    break;
635                }
636                // Build a string, concatenating policies we don't support
637                StringBuilder sb = new StringBuilder();
638                boolean first = true;
639                for (String policyName: unsupportedPolicies) {
640                    if (first) {
641                        first = false;
642                    } else {
643                        sb.append(", ");
644                    }
645                    sb.append(policyName);
646                }
647                message = sb.toString();
648                break;
649            case MessagingException.ACCESS_DENIED:
650                id = R.string.account_setup_failed_access_denied;
651                break;
652            case MessagingException.PROTOCOL_VERSION_UNSUPPORTED:
653                id = R.string.account_setup_failed_protocol_unsupported;
654                break;
655            case MessagingException.GENERAL_SECURITY:
656                id = R.string.account_setup_failed_security;
657                break;
658            case MessagingException.CLIENT_CERTIFICATE_REQUIRED:
659                id = R.string.account_setup_failed_certificate_required;
660                break;
661            case MessagingException.CLIENT_CERTIFICATE_ERROR:
662                id = R.string.account_setup_failed_certificate_inaccessible;
663                break;
664            default:
665                id = TextUtils.isEmpty(message)
666                        ? R.string.account_setup_failed_dlg_server_message
667                        : R.string.account_setup_failed_dlg_server_message_fmt;
668                break;
669        }
670        return TextUtils.isEmpty(message)
671                ? context.getString(id)
672                : context.getString(id, message);
673    }
674
675    /**
676     * Simple dialog that shows progress as we work through the settings checks.
677     * This is stateless except for its UI (e.g. current strings) and can be torn down or
678     * recreated at any time without affecting the account checking progress.
679     */
680    public static class CheckingDialog extends DialogFragment {
681        @SuppressWarnings("hiding")
682        public final static String TAG = "CheckProgressDialog";
683
684        // Extras for saved instance state
685        private final String EXTRA_PROGRESS_STRING = "CheckProgressDialog.Progress";
686
687        // UI
688        private String mProgressString;
689
690        /**
691         * Create a dialog that reports progress
692         * @param progress initial progress indication
693         */
694        public static CheckingDialog newInstance(AccountCheckSettingsFragment parentFragment,
695                int progress) {
696            CheckingDialog f = new CheckingDialog();
697            f.setTargetFragment(parentFragment, progress);
698            return f;
699        }
700
701        /**
702         * Update the progress of an existing dialog
703         * @param progress latest progress to be displayed
704         */
705        public void updateProgress(int progress) {
706            mProgressString = getProgressString(progress);
707            AlertDialog dialog = (AlertDialog) getDialog();
708            dialog.setMessage(mProgressString);
709        }
710
711        @Override
712        public Dialog onCreateDialog(Bundle savedInstanceState) {
713            Context context = getActivity();
714            if (savedInstanceState != null) {
715                mProgressString = savedInstanceState.getString(EXTRA_PROGRESS_STRING);
716            }
717            if (mProgressString == null) {
718                mProgressString = getProgressString(getTargetRequestCode());
719            }
720            final AccountCheckSettingsFragment target =
721                (AccountCheckSettingsFragment) getTargetFragment();
722
723            ProgressDialog dialog = new ProgressDialog(context);
724            dialog.setIndeterminate(true);
725            dialog.setMessage(mProgressString);
726            dialog.setButton(DialogInterface.BUTTON_NEGATIVE,
727                    context.getString(R.string.cancel_action),
728                    new DialogInterface.OnClickListener() {
729                        public void onClick(DialogInterface dialog, int which) {
730                            dismiss();
731                            target.onCheckingDialogCancel();
732                        }
733                    });
734            return dialog;
735        }
736
737        /**
738         * Listen for cancellation, which can happen from places other than the
739         * negative button (e.g. touching outside the dialog), and stop the checker
740         */
741        @Override
742        public void onCancel(DialogInterface dialog) {
743            AccountCheckSettingsFragment target =
744                (AccountCheckSettingsFragment) getTargetFragment();
745            target.onCheckingDialogCancel();
746            super.onCancel(dialog);
747        }
748
749        @Override
750        public void onSaveInstanceState(Bundle outState) {
751            super.onSaveInstanceState(outState);
752            outState.putString(EXTRA_PROGRESS_STRING, mProgressString);
753        }
754
755        /**
756         * Convert progress to message
757         */
758        private String getProgressString(int progress) {
759            int stringId = 0;
760            switch (progress) {
761                case STATE_CHECK_AUTODISCOVER:
762                    stringId = R.string.account_setup_check_settings_retr_info_msg;
763                    break;
764                case STATE_CHECK_INCOMING:
765                    stringId = R.string.account_setup_check_settings_check_incoming_msg;
766                    break;
767                case STATE_CHECK_OUTGOING:
768                    stringId = R.string.account_setup_check_settings_check_outgoing_msg;
769                    break;
770            }
771            return getActivity().getString(stringId);
772        }
773    }
774
775    /**
776     * The standard error dialog.  Calls back to onErrorDialogButton().
777     */
778    public static class ErrorDialog extends DialogFragment {
779        @SuppressWarnings("hiding")
780        public final static String TAG = "ErrorDialog";
781
782        // Bundle keys for arguments
783        private final static String ARGS_MESSAGE = "ErrorDialog.Message";
784        private final static String ARGS_EXCEPTION_ID = "ErrorDialog.ExceptionId";
785
786        /** Use {@link #newInstance} */
787        private ErrorDialog() {
788        }
789
790        public static ErrorDialog newInstance(Context context, AccountCheckSettingsFragment target,
791                MessagingException ex) {
792            ErrorDialog fragment = new ErrorDialog();
793            Bundle arguments = new Bundle();
794            arguments.putString(ARGS_MESSAGE, getErrorString(context, ex));
795            arguments.putInt(ARGS_EXCEPTION_ID, ex.getExceptionType());
796            fragment.setArguments(arguments);
797            fragment.setTargetFragment(target, 0);
798            return fragment;
799        }
800
801        @Override
802        public Dialog onCreateDialog(Bundle savedInstanceState) {
803            final Context context = getActivity();
804            final Bundle arguments = getArguments();
805            final String message = arguments.getString(ARGS_MESSAGE);
806            final int exceptionId = arguments.getInt(ARGS_EXCEPTION_ID);
807            final AccountCheckSettingsFragment target =
808                    (AccountCheckSettingsFragment) getTargetFragment();
809
810            AlertDialog.Builder builder = new AlertDialog.Builder(context)
811                .setIconAttribute(android.R.attr.alertDialogIcon)
812                .setTitle(context.getString(R.string.account_setup_failed_dlg_title))
813                .setMessage(message)
814                .setCancelable(true);
815
816            if (exceptionId == MessagingException.CLIENT_CERTIFICATE_REQUIRED) {
817                // Certificate error - show two buttons so the host fragment can auto pop
818                // into the appropriate flow.
819                builder.setPositiveButton(
820                        context.getString(android.R.string.ok),
821                        new DialogInterface.OnClickListener() {
822                            public void onClick(DialogInterface dialog, int which) {
823                                dismiss();
824                                target.onEditCertificateOk();
825                            }
826                        });
827                builder.setNegativeButton(
828                        context.getString(android.R.string.cancel),
829                        new DialogInterface.OnClickListener() {
830                            public void onClick(DialogInterface dialog, int which) {
831                                dismiss();
832                                target.onErrorDialogEditButton();
833                            }
834                        });
835
836            } else {
837                // "Normal" error - just use a single "Edit details" button.
838                builder.setPositiveButton(
839                        context.getString(R.string.account_setup_failed_dlg_edit_details_action),
840                        new DialogInterface.OnClickListener() {
841                            public void onClick(DialogInterface dialog, int which) {
842                                dismiss();
843                                target.onErrorDialogEditButton();
844                            }
845                        });
846            }
847
848            return builder.create();
849        }
850
851    }
852
853    /**
854     * The "security required" error dialog.  This is presented whenever an exchange account
855     * reports that it will require security policy control, and provide the user with the
856     * opportunity to accept or deny this.
857     *
858     * If the user clicks OK, calls onSecurityRequiredDialogResultOk(true) which reports back
859     * to the target as if the settings check was "ok".  If the user clicks "cancel", calls
860     * onSecurityRequiredDialogResultOk(false) which simply closes the checker (this is the
861     * same as any other failed check.)
862     */
863    public static class SecurityRequiredDialog extends DialogFragment {
864        @SuppressWarnings("hiding")
865        public final static String TAG = "SecurityRequiredDialog";
866
867        // Bundle keys for arguments
868        private final static String ARGS_HOST_NAME = "SecurityRequiredDialog.HostName";
869
870        public static SecurityRequiredDialog newInstance(AccountCheckSettingsFragment target,
871                String hostName) {
872            SecurityRequiredDialog fragment = new SecurityRequiredDialog();
873            Bundle arguments = new Bundle();
874            arguments.putString(ARGS_HOST_NAME, hostName);
875            fragment.setArguments(arguments);
876            fragment.setTargetFragment(target, 0);
877            return fragment;
878        }
879
880        @Override
881        public Dialog onCreateDialog(Bundle savedInstanceState) {
882            final Context context = getActivity();
883            final Bundle arguments = getArguments();
884            final String hostName = arguments.getString(ARGS_HOST_NAME);
885            final AccountCheckSettingsFragment target =
886                    (AccountCheckSettingsFragment) getTargetFragment();
887
888            return new AlertDialog.Builder(context)
889                .setIconAttribute(android.R.attr.alertDialogIcon)
890                .setTitle(context.getString(R.string.account_setup_security_required_title))
891                .setMessage(context.getString(
892                        R.string.account_setup_security_policies_required_fmt, hostName))
893                .setCancelable(true)
894                .setPositiveButton(
895                        context.getString(R.string.okay_action),
896                        new DialogInterface.OnClickListener() {
897                            public void onClick(DialogInterface dialog, int which) {
898                                dismiss();
899                                target.onSecurityRequiredDialogResultOk(true);
900                            }
901                        })
902                .setNegativeButton(
903                        context.getString(R.string.cancel_action),
904                        new DialogInterface.OnClickListener() {
905                            public void onClick(DialogInterface dialog, int which) {
906                                dismiss();
907                                target.onSecurityRequiredDialogResultOk(false);
908                            }
909                        })
910                 .create();
911        }
912
913    }
914
915}
916