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