/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.email.activity.setup; import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import com.android.email.R; import com.android.email.mail.Sender; import com.android.email.mail.Store; import com.android.email.service.EmailServiceUtils; import com.android.email.service.EmailServiceUtils.EmailServiceInfo; import com.android.emailcommon.Logging; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Policy; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.HostAuthCompat; import com.android.emailcommon.utility.Utility; import com.android.mail.utils.LogUtils; /** * Check incoming or outgoing settings, or perform autodiscovery. * * There are three components that work together. 1. This fragment is retained and non-displayed, * and controls the overall process. 2. An AsyncTask that works with the stores/services to * check the accounts settings. 3. A stateless progress dialog (which will be recreated on * orientation changes). * * There are also two lightweight error dialogs which are used for notification of terminal * conditions. */ public class AccountCheckSettingsFragment extends Fragment { public final static String TAG = "AccountCheckStgFrag"; // State private final static int STATE_START = 0; private final static int STATE_CHECK_AUTODISCOVER = 1; private final static int STATE_CHECK_INCOMING = 2; private final static int STATE_CHECK_OUTGOING = 3; private final static int STATE_CHECK_OK = 4; // terminal private final static int STATE_CHECK_SHOW_SECURITY = 5; // terminal private final static int STATE_CHECK_ERROR = 6; // terminal private final static int STATE_AUTODISCOVER_AUTH_DIALOG = 7; // terminal private final static int STATE_AUTODISCOVER_RESULT = 8; // terminal private int mState = STATE_START; // Args private final static String ARGS_MODE = "mode"; private int mMode; // Support for UI private boolean mAttached; private boolean mPaused = false; private MessagingException mProgressException; // Support for AsyncTask and account checking AccountCheckTask mAccountCheckTask; // Result codes returned by onCheckSettingsAutoDiscoverComplete. /** AutoDiscover completed successfully with server setup data */ public final static int AUTODISCOVER_OK = 0; /** AutoDiscover completed with no data (no server or AD not supported) */ public final static int AUTODISCOVER_NO_DATA = 1; /** AutoDiscover reported authentication error */ public final static int AUTODISCOVER_AUTHENTICATION = 2; /** * Callback interface for any target or activity doing account check settings */ public interface Callback { /** * Called when CheckSettings completed */ void onCheckSettingsComplete(); /** * Called when we determine that a security policy will need to be installed * @param hostName Passed back from the MessagingException */ void onCheckSettingsSecurityRequired(String hostName); /** * Called when we receive an error while validating the account * @param reason from * {@link CheckSettingsErrorDialogFragment#getReasonFromException(MessagingException)} * @param message from * {@link CheckSettingsErrorDialogFragment#getErrorString(Context, MessagingException)} */ void onCheckSettingsError(int reason, String message); /** * Called when autodiscovery completes. * @param result autodiscovery result code - success is AUTODISCOVER_OK */ void onCheckSettingsAutoDiscoverComplete(int result); } // Public no-args constructor needed for fragment re-instantiation public AccountCheckSettingsFragment() {} /** * Create a retained, invisible fragment that checks accounts * * @param mode incoming or outgoing */ public static AccountCheckSettingsFragment newInstance(int mode) { final AccountCheckSettingsFragment f = new AccountCheckSettingsFragment(); final Bundle b = new Bundle(1); b.putInt(ARGS_MODE, mode); f.setArguments(b); return f; } /** * Fragment initialization. Because we never implement onCreateView, and call * setRetainInstance here, this creates an invisible, persistent, "worker" fragment. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); mMode = getArguments().getInt(ARGS_MODE); } /** * This is called when the Fragment's Activity is ready to go, after * its content view has been installed; it is called both after * the initial fragment creation and after the fragment is re-attached * to a new activity. */ @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mAttached = true; // If this is the first time, start the AsyncTask if (mAccountCheckTask == null) { final SetupDataFragment.SetupDataContainer container = (SetupDataFragment.SetupDataContainer) getActivity(); // TODO: don't pass in the whole SetupDataFragment mAccountCheckTask = (AccountCheckTask) new AccountCheckTask(getActivity().getApplicationContext(), this, mMode, container.getSetupData()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } } /** * When resuming, restart the progress/error UI if necessary by re-reporting previous values */ @Override public void onResume() { super.onResume(); mPaused = false; if (mState != STATE_START) { reportProgress(mState, mProgressException); } } @Override public void onPause() { super.onPause(); mPaused = true; } /** * This is called when the fragment is going away. It is NOT called * when the fragment is being propagated between activity instances. */ @Override public void onDestroy() { super.onDestroy(); if (mAccountCheckTask != null) { Utility.cancelTaskInterrupt(mAccountCheckTask); mAccountCheckTask = null; } } /** * This is called right before the fragment is detached from its current activity instance. * All reporting and callbacks are halted until we reattach. */ @Override public void onDetach() { super.onDetach(); mAttached = false; } /** * The worker (AsyncTask) will call this (in the UI thread) to report progress. If we are * attached to an activity, update the progress immediately; If not, simply hold the * progress for later. * @param newState The new progress state being reported */ private void reportProgress(int newState, MessagingException ex) { mState = newState; mProgressException = ex; // If we are attached, create, recover, and/or update the dialog if (mAttached && !mPaused) { final FragmentManager fm = getFragmentManager(); switch (newState) { case STATE_CHECK_OK: // immediately terminate, clean up, and report back getCallbackTarget().onCheckSettingsComplete(); break; case STATE_CHECK_SHOW_SECURITY: // report that we need to accept a security policy String hostName = ex.getMessage(); if (hostName != null) { hostName = hostName.trim(); } getCallbackTarget().onCheckSettingsSecurityRequired(hostName); break; case STATE_CHECK_ERROR: case STATE_AUTODISCOVER_AUTH_DIALOG: // report that we had an error final int reason = CheckSettingsErrorDialogFragment.getReasonFromException(ex); final String errorMessage = CheckSettingsErrorDialogFragment.getErrorString(getActivity(), ex); getCallbackTarget().onCheckSettingsError(reason, errorMessage); break; case STATE_AUTODISCOVER_RESULT: final HostAuth autoDiscoverResult = ((AutoDiscoverResults) ex).mHostAuth; // report autodiscover results back to target fragment or activity getCallbackTarget().onCheckSettingsAutoDiscoverComplete( (autoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA); break; default: // Display a normal progress message CheckSettingsProgressDialogFragment checkingDialog = (CheckSettingsProgressDialogFragment) fm.findFragmentByTag(CheckSettingsProgressDialogFragment.TAG); if (checkingDialog != null) { checkingDialog.updateProgress(mState); } break; } } } /** * Find the callback target, either a target fragment or the activity */ private Callback getCallbackTarget() { final Fragment target = getTargetFragment(); if (target instanceof Callback) { return (Callback) target; } Activity activity = getActivity(); if (activity instanceof Callback) { return (Callback) activity; } throw new IllegalStateException(); } /** * This exception class is used to report autodiscover results via the reporting mechanism. */ public static class AutoDiscoverResults extends MessagingException { public final HostAuth mHostAuth; /** * @param authenticationError true if auth failure, false for result (or no response) * @param hostAuth null for "no autodiscover", non-null for server info to return */ public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) { super(null); if (authenticationError) { mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED; } else { mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT; } mHostAuth = hostAuth; } } /** * This AsyncTask does the actual account checking * * TODO: It would be better to remove the UI complete from here (the exception->string * conversions). */ private static class AccountCheckTask extends AsyncTask { final Context mContext; final AccountCheckSettingsFragment mCallback; final int mMode; final SetupDataFragment mSetupData; final Account mAccount; final String mStoreHost; final String mCheckPassword; final String mCheckEmail; /** * Create task and parameterize it * @param context application context object * @param mode bits request operations * @param setupData {@link SetupDataFragment} holding values to be checked */ public AccountCheckTask(Context context, AccountCheckSettingsFragment callback, int mode, SetupDataFragment setupData) { mContext = context; mCallback = callback; mMode = mode; mSetupData = setupData; mAccount = setupData.getAccount(); if (mAccount.mHostAuthRecv != null) { mStoreHost = mAccount.mHostAuthRecv.mAddress; mCheckPassword = mAccount.mHostAuthRecv.mPassword; } else { mStoreHost = null; mCheckPassword = null; } mCheckEmail = mAccount.mEmailAddress; } @Override protected MessagingException doInBackground(Void... params) { try { if ((mMode & SetupDataFragment.CHECK_AUTODISCOVER) != 0) { if (isCancelled()) return null; LogUtils.d(Logging.LOG_TAG, "Begin auto-discover for %s", mCheckEmail); publishProgress(STATE_CHECK_AUTODISCOVER); mSetupData.setAutodiscover(false); final Store store = Store.getInstance(mAccount, mContext); final Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword); // Result will be one of: // null: remote exception - proceed to manual setup // MessagingException.AUTHENTICATION_FAILED: username/password rejected // Other error: proceed to manual setup // No error: return autodiscover results if (result == null) { return new AutoDiscoverResults(false, null); } int errorCode = result.getInt( EmailServiceProxy.AUTO_DISCOVER_BUNDLE_MESSAGING_ERROR_CODE); if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) { return new AutoDiscoverResults(true, null); } else if (errorCode != MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT) { return new AutoDiscoverResults(false, null); } else { final HostAuthCompat hostAuthCompat = result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH); Account account = mSetupData.getAccount(); if (hostAuthCompat != null) { account.mHostAuthRecv = hostAuthCompat.toHostAuth(); } mSetupData.setAutodiscover(true); return new AutoDiscoverResults(false, account.mHostAuthRecv); } } // Check Incoming Settings if ((mMode & SetupDataFragment.CHECK_INCOMING) != 0) { if (isCancelled()) return null; LogUtils.d(Logging.LOG_TAG, "Begin check of incoming email settings"); publishProgress(STATE_CHECK_INCOMING); final Store store = Store.getInstance(mAccount, mContext); final Bundle bundle = store.checkSettings(); if (bundle == null) { return new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION); } mAccount.mProtocolVersion = bundle.getString( EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION); int resultCode = bundle.getInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE); final String redirectAddress = bundle.getString( EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS, null); if (redirectAddress != null) { mAccount.mHostAuthRecv.mAddress = redirectAddress; } // Only show "policies required" if this is a new account setup if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED && mAccount.isSaved()) { resultCode = MessagingException.NO_ERROR; } if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) { mSetupData.setPolicy((Policy)bundle.getParcelable( EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET)); return new MessagingException(resultCode, mStoreHost); } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) { final Policy policy = bundle.getParcelable( EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET); final String unsupported = policy.mProtocolPoliciesUnsupported; final String[] data = unsupported.split("" + Policy.POLICY_STRING_DELIMITER); return new MessagingException(resultCode, mStoreHost, data); } else if (resultCode != MessagingException.NO_ERROR) { final String errorMessage; errorMessage = bundle.getString( EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE); return new MessagingException(resultCode, errorMessage); } // Save account capabilities mAccount.mCapabilities = bundle.getInt( EmailServiceProxy.SETTINGS_BUNDLE_CAPABILITIES, 0); } final EmailServiceInfo info; if (mAccount.mHostAuthRecv != null) { final String protocol = mAccount.mHostAuthRecv.mProtocol; info = EmailServiceUtils .getServiceInfo(mContext, protocol); } else { info = null; } // Check Outgoing Settings if ((info == null || info.usesSmtp) && (mMode & SetupDataFragment.CHECK_OUTGOING) != 0) { if (isCancelled()) return null; LogUtils.d(Logging.LOG_TAG, "Begin check of outgoing email settings"); publishProgress(STATE_CHECK_OUTGOING); final Sender sender = Sender.getInstance(mContext, mAccount); sender.close(); sender.open(); sender.close(); } // If we reached the end, we completed the check(s) successfully return null; } catch (final MessagingException me) { // Some of the legacy account checkers return errors by throwing MessagingException, // which we catch and return here. return me; } } /** * Progress reports (runs in UI thread). This should be used for real progress only * (not for errors). */ @Override protected void onProgressUpdate(Integer... progress) { if (isCancelled()) return; mCallback.reportProgress(progress[0], null); } /** * Result handler (runs in UI thread). * * AutoDiscover authentication errors are handled a bit differently than the * other errors; If encountered, we display the error dialog, but we return with * a different callback used only for AutoDiscover. * * @param result null for a successful check; exception for various errors */ @Override protected void onPostExecute(MessagingException result) { if (isCancelled()) return; if (result == null) { mCallback.reportProgress(STATE_CHECK_OK, null); } else { int progressState = STATE_CHECK_ERROR; final int exceptionType = result.getExceptionType(); switch (exceptionType) { // NOTE: AutoDiscover reports have their own reporting state, handle differently // from the other exception types case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED: progressState = STATE_AUTODISCOVER_AUTH_DIALOG; break; case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT: progressState = STATE_AUTODISCOVER_RESULT; break; // NOTE: Security policies required has its own report state, handle it a bit // differently from the other exception types. case MessagingException.SECURITY_POLICIES_REQUIRED: progressState = STATE_CHECK_SHOW_SECURITY; break; } mCallback.reportProgress(progressState, result); } } } /** * Convert progress to message */ protected static String getProgressString(Context context, int progress) { int stringId = 0; switch (progress) { case STATE_CHECK_AUTODISCOVER: stringId = R.string.account_setup_check_settings_retr_info_msg; break; case STATE_START: case STATE_CHECK_INCOMING: stringId = R.string.account_setup_check_settings_check_incoming_msg; break; case STATE_CHECK_OUTGOING: stringId = R.string.account_setup_check_settings_check_outgoing_msg; break; } if (stringId != 0) { return context.getString(stringId); } else { return null; } } /** * Convert mode to initial progress */ protected static int getProgressForMode(int checkMode) { switch (checkMode) { case SetupDataFragment.CHECK_INCOMING: return STATE_CHECK_INCOMING; case SetupDataFragment.CHECK_OUTGOING: return STATE_CHECK_OUTGOING; case SetupDataFragment.CHECK_AUTODISCOVER: return STATE_CHECK_AUTODISCOVER; } return STATE_START; } }