Home | History | Annotate | Download | only in setup
      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 
     17 package com.android.email.activity.setup;
     18 
     19 import android.app.Activity;
     20 import android.app.AlertDialog;
     21 import android.app.Dialog;
     22 import android.app.DialogFragment;
     23 import android.app.Fragment;
     24 import android.app.FragmentManager;
     25 import android.app.ProgressDialog;
     26 import android.content.Context;
     27 import android.content.DialogInterface;
     28 import android.os.AsyncTask;
     29 import android.os.Bundle;
     30 import android.text.TextUtils;
     31 
     32 import com.android.email.R;
     33 import com.android.email.mail.Sender;
     34 import com.android.email.mail.Store;
     35 import com.android.email.service.EmailServiceUtils;
     36 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
     37 import com.android.emailcommon.Logging;
     38 import com.android.emailcommon.mail.MessagingException;
     39 import com.android.emailcommon.provider.Account;
     40 import com.android.emailcommon.provider.HostAuth;
     41 import com.android.emailcommon.provider.Policy;
     42 import com.android.emailcommon.service.EmailServiceProxy;
     43 import com.android.emailcommon.utility.Utility;
     44 import 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  */
     57 public 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