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.AlertDialog; 21import android.app.Dialog; 22import android.app.DialogFragment; 23import android.app.Fragment; 24import android.app.FragmentManager; 25import android.app.ProgressDialog; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.os.AsyncTask; 29import android.os.Bundle; 30import android.text.TextUtils; 31 32import com.android.email.R; 33import com.android.email.mail.Sender; 34import com.android.email.mail.Store; 35import com.android.email.service.EmailServiceUtils; 36import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 37import com.android.emailcommon.Logging; 38import com.android.emailcommon.mail.MessagingException; 39import com.android.emailcommon.provider.Account; 40import com.android.emailcommon.provider.HostAuth; 41import com.android.emailcommon.provider.Policy; 42import com.android.emailcommon.service.EmailServiceProxy; 43import com.android.emailcommon.utility.Utility; 44import com.android.mail.utils.LogUtils; 45 46/** 47 * Check incoming or outgoing settings, or perform autodiscovery. 48 * 49 * There are three components that work together. 1. This fragment is retained and non-displayed, 50 * and controls the overall process. 2. An AsyncTask that works with the stores/services to 51 * check the accounts settings. 3. A stateless progress dialog (which will be recreated on 52 * orientation changes). 53 * 54 * There are also two lightweight error dialogs which are used for notification of terminal 55 * conditions. 56 */ 57public class AccountCheckSettingsFragment extends Fragment { 58 59 public final static String TAG = "AccountCheckStgFrag"; 60 61 // State 62 private final static int STATE_START = 0; 63 private final static int STATE_CHECK_AUTODISCOVER = 1; 64 private final static int STATE_CHECK_INCOMING = 2; 65 private final static int STATE_CHECK_OUTGOING = 3; 66 private final static int STATE_CHECK_OK = 4; // terminal 67 private final static int STATE_CHECK_SHOW_SECURITY = 5; // terminal 68 private final static int STATE_CHECK_ERROR = 6; // terminal 69 private final static int STATE_AUTODISCOVER_AUTH_DIALOG = 7; // terminal 70 private final static int STATE_AUTODISCOVER_RESULT = 8; // terminal 71 private int mState = STATE_START; 72 private SetupData mSetupData; 73 74 // Support for UI 75 private boolean mAttached; 76 private boolean mPaused = false; 77 private CheckingDialog mCheckingDialog; 78 private MessagingException mProgressException; 79 80 // Support for AsyncTask and account checking 81 AccountCheckTask mAccountCheckTask; 82 83 // Result codes returned by onCheckSettingsComplete. 84 /** Check settings returned successfully */ 85 public final static int CHECK_SETTINGS_OK = 0; 86 /** Check settings failed due to connection, authentication, or other server error */ 87 public final static int CHECK_SETTINGS_SERVER_ERROR = 1; 88 /** Check settings failed due to user refusing to accept security requirements */ 89 public final static int CHECK_SETTINGS_SECURITY_USER_DENY = 2; 90 /** Check settings failed due to certificate being required - user needs to pick immediately. */ 91 public final static int CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED = 3; 92 93 // Result codes returned by onAutoDiscoverComplete. 94 /** AutoDiscover completed successfully with server setup data */ 95 public final static int AUTODISCOVER_OK = 0; 96 /** AutoDiscover completed with no data (no server or AD not supported) */ 97 public final static int AUTODISCOVER_NO_DATA = 1; 98 /** AutoDiscover reported authentication error */ 99 public final static int AUTODISCOVER_AUTHENTICATION = 2; 100 101 /** 102 * Callback interface for any target or activity doing account check settings 103 */ 104 public interface Callbacks { 105 /** 106 * Called when CheckSettings completed 107 * @param result check settings result code - success is CHECK_SETTINGS_OK 108 */ 109 public void onCheckSettingsComplete(int result, SetupData setupData); 110 111 /** 112 * Called when autodiscovery completes. 113 * @param result autodiscovery result code - success is AUTODISCOVER_OK 114 */ 115 public void onAutoDiscoverComplete(int result, SetupData setupData); 116 } 117 118 // Public no-args constructor needed for fragment re-instantiation 119 public AccountCheckSettingsFragment() {} 120 121 /** 122 * Create a retained, invisible fragment that checks accounts 123 * 124 * @param mode incoming or outgoing 125 */ 126 public static AccountCheckSettingsFragment newInstance(int mode, Fragment parentFragment) { 127 final AccountCheckSettingsFragment f = new AccountCheckSettingsFragment(); 128 f.setTargetFragment(parentFragment, mode); 129 return f; 130 } 131 132 /** 133 * Fragment initialization. Because we never implement onCreateView, and call 134 * setRetainInstance here, this creates an invisible, persistent, "worker" fragment. 135 */ 136 @Override 137 public void onCreate(Bundle savedInstanceState) { 138 super.onCreate(savedInstanceState); 139 setRetainInstance(true); 140 } 141 142 /** 143 * This is called when the Fragment's Activity is ready to go, after 144 * its content view has been installed; it is called both after 145 * the initial fragment creation and after the fragment is re-attached 146 * to a new activity. 147 */ 148 @Override 149 public void onActivityCreated(Bundle savedInstanceState) { 150 super.onActivityCreated(savedInstanceState); 151 mAttached = true; 152 153 // If this is the first time, start the AsyncTask 154 if (mAccountCheckTask == null) { 155 final int checkMode = getTargetRequestCode(); 156 final SetupData.SetupDataContainer container = 157 (SetupData.SetupDataContainer) getActivity(); 158 mSetupData = container.getSetupData(); 159 final Account checkAccount = mSetupData.getAccount(); 160 mAccountCheckTask = (AccountCheckTask) 161 new AccountCheckTask(checkMode, checkAccount) 162 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); 163 } 164 } 165 166 /** 167 * When resuming, restart the progress/error UI if necessary by re-reporting previous values 168 */ 169 @Override 170 public void onResume() { 171 super.onResume(); 172 mPaused = false; 173 174 if (mState != STATE_START) { 175 reportProgress(mState, mProgressException); 176 } 177 } 178 179 @Override 180 public void onPause() { 181 super.onPause(); 182 mPaused = true; 183 } 184 185 /** 186 * This is called when the fragment is going away. It is NOT called 187 * when the fragment is being propagated between activity instances. 188 */ 189 @Override 190 public void onDestroy() { 191 super.onDestroy(); 192 if (mAccountCheckTask != null) { 193 Utility.cancelTaskInterrupt(mAccountCheckTask); 194 mAccountCheckTask = null; 195 } 196 // Make doubly sure that the dialog isn't pointing at us before we're removed from the 197 // fragment manager 198 final Fragment f = getFragmentManager().findFragmentByTag(CheckingDialog.TAG); 199 if (f != null) { 200 f.setTargetFragment(null, 0); 201 } 202 } 203 204 /** 205 * This is called right before the fragment is detached from its current activity instance. 206 * All reporting and callbacks are halted until we reattach. 207 */ 208 @Override 209 public void onDetach() { 210 super.onDetach(); 211 mAttached = false; 212 } 213 214 /** 215 * The worker (AsyncTask) will call this (in the UI thread) to report progress. If we are 216 * attached to an activity, update the progress immediately; If not, simply hold the 217 * progress for later. 218 * @param newState The new progress state being reported 219 */ 220 private void reportProgress(int newState, MessagingException ex) { 221 mState = newState; 222 mProgressException = ex; 223 224 // If we are attached, create, recover, and/or update the dialog 225 if (mAttached && !mPaused) { 226 final FragmentManager fm = getFragmentManager(); 227 228 switch (newState) { 229 case STATE_CHECK_OK: 230 // immediately terminate, clean up, and report back 231 // 1. get rid of progress dialog (if any) 232 recoverAndDismissCheckingDialog(); 233 // 2. exit self 234 fm.popBackStack(); 235 // 3. report OK back to target fragment or activity 236 getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_OK, mSetupData); 237 break; 238 case STATE_CHECK_SHOW_SECURITY: 239 // 1. get rid of progress dialog (if any) 240 recoverAndDismissCheckingDialog(); 241 // 2. launch the error dialog, if needed 242 if (fm.findFragmentByTag(SecurityRequiredDialog.TAG) == null) { 243 String message = ex.getMessage(); 244 if (message != null) { 245 message = message.trim(); 246 } 247 SecurityRequiredDialog securityRequiredDialog = 248 SecurityRequiredDialog.newInstance(this, message); 249 fm.beginTransaction() 250 .add(securityRequiredDialog, SecurityRequiredDialog.TAG) 251 .commit(); 252 } 253 break; 254 case STATE_CHECK_ERROR: 255 case STATE_AUTODISCOVER_AUTH_DIALOG: 256 // 1. get rid of progress dialog (if any) 257 recoverAndDismissCheckingDialog(); 258 // 2. launch the error dialog, if needed 259 if (fm.findFragmentByTag(ErrorDialog.TAG) == null) { 260 ErrorDialog errorDialog = ErrorDialog.newInstance( 261 getActivity(), this, mProgressException); 262 fm.beginTransaction() 263 .add(errorDialog, ErrorDialog.TAG) 264 .commit(); 265 } 266 break; 267 case STATE_AUTODISCOVER_RESULT: 268 final HostAuth autoDiscoverResult = ((AutoDiscoverResults) ex).mHostAuth; 269 // 1. get rid of progress dialog (if any) 270 recoverAndDismissCheckingDialog(); 271 // 2. exit self 272 fm.popBackStack(); 273 // 3. report back to target fragment or activity 274 getCallbackTarget().onAutoDiscoverComplete( 275 (autoDiscoverResult != null) ? AUTODISCOVER_OK : AUTODISCOVER_NO_DATA, 276 mSetupData); 277 break; 278 default: 279 // Display a normal progress message 280 mCheckingDialog = (CheckingDialog) fm.findFragmentByTag(CheckingDialog.TAG); 281 282 if (mCheckingDialog == null) { 283 mCheckingDialog = CheckingDialog.newInstance(this, mState); 284 fm.beginTransaction() 285 .add(mCheckingDialog, CheckingDialog.TAG) 286 .commit(); 287 } else { 288 mCheckingDialog.updateProgress(mState); 289 } 290 break; 291 } 292 } 293 } 294 295 /** 296 * Find the callback target, either a target fragment or the activity 297 */ 298 private Callbacks getCallbackTarget() { 299 final Fragment target = getTargetFragment(); 300 if (target instanceof Callbacks) { 301 return (Callbacks) target; 302 } 303 Activity activity = getActivity(); 304 if (activity instanceof Callbacks) { 305 return (Callbacks) activity; 306 } 307 throw new IllegalStateException(); 308 } 309 310 /** 311 * Recover and dismiss the progress dialog fragment 312 */ 313 private void recoverAndDismissCheckingDialog() { 314 if (mCheckingDialog == null) { 315 mCheckingDialog = (CheckingDialog) 316 getFragmentManager().findFragmentByTag(CheckingDialog.TAG); 317 } 318 if (mCheckingDialog != null) { 319 // TODO: dismissAllowingStateLoss() can cause the fragment to return later as a zombie 320 // after the fragment manager restores state, if it happens that this call is executed 321 // after the state is saved. Figure out a way to clean this up later. b/11435698 322 mCheckingDialog.dismissAllowingStateLoss(); 323 mCheckingDialog = null; 324 } 325 } 326 327 /** 328 * This is called when the user clicks "cancel" on the progress dialog. Shuts everything 329 * down and dismisses everything. 330 * This should cause us to remain in the current screen (not accepting the settings) 331 */ 332 private void onCheckingDialogCancel() { 333 // 1. kill the checker 334 Utility.cancelTaskInterrupt(mAccountCheckTask); 335 mAccountCheckTask = null; 336 // 2. kill self with no report - this is "cancel" 337 finish(); 338 } 339 340 private void onEditCertificateOk() { 341 getCallbackTarget().onCheckSettingsComplete(CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED, 342 mSetupData); 343 finish(); 344 } 345 346 /** 347 * This is called when the user clicks "edit" from the error dialog. The error dialog 348 * should have already dismissed itself. 349 * Depending on the context, the target will remain in the current activity (e.g. editing 350 * settings) or return to its own parent (e.g. enter new credentials). 351 */ 352 private void onErrorDialogEditButton() { 353 // 1. handle "edit" - notify callback that we had a problem with the test 354 final Callbacks callbackTarget = getCallbackTarget(); 355 if (mState == STATE_AUTODISCOVER_AUTH_DIALOG) { 356 // report auth error to target fragment or activity 357 callbackTarget.onAutoDiscoverComplete(CHECK_SETTINGS_SERVER_ERROR, mSetupData); 358 } else { 359 // report check settings failure to target fragment or activity 360 callbackTarget.onCheckSettingsComplete(CHECK_SETTINGS_SERVER_ERROR, mSetupData); 361 } 362 finish(); 363 } 364 365 /** Kill self if not already killed. */ 366 private void finish() { 367 final FragmentManager fm = getFragmentManager(); 368 if (fm != null) { 369 fm.popBackStack(); 370 } 371 } 372 373 /** 374 * This is called when the user clicks "ok" or "cancel" on the "security required" dialog. 375 * Shuts everything down and dismisses everything, and reports the result appropriately. 376 */ 377 private void onSecurityRequiredDialogResultOk(boolean okPressed) { 378 // 1. handle OK/cancel - notify that security is OK and we can proceed 379 final Callbacks callbackTarget = getCallbackTarget(); 380 callbackTarget.onCheckSettingsComplete( 381 okPressed ? CHECK_SETTINGS_OK : CHECK_SETTINGS_SECURITY_USER_DENY, mSetupData); 382 383 // 2. kill self if not already killed by callback 384 final FragmentManager fm = getFragmentManager(); 385 if (fm != null) { 386 fm.popBackStack(); 387 } 388 } 389 390 /** 391 * This exception class is used to report autodiscover results via the reporting mechanism. 392 */ 393 public static class AutoDiscoverResults extends MessagingException { 394 public final HostAuth mHostAuth; 395 396 /** 397 * @param authenticationError true if auth failure, false for result (or no response) 398 * @param hostAuth null for "no autodiscover", non-null for server info to return 399 */ 400 public AutoDiscoverResults(boolean authenticationError, HostAuth hostAuth) { 401 super(null); 402 if (authenticationError) { 403 mExceptionType = AUTODISCOVER_AUTHENTICATION_FAILED; 404 } else { 405 mExceptionType = AUTODISCOVER_AUTHENTICATION_RESULT; 406 } 407 mHostAuth = hostAuth; 408 } 409 } 410 411 /** 412 * This AsyncTask does the actual account checking 413 * 414 * TODO: It would be better to remove the UI complete from here (the exception->string 415 * conversions). 416 */ 417 private class AccountCheckTask extends AsyncTask<Void, Integer, MessagingException> { 418 419 final Context mContext; 420 final int mMode; 421 final Account mAccount; 422 final String mStoreHost; 423 final String mCheckEmail; 424 final String mCheckPassword; 425 426 /** 427 * Create task and parameterize it 428 * @param mode bits request operations 429 * @param checkAccount account holding values to be checked 430 */ 431 public AccountCheckTask(int mode, Account checkAccount) { 432 mContext = getActivity().getApplicationContext(); 433 mMode = mode; 434 mAccount = checkAccount; 435 mStoreHost = checkAccount.mHostAuthRecv.mAddress; 436 mCheckEmail = checkAccount.mEmailAddress; 437 mCheckPassword = checkAccount.mHostAuthRecv.mPassword; 438 } 439 440 @Override 441 protected MessagingException doInBackground(Void... params) { 442 try { 443 if ((mMode & SetupData.CHECK_AUTODISCOVER) != 0) { 444 if (isCancelled()) return null; 445 publishProgress(STATE_CHECK_AUTODISCOVER); 446 LogUtils.d(Logging.LOG_TAG, "Begin auto-discover for " + mCheckEmail); 447 final Store store = Store.getInstance(mAccount, mContext); 448 final Bundle result = store.autoDiscover(mContext, mCheckEmail, mCheckPassword); 449 // Result will be one of: 450 // null: remote exception - proceed to manual setup 451 // MessagingException.AUTHENTICATION_FAILED: username/password rejected 452 // Other error: proceed to manual setup 453 // No error: return autodiscover results 454 if (result == null) { 455 return new AutoDiscoverResults(false, null); 456 } 457 int errorCode = 458 result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE); 459 if (errorCode == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) { 460 return new AutoDiscoverResults(true, null); 461 } else if (errorCode != MessagingException.NO_ERROR) { 462 return new AutoDiscoverResults(false, null); 463 } else { 464 HostAuth serverInfo = 465 result.getParcelable(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH); 466 return new AutoDiscoverResults(false, serverInfo); 467 } 468 } 469 470 // Check Incoming Settings 471 if ((mMode & SetupData.CHECK_INCOMING) != 0) { 472 if (isCancelled()) return null; 473 LogUtils.d(Logging.LOG_TAG, "Begin check of incoming email settings"); 474 publishProgress(STATE_CHECK_INCOMING); 475 final Store store = Store.getInstance(mAccount, mContext); 476 final Bundle bundle = store.checkSettings(); 477 if (bundle == null) { 478 return new MessagingException(MessagingException.UNSPECIFIED_EXCEPTION); 479 } 480 mAccount.mProtocolVersion = bundle.getString( 481 EmailServiceProxy.VALIDATE_BUNDLE_PROTOCOL_VERSION); 482 int resultCode = bundle.getInt(EmailServiceProxy.VALIDATE_BUNDLE_RESULT_CODE); 483 final String redirectAddress = bundle.getString( 484 EmailServiceProxy.VALIDATE_BUNDLE_REDIRECT_ADDRESS, null); 485 if (redirectAddress != null) { 486 mAccount.mHostAuthRecv.mAddress = redirectAddress; 487 } 488 // Only show "policies required" if this is a new account setup 489 if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED && 490 mAccount.isSaved()) { 491 resultCode = MessagingException.NO_ERROR; 492 } 493 if (resultCode == MessagingException.SECURITY_POLICIES_REQUIRED) { 494 mSetupData.setPolicy((Policy)bundle.getParcelable( 495 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET)); 496 return new MessagingException(resultCode, mStoreHost); 497 } else if (resultCode == MessagingException.SECURITY_POLICIES_UNSUPPORTED) { 498 final Policy policy = bundle.getParcelable( 499 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET); 500 final String unsupported = policy.mProtocolPoliciesUnsupported; 501 final String[] data = 502 unsupported.split("" + Policy.POLICY_STRING_DELIMITER); 503 return new MessagingException(resultCode, mStoreHost, data); 504 } else if (resultCode != MessagingException.NO_ERROR) { 505 final String errorMessage; 506 errorMessage = bundle.getString( 507 EmailServiceProxy.VALIDATE_BUNDLE_ERROR_MESSAGE); 508 return new MessagingException(resultCode, errorMessage); 509 } 510 } 511 512 final String protocol = mAccount.mHostAuthRecv.mProtocol; 513 final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, protocol); 514 515 // Check Outgoing Settings 516 if (info.usesSmtp && (mMode & SetupData.CHECK_OUTGOING) != 0) { 517 if (isCancelled()) return null; 518 LogUtils.d(Logging.LOG_TAG, "Begin check of outgoing email settings"); 519 publishProgress(STATE_CHECK_OUTGOING); 520 final Sender sender = Sender.getInstance(mContext, mAccount); 521 sender.close(); 522 sender.open(); 523 sender.close(); 524 } 525 526 // If we reached the end, we completed the check(s) successfully 527 return null; 528 } catch (final MessagingException me) { 529 // Some of the legacy account checkers return errors by throwing MessagingException, 530 // which we catch and return here. 531 return me; 532 } 533 } 534 535 /** 536 * Progress reports (runs in UI thread). This should be used for real progress only 537 * (not for errors). 538 */ 539 @Override 540 protected void onProgressUpdate(Integer... progress) { 541 if (isCancelled()) return; 542 reportProgress(progress[0], null); 543 } 544 545 /** 546 * Result handler (runs in UI thread). 547 * 548 * AutoDiscover authentication errors are handled a bit differently than the 549 * other errors; If encountered, we display the error dialog, but we return with 550 * a different callback used only for AutoDiscover. 551 * 552 * @param result null for a successful check; exception for various errors 553 */ 554 @Override 555 protected void onPostExecute(MessagingException result) { 556 if (isCancelled()) return; 557 if (result == null) { 558 reportProgress(STATE_CHECK_OK, null); 559 } else { 560 int progressState = STATE_CHECK_ERROR; 561 final int exceptionType = result.getExceptionType(); 562 563 switch (exceptionType) { 564 // NOTE: AutoDiscover reports have their own reporting state, handle differently 565 // from the other exception types 566 case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED: 567 progressState = STATE_AUTODISCOVER_AUTH_DIALOG; 568 break; 569 case MessagingException.AUTODISCOVER_AUTHENTICATION_RESULT: 570 progressState = STATE_AUTODISCOVER_RESULT; 571 break; 572 // NOTE: Security policies required has its own report state, handle it a bit 573 // differently from the other exception types. 574 case MessagingException.SECURITY_POLICIES_REQUIRED: 575 progressState = STATE_CHECK_SHOW_SECURITY; 576 break; 577 } 578 reportProgress(progressState, result); 579 } 580 } 581 } 582 583 private static String getErrorString(Context context, MessagingException ex) { 584 final int id; 585 String message = ex.getMessage(); 586 if (message != null) { 587 message = message.trim(); 588 } 589 switch (ex.getExceptionType()) { 590 // The remaining exception types are handled by setting the state to 591 // STATE_CHECK_ERROR (above, default) and conversion to specific error strings. 592 case MessagingException.CERTIFICATE_VALIDATION_ERROR: 593 id = TextUtils.isEmpty(message) 594 ? R.string.account_setup_failed_dlg_certificate_message 595 : R.string.account_setup_failed_dlg_certificate_message_fmt; 596 break; 597 case MessagingException.AUTHENTICATION_FAILED: 598 id = R.string.account_setup_failed_dlg_auth_message; 599 break; 600 case MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED: 601 id = R.string.account_setup_autodiscover_dlg_authfail_message; 602 break; 603 case MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR: 604 id = R.string.account_setup_failed_check_credentials_message; 605 break; 606 case MessagingException.IOERROR: 607 id = R.string.account_setup_failed_ioerror; 608 break; 609 case MessagingException.TLS_REQUIRED: 610 id = R.string.account_setup_failed_tls_required; 611 break; 612 case MessagingException.AUTH_REQUIRED: 613 id = R.string.account_setup_failed_auth_required; 614 break; 615 case MessagingException.SECURITY_POLICIES_UNSUPPORTED: 616 id = R.string.account_setup_failed_security_policies_unsupported; 617 // Belt and suspenders here; there should always be a non-empty array here 618 String[] unsupportedPolicies = (String[]) ex.getExceptionData(); 619 if (unsupportedPolicies == null) { 620 LogUtils.w(TAG, "No data for unsupported policies?"); 621 break; 622 } 623 // Build a string, concatenating policies we don't support 624 final StringBuilder sb = new StringBuilder(); 625 boolean first = true; 626 for (String policyName: unsupportedPolicies) { 627 if (first) { 628 first = false; 629 } else { 630 sb.append(", "); 631 } 632 sb.append(policyName); 633 } 634 message = sb.toString(); 635 break; 636 case MessagingException.ACCESS_DENIED: 637 id = R.string.account_setup_failed_access_denied; 638 break; 639 case MessagingException.PROTOCOL_VERSION_UNSUPPORTED: 640 id = R.string.account_setup_failed_protocol_unsupported; 641 break; 642 case MessagingException.GENERAL_SECURITY: 643 id = R.string.account_setup_failed_security; 644 break; 645 case MessagingException.CLIENT_CERTIFICATE_REQUIRED: 646 id = R.string.account_setup_failed_certificate_required; 647 break; 648 case MessagingException.CLIENT_CERTIFICATE_ERROR: 649 id = R.string.account_setup_failed_certificate_inaccessible; 650 break; 651 default: 652 id = TextUtils.isEmpty(message) 653 ? R.string.account_setup_failed_dlg_server_message 654 : R.string.account_setup_failed_dlg_server_message_fmt; 655 break; 656 } 657 return TextUtils.isEmpty(message) 658 ? context.getString(id) 659 : context.getString(id, message); 660 } 661 662 /** 663 * Simple dialog that shows progress as we work through the settings checks. 664 * This is stateless except for its UI (e.g. current strings) and can be torn down or 665 * recreated at any time without affecting the account checking progress. 666 */ 667 public static class CheckingDialog extends DialogFragment { 668 @SuppressWarnings("hiding") 669 public final static String TAG = "CheckProgressDialog"; 670 671 // Extras for saved instance state 672 private final String EXTRA_PROGRESS_STRING = "CheckProgressDialog.Progress"; 673 674 // UI 675 private String mProgressString; 676 677 // Public no-args constructor needed for fragment re-instantiation 678 public CheckingDialog() {} 679 680 /** 681 * Create a dialog that reports progress 682 * @param progress initial progress indication 683 */ 684 public static CheckingDialog newInstance(AccountCheckSettingsFragment parentFragment, 685 int progress) { 686 final CheckingDialog f = new CheckingDialog(); 687 f.setTargetFragment(parentFragment, progress); 688 return f; 689 } 690 691 /** 692 * Update the progress of an existing dialog 693 * @param progress latest progress to be displayed 694 */ 695 public void updateProgress(int progress) { 696 mProgressString = getProgressString(progress); 697 final AlertDialog dialog = (AlertDialog) getDialog(); 698 if (dialog != null && mProgressString != null) { 699 dialog.setMessage(mProgressString); 700 } 701 } 702 703 @Override 704 public Dialog onCreateDialog(Bundle savedInstanceState) { 705 final Context context = getActivity(); 706 if (savedInstanceState != null) { 707 mProgressString = savedInstanceState.getString(EXTRA_PROGRESS_STRING); 708 } 709 if (mProgressString == null) { 710 mProgressString = getProgressString(getTargetRequestCode()); 711 } 712 713 final ProgressDialog dialog = new ProgressDialog(context); 714 dialog.setIndeterminate(true); 715 dialog.setMessage(mProgressString); 716 dialog.setButton(DialogInterface.BUTTON_NEGATIVE, 717 context.getString(R.string.cancel_action), 718 new DialogInterface.OnClickListener() { 719 @Override 720 public void onClick(DialogInterface dialog, int which) { 721 dismiss(); 722 723 final AccountCheckSettingsFragment target = 724 (AccountCheckSettingsFragment) getTargetFragment(); 725 if (target != null) { 726 target.onCheckingDialogCancel(); 727 } 728 } 729 }); 730 return dialog; 731 } 732 733 /** 734 * Listen for cancellation, which can happen from places other than the 735 * negative button (e.g. touching outside the dialog), and stop the checker 736 */ 737 @Override 738 public void onCancel(DialogInterface dialog) { 739 final AccountCheckSettingsFragment target = 740 (AccountCheckSettingsFragment) getTargetFragment(); 741 if (target != null) { 742 target.onCheckingDialogCancel(); 743 } 744 super.onCancel(dialog); 745 } 746 747 @Override 748 public void onSaveInstanceState(Bundle outState) { 749 super.onSaveInstanceState(outState); 750 outState.putString(EXTRA_PROGRESS_STRING, mProgressString); 751 } 752 753 /** 754 * Convert progress to message 755 */ 756 private String getProgressString(int progress) { 757 int stringId = 0; 758 switch (progress) { 759 case STATE_CHECK_AUTODISCOVER: 760 stringId = R.string.account_setup_check_settings_retr_info_msg; 761 break; 762 case STATE_CHECK_INCOMING: 763 stringId = R.string.account_setup_check_settings_check_incoming_msg; 764 break; 765 case STATE_CHECK_OUTGOING: 766 stringId = R.string.account_setup_check_settings_check_outgoing_msg; 767 break; 768 } 769 return getActivity().getString(stringId); 770 } 771 } 772 773 /** 774 * The standard error dialog. Calls back to onErrorDialogButton(). 775 */ 776 public static class ErrorDialog extends DialogFragment { 777 @SuppressWarnings("hiding") 778 public final static String TAG = "ErrorDialog"; 779 780 // Bundle keys for arguments 781 private final static String ARGS_MESSAGE = "ErrorDialog.Message"; 782 private final static String ARGS_EXCEPTION_ID = "ErrorDialog.ExceptionId"; 783 784 /** 785 * Use {@link #newInstance} This public constructor is still required so 786 * that DialogFragment state can be automatically restored by the 787 * framework. 788 */ 789 public ErrorDialog() { 790 } 791 792 public static ErrorDialog newInstance(Context context, AccountCheckSettingsFragment target, 793 MessagingException ex) { 794 final ErrorDialog fragment = new ErrorDialog(); 795 final Bundle arguments = new Bundle(2); 796 arguments.putString(ARGS_MESSAGE, getErrorString(context, ex)); 797 arguments.putInt(ARGS_EXCEPTION_ID, ex.getExceptionType()); 798 fragment.setArguments(arguments); 799 fragment.setTargetFragment(target, 0); 800 return fragment; 801 } 802 803 @Override 804 public Dialog onCreateDialog(Bundle savedInstanceState) { 805 final Context context = getActivity(); 806 final Bundle arguments = getArguments(); 807 final String message = arguments.getString(ARGS_MESSAGE); 808 final int exceptionId = arguments.getInt(ARGS_EXCEPTION_ID); 809 final AccountCheckSettingsFragment target = 810 (AccountCheckSettingsFragment) getTargetFragment(); 811 812 final AlertDialog.Builder builder = new AlertDialog.Builder(context) 813 .setMessage(message) 814 .setCancelable(true); 815 816 // Use a different title when we get 817 // MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED 818 if (exceptionId == MessagingException.AUTODISCOVER_AUTHENTICATION_FAILED) { 819 builder.setTitle(R.string.account_setup_autodiscover_dlg_authfail_title); 820 } else { 821 builder.setIconAttribute(android.R.attr.alertDialogIcon) 822 .setTitle(context.getString(R.string.account_setup_failed_dlg_title)); 823 } 824 825 if (exceptionId == MessagingException.CLIENT_CERTIFICATE_REQUIRED) { 826 // Certificate error - show two buttons so the host fragment can auto pop 827 // into the appropriate flow. 828 builder.setPositiveButton( 829 context.getString(android.R.string.ok), 830 new DialogInterface.OnClickListener() { 831 @Override 832 public void onClick(DialogInterface dialog, int which) { 833 dismiss(); 834 target.onEditCertificateOk(); 835 } 836 }); 837 builder.setNegativeButton( 838 context.getString(android.R.string.cancel), 839 new DialogInterface.OnClickListener() { 840 @Override 841 public void onClick(DialogInterface dialog, int which) { 842 dismiss(); 843 target.onErrorDialogEditButton(); 844 } 845 }); 846 847 } else { 848 // "Normal" error - just use a single "Edit details" button. 849 builder.setPositiveButton( 850 context.getString(R.string.account_setup_failed_dlg_edit_details_action), 851 new DialogInterface.OnClickListener() { 852 @Override 853 public void onClick(DialogInterface dialog, int which) { 854 dismiss(); 855 target.onErrorDialogEditButton(); 856 } 857 }); 858 } 859 860 return builder.create(); 861 } 862 863 } 864 865 /** 866 * The "security required" error dialog. This is presented whenever an exchange account 867 * reports that it will require security policy control, and provide the user with the 868 * opportunity to accept or deny this. 869 * 870 * If the user clicks OK, calls onSecurityRequiredDialogResultOk(true) which reports back 871 * to the target as if the settings check was "ok". If the user clicks "cancel", calls 872 * onSecurityRequiredDialogResultOk(false) which simply closes the checker (this is the 873 * same as any other failed check.) 874 */ 875 public static class SecurityRequiredDialog extends DialogFragment { 876 @SuppressWarnings("hiding") 877 public final static String TAG = "SecurityRequiredDialog"; 878 879 // Bundle keys for arguments 880 private final static String ARGS_HOST_NAME = "SecurityRequiredDialog.HostName"; 881 882 // Public no-args constructor needed for fragment re-instantiation 883 public SecurityRequiredDialog() {} 884 885 public static SecurityRequiredDialog newInstance(AccountCheckSettingsFragment target, 886 String hostName) { 887 final SecurityRequiredDialog fragment = new SecurityRequiredDialog(); 888 final Bundle arguments = new Bundle(1); 889 arguments.putString(ARGS_HOST_NAME, hostName); 890 fragment.setArguments(arguments); 891 fragment.setTargetFragment(target, 0); 892 return fragment; 893 } 894 895 @Override 896 public Dialog onCreateDialog(Bundle savedInstanceState) { 897 final Context context = getActivity(); 898 final Bundle arguments = getArguments(); 899 final String hostName = arguments.getString(ARGS_HOST_NAME); 900 final AccountCheckSettingsFragment target = 901 (AccountCheckSettingsFragment) getTargetFragment(); 902 903 return new AlertDialog.Builder(context) 904 .setIconAttribute(android.R.attr.alertDialogIcon) 905 .setTitle(context.getString(R.string.account_setup_security_required_title)) 906 .setMessage(context.getString( 907 R.string.account_setup_security_policies_required_fmt, hostName)) 908 .setCancelable(true) 909 .setPositiveButton( 910 context.getString(R.string.okay_action), 911 new DialogInterface.OnClickListener() { 912 @Override 913 public void onClick(DialogInterface dialog, int which) { 914 dismiss(); 915 target.onSecurityRequiredDialogResultOk(true); 916 } 917 }) 918 .setNegativeButton( 919 context.getString(R.string.cancel_action), 920 new DialogInterface.OnClickListener() { 921 @Override 922 public void onClick(DialogInterface dialog, int which) { 923 dismiss(); 924 target.onSecurityRequiredDialogResultOk(false); 925 } 926 }) 927 .create(); 928 } 929 930 } 931 932} 933