1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.email.activity.setup; 18 19 import com.android.email.Email; 20 import com.android.email.R; 21 import com.android.email.mail.AuthenticationFailedException; 22 import com.android.email.mail.CertificateValidationException; 23 import com.android.email.mail.MessagingException; 24 import com.android.email.mail.Sender; 25 import com.android.email.mail.Store; 26 import com.android.email.provider.EmailContent; 27 import com.android.email.service.EmailServiceProxy; 28 29 import android.app.Activity; 30 import android.app.AlertDialog; 31 import android.content.DialogInterface; 32 import android.content.Intent; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Process; 36 import android.util.Log; 37 import android.view.View; 38 import android.view.View.OnClickListener; 39 import android.widget.Button; 40 import android.widget.ProgressBar; 41 import android.widget.TextView; 42 43 /** 44 * Checks the given settings to make sure that they can be used to send and 45 * receive mail. 46 * 47 * XXX NOTE: The manifest for this activity has it ignore config changes, because 48 * it doesn't correctly deal with restarting while its thread is running. 49 * Do not attempt to define orientation-specific resources, they won't be loaded. 50 */ 51 public class AccountSetupCheckSettings extends Activity implements OnClickListener { 52 53 // If true, returns immediately as if account was OK 54 private static final boolean DBG_SKIP_CHECK_OK = false; // DO NOT CHECK IN WHILE TRUE 55 // If true, returns immediately as if account was not OK 56 private static final boolean DBG_SKIP_CHECK_ERR = false; // DO NOT CHECK IN WHILE TRUE 57 // If true, performs real check(s), but always returns fixed OK result 58 private static final boolean DBG_FORCE_RESULT_OK = false; // DO NOT CHECK IN WHILE TRUE 59 60 private static final String EXTRA_ACCOUNT = "account"; 61 private static final String EXTRA_CHECK_INCOMING = "checkIncoming"; 62 private static final String EXTRA_CHECK_OUTGOING = "checkOutgoing"; 63 private static final String EXTRA_AUTO_DISCOVER = "autoDiscover"; 64 private static final String EXTRA_AUTO_DISCOVER_USERNAME = "userName"; 65 private static final String EXTRA_AUTO_DISCOVER_PASSWORD = "password"; 66 67 public static final int REQUEST_CODE_VALIDATE = 1; 68 public static final int REQUEST_CODE_AUTO_DISCOVER = 2; 69 70 // We'll define special result codes for certain types of connection results 71 public static final int RESULT_AUTO_DISCOVER_AUTH_FAILED = Activity.RESULT_FIRST_USER; 72 public static final int RESULT_SECURITY_REQUIRED_USER_CANCEL = Activity.RESULT_FIRST_USER + 1; 73 74 private Handler mHandler = new Handler(); 75 private ProgressBar mProgressBar; 76 private TextView mMessageView; 77 78 private EmailContent.Account mAccount; 79 private boolean mCheckIncoming; 80 private boolean mCheckOutgoing; 81 private boolean mAutoDiscover; 82 private boolean mCanceled; 83 private boolean mDestroyed; 84 85 public static void actionValidateSettings(Activity fromActivity, EmailContent.Account account, 86 boolean checkIncoming, boolean checkOutgoing) { 87 Intent i = new Intent(fromActivity, AccountSetupCheckSettings.class); 88 i.putExtra(EXTRA_ACCOUNT, account); 89 i.putExtra(EXTRA_CHECK_INCOMING, checkIncoming); 90 i.putExtra(EXTRA_CHECK_OUTGOING, checkOutgoing); 91 fromActivity.startActivityForResult(i, REQUEST_CODE_VALIDATE); 92 } 93 94 public static void actionAutoDiscover(Activity fromActivity, EmailContent.Account account, 95 String userName, String password) { 96 Intent i = new Intent(fromActivity, AccountSetupCheckSettings.class); 97 i.putExtra(EXTRA_ACCOUNT, account); 98 i.putExtra(EXTRA_AUTO_DISCOVER, true); 99 i.putExtra(EXTRA_AUTO_DISCOVER_USERNAME, userName); 100 i.putExtra(EXTRA_AUTO_DISCOVER_PASSWORD, password); 101 fromActivity.startActivityForResult(i, REQUEST_CODE_AUTO_DISCOVER); 102 } 103 104 /** 105 * We create this simple class so that showErrorDialog can differentiate between a regular 106 * auth error and an auth error during the autodiscover sequence and respond appropriately 107 */ 108 private class AutoDiscoverAuthenticationException extends AuthenticationFailedException { 109 private static final long serialVersionUID = 1L; 110 111 public AutoDiscoverAuthenticationException(String message) { 112 super(message); 113 } 114 } 115 116 @Override 117 public void onCreate(Bundle savedInstanceState) { 118 super.onCreate(savedInstanceState); 119 setContentView(R.layout.account_setup_check_settings); 120 mMessageView = (TextView)findViewById(R.id.message); 121 mProgressBar = (ProgressBar)findViewById(R.id.progress); 122 ((Button)findViewById(R.id.cancel)).setOnClickListener(this); 123 124 setMessage(R.string.account_setup_check_settings_retr_info_msg); 125 mProgressBar.setIndeterminate(true); 126 127 // For debugging UI only, force a true or false result - don't actually try connection 128 if (DBG_SKIP_CHECK_OK || DBG_SKIP_CHECK_ERR) { 129 setResult(DBG_SKIP_CHECK_OK ? RESULT_OK : RESULT_CANCELED); 130 finish(); 131 return; 132 } 133 134 final Intent intent = getIntent(); 135 mAccount = (EmailContent.Account)intent.getParcelableExtra(EXTRA_ACCOUNT); 136 mCheckIncoming = intent.getBooleanExtra(EXTRA_CHECK_INCOMING, false); 137 mCheckOutgoing = intent.getBooleanExtra(EXTRA_CHECK_OUTGOING, false); 138 mAutoDiscover = intent.getBooleanExtra(EXTRA_AUTO_DISCOVER, false); 139 140 new Thread() { 141 @Override 142 public void run() { 143 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 144 try { 145 if (mDestroyed) { 146 return; 147 } 148 if (mCanceled) { 149 finish(); 150 return; 151 } 152 if (mAutoDiscover) { 153 String userName = intent.getStringExtra(EXTRA_AUTO_DISCOVER_USERNAME); 154 String password = intent.getStringExtra(EXTRA_AUTO_DISCOVER_PASSWORD); 155 Log.d(Email.LOG_TAG, "Begin auto-discover for " + userName); 156 Store store = Store.getInstance( 157 mAccount.getStoreUri(AccountSetupCheckSettings.this), 158 getApplication(), null); 159 Bundle result = store.autoDiscover(AccountSetupCheckSettings.this, 160 userName, password); 161 // Result will be null if there was a remote exception 162 // Otherwise, we can check the exception code and handle auth failed 163 // Other errors will be ignored, and the user will be taken to manual 164 // setup 165 if (result != null) { 166 int errorCode = 167 result.getInt(EmailServiceProxy.AUTO_DISCOVER_BUNDLE_ERROR_CODE); 168 if (errorCode == MessagingException.AUTHENTICATION_FAILED) { 169 throw new AutoDiscoverAuthenticationException(null); 170 } else if (errorCode != MessagingException.NO_ERROR) { 171 setResult(RESULT_OK); 172 finish(); 173 } 174 // The success case is here 175 Intent resultIntent = new Intent(); 176 resultIntent.putExtra("HostAuth", result.getParcelable( 177 EmailServiceProxy.AUTO_DISCOVER_BUNDLE_HOST_AUTH)); 178 setResult(RESULT_OK, resultIntent); 179 finish(); 180 // auto-discover is never combined with other ops, so exit now 181 return; 182 } 183 } 184 if (mDestroyed) { 185 return; 186 } 187 if (mCanceled) { 188 finish(); 189 return; 190 } 191 if (mCheckIncoming) { 192 Log.d(Email.LOG_TAG, "Begin check of incoming email settings"); 193 setMessage(R.string.account_setup_check_settings_check_incoming_msg); 194 Store store = Store.getInstance( 195 mAccount.getStoreUri(AccountSetupCheckSettings.this), 196 getApplication(), null); 197 store.checkSettings(); 198 } 199 if (mDestroyed) { 200 return; 201 } 202 if (mCanceled) { 203 finish(); 204 return; 205 } 206 if (mCheckOutgoing) { 207 Log.d(Email.LOG_TAG, "Begin check of outgoing email settings"); 208 setMessage(R.string.account_setup_check_settings_check_outgoing_msg); 209 Sender sender = Sender.getInstance(getApplication(), 210 mAccount.getSenderUri(AccountSetupCheckSettings.this)); 211 sender.close(); 212 sender.open(); 213 sender.close(); 214 } 215 if (mDestroyed) { 216 return; 217 } 218 setResult(RESULT_OK); 219 finish(); 220 } catch (final AuthenticationFailedException afe) { 221 // Could be two separate blocks (one for AutoDiscover) but this way we save 222 // some code 223 String message = afe.getMessage(); 224 int id = (message == null) 225 ? R.string.account_setup_failed_dlg_auth_message 226 : R.string.account_setup_failed_dlg_auth_message_fmt; 227 showErrorDialog(afe instanceof AutoDiscoverAuthenticationException, 228 id, message); 229 } catch (final CertificateValidationException cve) { 230 String message = cve.getMessage(); 231 int id = (message == null) 232 ? R.string.account_setup_failed_dlg_certificate_message 233 : R.string.account_setup_failed_dlg_certificate_message_fmt; 234 showErrorDialog(false, id, message); 235 } catch (final MessagingException me) { 236 int exceptionType = me.getExceptionType(); 237 // Check for non-fatal errors first 238 if (exceptionType == MessagingException.SECURITY_POLICIES_REQUIRED) { 239 showSecurityRequiredDialog(); 240 return; 241 } 242 // Handle fatal errors 243 int id; 244 String message = me.getMessage(); 245 switch (exceptionType) { 246 case MessagingException.IOERROR: 247 id = R.string.account_setup_failed_ioerror; 248 break; 249 case MessagingException.TLS_REQUIRED: 250 id = R.string.account_setup_failed_tls_required; 251 break; 252 case MessagingException.AUTH_REQUIRED: 253 id = R.string.account_setup_failed_auth_required; 254 break; 255 case MessagingException.SECURITY_POLICIES_UNSUPPORTED: 256 id = R.string.account_setup_failed_security_policies_unsupported; 257 break; 258 case MessagingException.GENERAL_SECURITY: 259 id = R.string.account_setup_failed_security; 260 break; 261 default: 262 id = (message == null) 263 ? R.string.account_setup_failed_dlg_server_message 264 : R.string.account_setup_failed_dlg_server_message_fmt; 265 break; 266 } 267 showErrorDialog(false, id, message); 268 } 269 } 270 }.start(); 271 } 272 273 @Override 274 public void onDestroy() { 275 super.onDestroy(); 276 mDestroyed = true; 277 mCanceled = true; 278 } 279 280 private void setMessage(final int resId) { 281 mHandler.post(new Runnable() { 282 public void run() { 283 if (mDestroyed) { 284 return; 285 } 286 mMessageView.setText(getString(resId)); 287 } 288 }); 289 } 290 291 /** 292 * The first argument here indicates whether we return an OK result or a cancelled result 293 * An OK result is used by Exchange to indicate a failed authentication via AutoDiscover 294 * In that case, we'll end up returning to the AccountSetupBasic screen 295 */ 296 private void showErrorDialog(final boolean autoDiscoverAuthException, final int msgResId, 297 final Object... args) { 298 mHandler.post(new Runnable() { 299 public void run() { 300 if (mDestroyed) { 301 return; 302 } 303 mProgressBar.setIndeterminate(false); 304 new AlertDialog.Builder(AccountSetupCheckSettings.this) 305 .setIcon(android.R.drawable.ic_dialog_alert) 306 .setTitle(getString(R.string.account_setup_failed_dlg_title)) 307 .setMessage(getString(msgResId, args)) 308 .setCancelable(true) 309 .setPositiveButton( 310 getString(R.string.account_setup_failed_dlg_edit_details_action), 311 new DialogInterface.OnClickListener() { 312 public void onClick(DialogInterface dialog, int which) { 313 if (autoDiscoverAuthException) { 314 setResult(RESULT_AUTO_DISCOVER_AUTH_FAILED); 315 } else if (DBG_FORCE_RESULT_OK) { 316 setResult(RESULT_OK); 317 } 318 finish(); 319 } 320 }) 321 .show(); 322 } 323 }); 324 } 325 326 /** 327 * Display a dialog asking the user if they are willing to accept control by the remote 328 * server. This converts the MessagingException.SECURITY_POLICIES_REQUIRED exception into an 329 * Activity result of RESULT_OK, thus hiding the exception from the caller entirely. 330 * 331 * TODO: Perhaps use stronger button names than "OK" and "Cancel" (e.g. "Allow" / "Deny") 332 */ 333 private void showSecurityRequiredDialog() { 334 mHandler.post(new Runnable() { 335 public void run() { 336 if (mDestroyed) { 337 return; 338 } 339 mProgressBar.setIndeterminate(false); 340 String host = mAccount.mHostAuthRecv.mAddress; 341 Object[] args = new String[] { host }; 342 new AlertDialog.Builder(AccountSetupCheckSettings.this) 343 .setIcon(android.R.drawable.ic_dialog_alert) 344 .setTitle(getString(R.string.account_setup_security_required_title)) 345 .setMessage(getString( 346 R.string.account_setup_security_policies_required_fmt, args)) 347 .setCancelable(true) 348 .setPositiveButton( 349 getString(R.string.okay_action), 350 new DialogInterface.OnClickListener() { 351 public void onClick(DialogInterface dialog, int which) { 352 setResult(RESULT_OK); 353 finish(); 354 } 355 }) 356 .setNegativeButton( 357 getString(R.string.cancel_action), 358 new DialogInterface.OnClickListener() { 359 public void onClick(DialogInterface dialog, int which) { 360 setResult(RESULT_SECURITY_REQUIRED_USER_CANCEL); 361 finish(); 362 } 363 }) 364 .show(); 365 } 366 }); 367 } 368 369 private void onCancel() { 370 mCanceled = true; 371 setMessage(R.string.account_setup_check_settings_canceling_msg); 372 } 373 374 public void onClick(View v) { 375 switch (v.getId()) { 376 case R.id.cancel: 377 onCancel(); 378 break; 379 } 380 } 381 } 382