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.Fragment;
     21 import android.app.FragmentManager;
     22 import android.content.Context;
     23 import android.os.AsyncTask;
     24 import android.os.Bundle;
     25 
     26 import com.android.email.R;
     27 import com.android.email.mail.Sender;
     28 import com.android.email.mail.Store;
     29 import com.android.email.service.EmailServiceUtils;
     30 import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
     31 import com.android.emailcommon.Logging;
     32 import com.android.emailcommon.mail.MessagingException;
     33 import com.android.emailcommon.provider.Account;
     34 import com.android.emailcommon.provider.HostAuth;
     35 import com.android.emailcommon.provider.Policy;
     36 import com.android.emailcommon.service.EmailServiceProxy;
     37 import com.android.emailcommon.service.HostAuthCompat;
     38 import com.android.emailcommon.utility.Utility;
     39 import com.android.mail.utils.LogUtils;
     40 
     41 /**
     42  * Check incoming or outgoing settings, or perform autodiscovery.
     43  *
     44  * There are three components that work together.  1. This fragment is retained and non-displayed,
     45  * and controls the overall process.  2. An AsyncTask that works with the stores/services to
     46  * check the accounts settings.  3. A stateless progress dialog (which will be recreated on
     47  * orientation changes).
     48  *
     49  * There are also two lightweight error dialogs which are used for notification of terminal
     50  * conditions.
     51  */
     52 public class AccountCheckSettingsFragment extends Fragment {
     53 
     54     public final static String TAG = "AccountCheckStgFrag";
     55 
     56     // State
     57     private final static int STATE_START = 0;
     58     private final static int STATE_CHECK_AUTODISCOVER = 1;
     59     private final static int STATE_CHECK_INCOMING = 2;
     60     private final static int STATE_CHECK_OUTGOING = 3;
     61     private final static int STATE_CHECK_OK = 4;                    // terminal
     62     private final static int STATE_CHECK_SHOW_SECURITY = 5;         // terminal
     63     private final static int STATE_CHECK_ERROR = 6;                 // terminal
     64     private final static int STATE_AUTODISCOVER_AUTH_DIALOG = 7;    // terminal
     65     private final static int STATE_AUTODISCOVER_RESULT = 8;         // terminal
     66     private int mState = STATE_START;
     67 
     68     // Args
     69     private final static String ARGS_MODE = "mode";
     70 
     71     private int mMode;
     72 
     73     // Support for UI
     74     private boolean mAttached;
     75     private boolean mPaused = false;
     76     private MessagingException mProgressException;
     77 
     78     // Support for AsyncTask and account checking
     79     AccountCheckTask mAccountCheckTask;
     80 
     81     // Result codes returned by onCheckSettingsAutoDiscoverComplete.
     82     /** AutoDiscover completed successfully with server setup data */
     83     public final static int AUTODISCOVER_OK = 0;
     84     /** AutoDiscover completed with no data (no server or AD not supported) */
     85     public final static int AUTODISCOVER_NO_DATA = 1;
     86     /** AutoDiscover reported authentication error */
     87     public final static int AUTODISCOVER_AUTHENTICATION = 2;
     88 
     89     /**
     90      * Callback interface for any target or activity doing account check settings
     91      */
     92     public interface Callback {
     93         /**
     94          * Called when CheckSettings completed
     95          */
     96         void onCheckSettingsComplete();
     97 
     98         /**
     99          * Called when we determine that a security policy will need to be installed
    100          * @param hostName Passed back from the MessagingException
    101          */
    102         void onCheckSettingsSecurityRequired(String hostName);
    103 
    104         /**
    105          * Called when we receive an error while validating the account
    106          * @param reason from
    107          *      {@link CheckSettingsErrorDialogFragment#getReasonFromException(MessagingException)}
    108          * @param message from
    109          *      {@link CheckSettingsErrorDialogFragment#getErrorString(Context, MessagingException)}
    110          */
    111         void onCheckSettingsError(int reason, String message);
    112 
    113         /**
    114          * Called when autodiscovery completes.
    115          * @param result autodiscovery result code - success is AUTODISCOVER_OK
    116          */
    117         void onCheckSettingsAutoDiscoverComplete(int result);
    118     }
    119 
    120     // Public no-args constructor needed for fragment re-instantiation
    121     public AccountCheckSettingsFragment() {}
    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) {
    129         final AccountCheckSettingsFragment f = new AccountCheckSettingsFragment();
    130         final Bundle b = new Bundle(1);
    131         b.putInt(ARGS_MODE, mode);
    132         f.setArguments(b);
    133         return f;
    134     }
    135 
    136     /**
    137      * Fragment initialization.  Because we never implement onCreateView, and call
    138      * setRetainInstance here, this creates an invisible, persistent, "worker" fragment.
    139      */
    140     @Override
    141     public void onCreate(Bundle savedInstanceState) {
    142         super.onCreate(savedInstanceState);
    143         setRetainInstance(true);
    144         mMode = getArguments().getInt(ARGS_MODE);
    145     }
    146 
    147     /**
    148      * This is called when the Fragment's Activity is ready to go, after
    149      * its content view has been installed; it is called both after
    150      * the initial fragment creation and after the fragment is re-attached
    151      * to a new activity.
    152      */
    153     @Override
    154     public void onActivityCreated(Bundle savedInstanceState) {
    155         super.onActivityCreated(savedInstanceState);
    156         mAttached = true;
    157 
    158         // If this is the first time, start the AsyncTask
    159         if (mAccountCheckTask == null) {
    160             final SetupDataFragment.SetupDataContainer container =
    161                     (SetupDataFragment.SetupDataContainer) getActivity();
    162             // TODO: don't pass in the whole SetupDataFragment
    163             mAccountCheckTask = (AccountCheckTask)
    164                     new AccountCheckTask(getActivity().getApplicationContext(), this, mMode,
    165                             container.getSetupData())
    166                     .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    167         }
    168     }
    169 
    170     /**
    171      * When resuming, restart the progress/error UI if necessary by re-reporting previous values
    172      */
    173     @Override
    174     public void onResume() {
    175         super.onResume();
    176         mPaused = false;
    177 
    178         if (mState != STATE_START) {
    179             reportProgress(mState, mProgressException);
    180         }
    181     }
    182 
    183     @Override
    184     public void onPause() {
    185         super.onPause();
    186         mPaused = true;
    187     }
    188 
    189     /**
    190      * This is called when the fragment is going away.  It is NOT called
    191      * when the fragment is being propagated between activity instances.
    192      */
    193     @Override
    194     public void onDestroy() {
    195         super.onDestroy();
    196         if (mAccountCheckTask != null) {
    197             Utility.cancelTaskInterrupt(mAccountCheckTask);
    198             mAccountCheckTask = null;
    199         }
    200     }
    201 
    202     /**
    203      * This is called right before the fragment is detached from its current activity instance.
    204      * All reporting and callbacks are halted until we reattach.
    205      */
    206     @Override
    207     public void onDetach() {
    208         super.onDetach();
    209         mAttached = false;
    210     }
    211 
    212     /**
    213      * The worker (AsyncTask) will call this (in the UI thread) to report progress.  If we are
    214      * attached to an activity, update the progress immediately;  If not, simply hold the
    215      * progress for later.
    216      * @param newState The new progress state being reported
    217      */
    218     private void reportProgress(int newState, MessagingException ex) {
    219         mState = newState;
    220         mProgressException = ex;
    221 
    222         // If we are attached, create, recover, and/or update the dialog
    223         if (mAttached && !mPaused) {
    224             final FragmentManager fm = getFragmentManager();
    225 
    226             switch (newState) {
    227                 case STATE_CHECK_OK:
    228                     // immediately terminate, clean up, and report back
    229                     getCallbackTarget().onCheckSettingsComplete();
    230                     break;
    231                 case STATE_CHECK_SHOW_SECURITY:
    232                     // report that we need to accept a security policy
    233                     String hostName = ex.getMessage();
    234                     if (hostName != null) {
    235                         hostName = hostName.trim();
    236                     }
    237                     getCallbackTarget().onCheckSettingsSecurityRequired(hostName);
    238                     break;
    239                 case STATE_CHECK_ERROR:
    240                 case STATE_AUTODISCOVER_AUTH_DIALOG:
    241                     // report that we had an error
    242                     final int reason =
    243                             CheckSettingsErrorDialogFragment.getReasonFromException(ex);
    244                     final String errorMessage =
    245                             CheckSettingsErrorDialogFragment.getErrorString(getActivity(), ex);
    246                     getCallbackTarget().onCheckSettingsError(reason, errorMessage);
    247                     break;
    248                 case STATE_AUTODISCOVER_RESULT:
    249                     final HostAuth autoDiscoverResult = ((AutoDiscoverResults) ex).mHostAuth;
    250                     // report autodiscover results back to target fragment or activity
    251                     getCallbackTarget().onCheckSettingsAutoDiscoverComplete(
    252                             (autoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA);
    253                     break;
    254                 default:
    255                     // Display a normal progress message
    256                     CheckSettingsProgressDialogFragment checkingDialog =
    257                             (CheckSettingsProgressDialogFragment)
    258                                     fm.findFragmentByTag(CheckSettingsProgressDialogFragment.TAG);
    259 
    260                     if (checkingDialog != null) {
    261                         checkingDialog.updateProgress(mState);
    262                     }
    263                     break;
    264             }
    265         }
    266     }
    267 
    268     /**
    269      * Find the callback target, either a target fragment or the activity
    270      */
    271     private Callback getCallbackTarget() {
    272         final Fragment target = getTargetFragment();
    273         if (target instanceof Callback) {
    274             return (Callback) target;
    275         }
    276         Activity activity = getActivity();
    277         if (activity instanceof Callback) {
    278             return (Callback) activity;
    279         }
    280         throw new IllegalStateException();
    281     }
    282 
    283     /**
    284      * This exception class is used to report autodiscover results via the reporting mechanism.
    285      */
    286     public static class AutoDiscoverResults extends MessagingException {
    287         public final HostAuth mHostAuth;
    288 
    289         /**
    290          * @param authenticationError true if auth failure, false for result (or no response)
    291          * @param hostAuth null for "no autodiscover", non-null for server info to return
    292          */
    293         public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) {
    294             super(null);
    295             if (authenticationError) {
    296                 mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED;
    297             } else {
    298                 mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT;
    299             }
    300             mHostAuth = hostAuth;
    301         }
    302     }
    303 
    304     /**
    305      * This AsyncTask does the actual account checking
    306      *
    307      * TODO: It would be better to remove the UI complete from here (the exception->string
    308      * conversions).
    309      */
    310     private static class AccountCheckTask extends AsyncTask<Void, Integer, MessagingException> {
    311         final Context mContext;
    312         final AccountCheckSettingsFragment mCallback;
    313         final int mMode;
    314         final SetupDataFragment mSetupData;
    315         final Account mAccount;
    316         final String mStoreHost;
    317         final String mCheckEmail;
    318         final String mCheckPassword;
    319 
    320         /**
    321          * Create task and parameterize it
    322          * @param context application context object
    323          * @param mode bits request operations
    324          * @param setupData {@link SetupDataFragment} holding values to be checked
    325          */
    326         public AccountCheckTask(Context context, AccountCheckSettingsFragment callback, int mode,
    327                 SetupDataFragment setupData) {
    328             mContext = context;
    329             mCallback = callback;
    330             mMode = mode;
    331             mSetupData = setupData;
    332             mAccount = setupData.getAccount();
    333             mStoreHost = mAccount.mHostAuthRecv.mAddress;
    334             mCheckEmail = mAccount.mEmailAddress;
    335             mCheckPassword = mAccount.mHostAuthRecv.mPassword;
    336         }
    337 
    338         @Override
    339         protected MessagingException doInBackground(Void... params) {
    340             try {
    341                 if ((mMode & SetupDataFragment.CHECK_AUTODISCOVER) != 0) {
    342                     if (isCancelled()) return null;
    343                     LogUtils.d(Logging.LOG_TAG, "Begin auto-discover for %s", mCheckEmail);
    344                     publishProgress(STATE_CHECK_AUTODISCOVER);
    345                     final Store store = Store.getInstance(mAccount, mContext);
    346                     final Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
    347                     // Result will be one of:
    348                     //  null: remote exception - proceed to manual setup
    349                     //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
    350                     //  Other error: proceed to manual setup
    351                     //  No error: return autodiscover results
    352                     if (result == null) {
    353                         return new AutoDiscoverResults(false, null);
    354                     }
    355                     int errorCode =
    356                             result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
    357                     if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) {
    358                         return new AutoDiscoverResults(true, null);
    359                     } else if (errorCode != MessagingException.NO_ERROR) {
    360                         return new AutoDiscoverResults(false, null);
    361                     } else {
    362                         final HostAuthCompat hostAuthCompat =
    363                             result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
    364                         HostAuth serverInfo = null;
    365                         if (hostAuthCompat != null) {
    366                             serverInfo = hostAuthCompat.toHostAuth();
    367                         }
    368                         return new AutoDiscoverResults(false, serverInfo);
    369                     }
    370                 }
    371 
    372                 // Check Incoming Settings
    373                 if ((mMode & SetupDataFragment.CHECK_INCOMING) != 0) {
    374                     if (isCancelled()) return null;
    375                     LogUtils.d(Logging.LOG_TAG, "Begin check of incoming email settings");
    376                     publishProgress(STATE_CHECK_INCOMING);
    377                     final Store store = Store.getInstance(mAccount, mContext);
    378                     final Bundle bundle = store.checkSettings();
    379                     if (bundle == null) {
    380                         return new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
    381                     }
    382                     mAccount.mProtocolVersion = bundle.getString(
    383                             EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION);
    384                     int resultCode = bundle.getInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
    385                     final String redirectAddress = bundle.getString(
    386                             EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS, null);
    387                     if (redirectAddress != null) {
    388                         mAccount.mHostAuthRecv.mAddress = redirectAddress;
    389                     }
    390                     // Only show "policies required" if this is a new account setup
    391                     if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED &&
    392                             mAccount.isSaved()) {
    393                         resultCode = MessagingException.NO_ERROR;
    394                     }
    395                     if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
    396                         mSetupData.setPolicy((Policy)bundle.getParcelable(
    397                                 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
    398                         return new MessagingException(resultCode, mStoreHost);
    399                     } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
    400                         final Policy policy = bundle.getParcelable(
    401                                 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET);
    402                         final String unsupported = policy.mProtocolPoliciesUnsupported;
    403                         final String[] data =
    404                                 unsupported.split("" + Policy.POLICY_STRING_DELIMITER);
    405                         return new MessagingException(resultCode, mStoreHost, data);
    406                     } else if (resultCode != MessagingException.NO_ERROR) {
    407                         final String errorMessage;
    408                         errorMessage = bundle.getString(
    409                                 EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
    410                         return new MessagingException(resultCode, errorMessage);
    411                     }
    412                 }
    413 
    414                 final String protocol = mAccount.mHostAuthRecv.mProtocol;
    415                 final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, protocol);
    416 
    417                 // Check Outgoing Settings
    418                 if (info.usesSmtp && (mMode & SetupDataFragment.CHECK_OUTGOING) != 0) {
    419                     if (isCancelled()) return null;
    420                     LogUtils.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
    421                     publishProgress(STATE_CHECK_OUTGOING);
    422                     final Sender sender = Sender.getInstance(mContext, mAccount);
    423                     sender.close();
    424                     sender.open();
    425                     sender.close();
    426                 }
    427 
    428                 // If we reached the end, we completed the check(s) successfully
    429                 return null;
    430             } catch (final MessagingException me) {
    431                 // Some of the legacy account checkers return errors by throwing MessagingException,
    432                 // which we catch and return here.
    433                 return me;
    434             }
    435         }
    436 
    437         /**
    438          * Progress reports (runs in UI thread).  This should be used for real progress only
    439          * (not for errors).
    440          */
    441         @Override
    442         protected void onProgressUpdate(Integer... progress) {
    443             if (isCancelled()) return;
    444             mCallback.reportProgress(progress[0], null);
    445         }
    446 
    447         /**
    448          * Result handler (runs in UI thread).
    449          *
    450          * AutoDiscover authentication errors are handled a bit differently than the
    451          * other errors;  If encountered, we display the error dialog, but we return with
    452          * a different callback used only for AutoDiscover.
    453          *
    454          * @param result null for a successful check;  exception for various errors
    455          */
    456         @Override
    457         protected void onPostExecute(MessagingException result) {
    458             if (isCancelled()) return;
    459             if (result == null) {
    460                 mCallback.reportProgress(STATE_CHECK_OK, null);
    461             } else {
    462                 int progressState = STATE_CHECK_ERROR;
    463                 final int exceptionType = result.getExceptionType();
    464 
    465                 switch (exceptionType) {
    466                     // NOTE: AutoDiscover reports have their own reporting state, handle differently
    467                     // from the other exception types
    468                     case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
    469                         progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
    470                         break;
    471                     case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
    472                         progressState = STATE_AUTODISCOVER_RESULT;
    473                         break;
    474                     // NOTE: Security policies required has its own report state, handle it a bit
    475                     // differently from the other exception types.
    476                     case MessagingException.SECURITY_POLICIES_REQUIRED:
    477                         progressState = STATE_CHECK_SHOW_SECURITY;
    478                         break;
    479                 }
    480                 mCallback.reportProgress(progressState, result);
    481             }
    482         }
    483     }
    484 
    485     /**
    486      * Convert progress to message
    487      */
    488     protected static String getProgressString(Context context, int progress) {
    489         int stringId = 0;
    490         switch (progress) {
    491             case STATE_CHECK_AUTODISCOVER:
    492                 stringId = R.string.account_setup_check_settings_retr_info_msg;
    493                 break;
    494             case STATE_START:
    495             case STATE_CHECK_INCOMING:
    496                 stringId = R.string.account_setup_check_settings_check_incoming_msg;
    497                 break;
    498             case STATE_CHECK_OUTGOING:
    499                 stringId = R.string.account_setup_check_settings_check_outgoing_msg;
    500                 break;
    501         }
    502         if (stringId != 0) {
    503             return context.getString(stringId);
    504         } else {
    505             return null;
    506         }
    507     }
    508 
    509     /**
    510      * Convert mode to initial progress
    511      */
    512     protected static int getProgressForMode(int checkMode) {
    513         switch (checkMode) {
    514             case SetupDataFragment.CHECK_INCOMING:
    515                 return STATE_CHECK_INCOMING;
    516             case SetupDataFragment.CHECK_OUTGOING:
    517                 return STATE_CHECK_OUTGOING;
    518             case SetupDataFragment.CHECK_AUTODISCOVER:
    519                 return STATE_CHECK_AUTODISCOVER;
    520         }
    521         return STATE_START;
    522     }
    523 }
    524