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