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