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