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 */ onCheckSettingsComplete()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 */ onCheckSettingsSecurityRequired(String hostName)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 */ onCheckSettingsError(int reason, String message)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 */ onCheckSettingsAutoDiscoverComplete(int result)117 void onCheckSettingsAutoDiscoverComplete(int result); 118 } 119 120 // Public no-args constructor needed for fragment re-instantiation AccountCheckSettingsFragment()121 public AccountCheckSettingsFragment() {} 122 123 /** 124 * Create a retained, invisible fragment that checks accounts 125 * 126 * @param mode incoming or outgoing 127 */ newInstance(int mode)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 onCreate(Bundle savedInstanceState)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 onActivityCreated(Bundle savedInstanceState)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 onResume()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 onPause()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 onDestroy()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 onDetach()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 */ reportProgress(int newState, MessagingException ex)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 */ getCallbackTarget()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 */ AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth)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 */ AccountCheckTask(Context context, AccountCheckSettingsFragment callback, int mode, SetupDataFragment setupData)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 doInBackground(Void... params)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 onProgressUpdate(Integer... progress)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 onPostExecute(MessagingException result)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 */ getProgressString(Context context, int progress)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 */ getProgressForMode(int checkMode)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