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 mCheckPassword;
    318         final String mCheckEmail;
    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             if (mAccount.mHostAuthRecv != null) {
    334                 mStoreHost = mAccount.mHostAuthRecv.mAddress;
    335                 mCheckPassword = mAccount.mHostAuthRecv.mPassword;
    336             } else {
    337                 mStoreHost = null;
    338                 mCheckPassword = null;
    339             }
    340             mCheckEmail = mAccount.mEmailAddress;
    341         }
    342 
    343         @Override
    344         protected MessagingException doInBackground(Void... params) {
    345             try {
    346                 if ((mMode & SetupDataFragment.CHECK_AUTODISCOVER) != 0) {
    347                     if (isCancelled()) return null;
    348                     LogUtils.d(Logging.LOG_TAG, "Begin auto-discover for %s", mCheckEmail);
    349                     publishProgress(STATE_CHECK_AUTODISCOVER);
    350                     final Store store = Store.getInstance(mAccount, mContext);
    351                     final Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword);
    352                     // Result will be one of:
    353                     //  null: remote exception - proceed to manual setup
    354                     //  MessagingException.AUTHENTICATION_FAILED: username/password rejected
    355                     //  Other error: proceed to manual setup
    356                     //  No error: return autodiscover results
    357                     if (result == null) {
    358                         return new AutoDiscoverResults(false, null);
    359                     }
    360                     int errorCode =
    361                             result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE);
    362                     if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) {
    363                         return new AutoDiscoverResults(true, null);
    364                     } else if (errorCode != MessagingException.NO_ERROR) {
    365                         return new AutoDiscoverResults(false, null);
    366                     } else {
    367                         final HostAuthCompat hostAuthCompat =
    368                             result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH);
    369                         HostAuth serverInfo = null;
    370                         if (hostAuthCompat != null) {
    371                             serverInfo = hostAuthCompat.toHostAuth();
    372                         }
    373                         return new AutoDiscoverResults(false, serverInfo);
    374                     }
    375                 }
    376 
    377                 // Check Incoming Settings
    378                 if ((mMode & SetupDataFragment.CHECK_INCOMING) != 0) {
    379                     if (isCancelled()) return null;
    380                     LogUtils.d(Logging.LOG_TAG, "Begin check of incoming email settings");
    381                     publishProgress(STATE_CHECK_INCOMING);
    382                     final Store store = Store.getInstance(mAccount, mContext);
    383                     final Bundle bundle = store.checkSettings();
    384                     if (bundle == null) {
    385                         return new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION);
    386                     }
    387                     mAccount.mProtocolVersion = bundle.getString(
    388                             EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION);
    389                     int resultCode = bundle.getInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE);
    390                     final String redirectAddress = bundle.getString(
    391                             EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS, null);
    392                     if (redirectAddress != null) {
    393                         mAccount.mHostAuthRecv.mAddress = redirectAddress;
    394                     }
    395                     // Only show "policies required" if this is a new account setup
    396                     if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED &&
    397                             mAccount.isSaved()) {
    398                         resultCode = MessagingException.NO_ERROR;
    399                     }
    400                     if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) {
    401                         mSetupData.setPolicy((Policy)bundle.getParcelable(
    402                                 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET));
    403                         return new MessagingException(resultCode, mStoreHost);
    404                     } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) {
    405                         final Policy policy = bundle.getParcelable(
    406                                 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET);
    407                         final String unsupported = policy.mProtocolPoliciesUnsupported;
    408                         final String[] data =
    409                                 unsupported.split("" + Policy.POLICY_STRING_DELIMITER);
    410                         return new MessagingException(resultCode, mStoreHost, data);
    411                     } else if (resultCode != MessagingException.NO_ERROR) {
    412                         final String errorMessage;
    413                         errorMessage = bundle.getString(
    414                                 EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE);
    415                         return new MessagingException(resultCode, errorMessage);
    416                     }
    417                 }
    418 
    419                 final EmailServiceInfo info;
    420                 if (mAccount.mHostAuthRecv != null) {
    421                     final String protocol = mAccount.mHostAuthRecv.mProtocol;
    422                     info = EmailServiceUtils
    423                             .getServiceInfo(mContext, protocol);
    424                 } else {
    425                     info = null;
    426                 }
    427 
    428                 // Check Outgoing Settings
    429                 if ((info == null || info.usesSmtp) &&
    430                         (mMode & SetupDataFragment.CHECK_OUTGOING) != 0) {
    431                     if (isCancelled()) return null;
    432                     LogUtils.d(Logging.LOG_TAG, "Begin check of outgoing email settings");
    433                     publishProgress(STATE_CHECK_OUTGOING);
    434                     final Sender sender = Sender.getInstance(mContext, mAccount);
    435                     sender.close();
    436                     sender.open();
    437                     sender.close();
    438                 }
    439 
    440                 // If we reached the end, we completed the check(s) successfully
    441                 return null;
    442             } catch (final MessagingException me) {
    443                 // Some of the legacy account checkers return errors by throwing MessagingException,
    444                 // which we catch and return here.
    445                 return me;
    446             }
    447         }
    448 
    449         /**
    450          * Progress reports (runs in UI thread).  This should be used for real progress only
    451          * (not for errors).
    452          */
    453         @Override
    454         protected void onProgressUpdate(Integer... progress) {
    455             if (isCancelled()) return;
    456             mCallback.reportProgress(progress[0], null);
    457         }
    458 
    459         /**
    460          * Result handler (runs in UI thread).
    461          *
    462          * AutoDiscover authentication errors are handled a bit differently than the
    463          * other errors;  If encountered, we display the error dialog, but we return with
    464          * a different callback used only for AutoDiscover.
    465          *
    466          * @param result null for a successful check;  exception for various errors
    467          */
    468         @Override
    469         protected void onPostExecute(MessagingException result) {
    470             if (isCancelled()) return;
    471             if (result == null) {
    472                 mCallback.reportProgress(STATE_CHECK_OK, null);
    473             } else {
    474                 int progressState = STATE_CHECK_ERROR;
    475                 final int exceptionType = result.getExceptionType();
    476 
    477                 switch (exceptionType) {
    478                     // NOTE: AutoDiscover reports have their own reporting state, handle differently
    479                     // from the other exception types
    480                     case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED:
    481                         progressState = STATE_AUTODISCOVER_AUTH_DIALOG;
    482                         break;
    483                     case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT:
    484                         progressState = STATE_AUTODISCOVER_RESULT;
    485                         break;
    486                     // NOTE: Security policies required has its own report state, handle it a bit
    487                     // differently from the other exception types.
    488                     case MessagingException.SECURITY_POLICIES_REQUIRED:
    489                         progressState = STATE_CHECK_SHOW_SECURITY;
    490                         break;
    491                 }
    492                 mCallback.reportProgress(progressState, result);
    493             }
    494         }
    495     }
    496 
    497     /**
    498      * Convert progress to message
    499      */
    500     protected static String getProgressString(Context context, int progress) {
    501         int stringId = 0;
    502         switch (progress) {
    503             case STATE_CHECK_AUTODISCOVER:
    504                 stringId = R.string.account_setup_check_settings_retr_info_msg;
    505                 break;
    506             case STATE_START:
    507             case STATE_CHECK_INCOMING:
    508                 stringId = R.string.account_setup_check_settings_check_incoming_msg;
    509                 break;
    510             case STATE_CHECK_OUTGOING:
    511                 stringId = R.string.account_setup_check_settings_check_outgoing_msg;
    512                 break;
    513         }
    514         if (stringId != 0) {
    515             return context.getString(stringId);
    516         } else {
    517             return null;
    518         }
    519     }
    520 
    521     /**
    522      * Convert mode to initial progress
    523      */
    524     protected static int getProgressForMode(int checkMode) {
    525         switch (checkMode) {
    526             case SetupDataFragment.CHECK_INCOMING:
    527                 return STATE_CHECK_INCOMING;
    528             case SetupDataFragment.CHECK_OUTGOING:
    529                 return STATE_CHECK_OUTGOING;
    530             case SetupDataFragment.CHECK_AUTODISCOVER:
    531                 return STATE_CHECK_AUTODISCOVER;
    532         }
    533         return STATE_START;
    534     }
    535 }
    536