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