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