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