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