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