AccountSetupExchange.java revision d6d874f8c6ce2580ef9ec2406fe411af45b2d92d
1/* 2 * Copyright (C) 2009 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 com.android.email.AccountBackupRestore; 20import com.android.email.ExchangeUtils; 21import com.android.email.R; 22import com.android.email.Utility; 23import com.android.email.SecurityPolicy.PolicySet; 24import com.android.email.provider.EmailContent; 25import com.android.email.provider.EmailContent.Account; 26import com.android.email.provider.EmailContent.HostAuth; 27import com.android.email.service.EmailServiceProxy; 28import com.android.exchange.SyncManager; 29 30import android.app.Activity; 31import android.app.AlertDialog; 32import android.app.Dialog; 33import android.content.DialogInterface; 34import android.content.Intent; 35import android.os.Bundle; 36import android.os.Parcelable; 37import android.os.RemoteException; 38import android.text.Editable; 39import android.text.TextWatcher; 40import android.view.View; 41import android.view.View.OnClickListener; 42import android.widget.Button; 43import android.widget.CheckBox; 44import android.widget.CompoundButton; 45import android.widget.EditText; 46import android.widget.TextView; 47import android.widget.CompoundButton.OnCheckedChangeListener; 48 49import java.io.IOException; 50import java.net.URI; 51import java.net.URISyntaxException; 52 53/** 54 * Provides generic setup for Exchange accounts. The following fields are supported: 55 * 56 * Email Address (from previous setup screen) 57 * Server 58 * Domain 59 * Requires SSL? 60 * User (login) 61 * Password 62 * 63 * There are two primary paths through this activity: 64 * Edit existing: 65 * Load existing values from account into fields 66 * When user clicks 'next': 67 * Confirm not a duplicate account 68 * Try new values (check settings) 69 * If new values are OK: 70 * Write new values (save to provider) 71 * finish() (pop to previous) 72 * 73 * Creating New: 74 * Try Auto-discover to get details from server 75 * If Auto-discover reports an authentication failure: 76 * finish() (pop to previous, to re-enter username & password) 77 * If Auto-discover succeeds: 78 * write server's account details into account 79 * Load values from account into fields 80 * Confirm not a duplicate account 81 * Try new values (check settings) 82 * If new values are OK: 83 * Write new values (save to provider) 84 * Proceed to options screen 85 * finish() (removes self from back stack) 86 * 87 * NOTE: The manifest for this activity has it ignore config changes, because 88 * we don't want to restart on every orientation - this would launch autodiscover again. 89 * Do not attempt to define orientation-specific resources, they won't be loaded. 90 */ 91public class AccountSetupExchange extends Activity implements OnClickListener, 92 OnCheckedChangeListener { 93 /*package*/ static final String EXTRA_ACCOUNT = "account"; 94 private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; 95 private static final String EXTRA_EAS_FLOW = "easFlow"; 96 /*package*/ static final String EXTRA_DISABLE_AUTO_DISCOVER = "disableAutoDiscover"; 97 98 private final static int DIALOG_DUPLICATE_ACCOUNT = 1; 99 100 private EditText mUsernameView; 101 private EditText mPasswordView; 102 private EditText mServerView; 103 private CheckBox mSslSecurityView; 104 private CheckBox mTrustCertificatesView; 105 106 private Button mNextButton; 107 private Account mAccount; 108 private boolean mMakeDefault; 109 private String mCacheLoginCredential; 110 private String mDuplicateAccountName; 111 112 public static void actionIncomingSettings(Activity fromActivity, Account account, 113 boolean makeDefault, boolean easFlowMode, boolean allowAutoDiscover) { 114 Intent i = new Intent(fromActivity, AccountSetupExchange.class); 115 i.putExtra(EXTRA_ACCOUNT, account); 116 i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault); 117 i.putExtra(EXTRA_EAS_FLOW, easFlowMode); 118 if (!allowAutoDiscover) { 119 i.putExtra(EXTRA_DISABLE_AUTO_DISCOVER, true); 120 } 121 fromActivity.startActivity(i); 122 } 123 124 public static void actionEditIncomingSettings(Activity fromActivity, Account account) 125 { 126 Intent i = new Intent(fromActivity, AccountSetupExchange.class); 127 i.setAction(Intent.ACTION_EDIT); 128 i.putExtra(EXTRA_ACCOUNT, account); 129 fromActivity.startActivity(i); 130 } 131 132 /** 133 * For now, we'll simply replicate outgoing, for the purpose of satisfying the 134 * account settings flow. 135 */ 136 public static void actionEditOutgoingSettings(Activity fromActivity, Account account) 137 { 138 Intent i = new Intent(fromActivity, AccountSetupExchange.class); 139 i.setAction(Intent.ACTION_EDIT); 140 i.putExtra(EXTRA_ACCOUNT, account); 141 fromActivity.startActivity(i); 142 } 143 144 @Override 145 public void onCreate(Bundle savedInstanceState) { 146 super.onCreate(savedInstanceState); 147 setContentView(R.layout.account_setup_exchange); 148 149 mUsernameView = (EditText) findViewById(R.id.account_username); 150 mPasswordView = (EditText) findViewById(R.id.account_password); 151 mServerView = (EditText) findViewById(R.id.account_server); 152 mSslSecurityView = (CheckBox) findViewById(R.id.account_ssl); 153 mSslSecurityView.setOnCheckedChangeListener(this); 154 mTrustCertificatesView = (CheckBox) findViewById(R.id.account_trust_certificates); 155 156 mNextButton = (Button)findViewById(R.id.next); 157 mNextButton.setOnClickListener(this); 158 159 /* 160 * Calls validateFields() which enables or disables the Next button 161 * based on the fields' validity. 162 */ 163 TextWatcher validationTextWatcher = new TextWatcher() { 164 public void afterTextChanged(Editable s) { 165 validateFields(); 166 } 167 168 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 169 } 170 171 public void onTextChanged(CharSequence s, int start, int before, int count) { 172 } 173 }; 174 mUsernameView.addTextChangedListener(validationTextWatcher); 175 mPasswordView.addTextChangedListener(validationTextWatcher); 176 mServerView.addTextChangedListener(validationTextWatcher); 177 178 Intent intent = getIntent(); 179 mAccount = (EmailContent.Account) intent.getParcelableExtra(EXTRA_ACCOUNT); 180 mMakeDefault = intent.getBooleanExtra(EXTRA_MAKE_DEFAULT, false); 181 182 /* 183 * If we're being reloaded we override the original account with the one 184 * we saved 185 */ 186 if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { 187 mAccount = (EmailContent.Account) savedInstanceState.getParcelable(EXTRA_ACCOUNT); 188 } 189 190 loadFields(mAccount); 191 validateFields(); 192 193 // If we've got a username and password and we're NOT editing, try autodiscover 194 String username = mAccount.mHostAuthRecv.mLogin; 195 String password = mAccount.mHostAuthRecv.mPassword; 196 if (username != null && password != null && 197 !Intent.ACTION_EDIT.equals(intent.getAction())) { 198 // NOTE: Disabling AutoDiscover is only used in unit tests 199 boolean disableAutoDiscover = 200 intent.getBooleanExtra(EXTRA_DISABLE_AUTO_DISCOVER, false); 201 if (!disableAutoDiscover) { 202 AccountSetupCheckSettings 203 .actionAutoDiscover(this, mAccount, mAccount.mEmailAddress, password); 204 } 205 } 206 207 //EXCHANGE-REMOVE-SECTION-START 208 // Show device ID 209 try { 210 ((TextView) findViewById(R.id.device_id)).setText(SyncManager.getDeviceId(this)); 211 } catch (IOException ignore) { 212 // There's nothing we can do here... 213 } 214 //EXCHANGE-REMOVE-SECTION-END 215 } 216 217 @Override 218 public void onSaveInstanceState(Bundle outState) { 219 super.onSaveInstanceState(outState); 220 outState.putParcelable(EXTRA_ACCOUNT, mAccount); 221 } 222 223 private boolean usernameFieldValid(EditText usernameView) { 224 return Utility.requiredFieldValid(usernameView) && 225 !usernameView.getText().toString().equals("\\"); 226 } 227 228 /** 229 * Prepare a cached dialog with current values (e.g. account name) 230 */ 231 @Override 232 public Dialog onCreateDialog(int id) { 233 switch (id) { 234 case DIALOG_DUPLICATE_ACCOUNT: 235 return new AlertDialog.Builder(this) 236 .setIcon(android.R.drawable.ic_dialog_alert) 237 .setTitle(R.string.account_duplicate_dlg_title) 238 .setMessage(getString(R.string.account_duplicate_dlg_message_fmt, 239 mDuplicateAccountName)) 240 .setPositiveButton(R.string.okay_action, 241 new DialogInterface.OnClickListener() { 242 public void onClick(DialogInterface dialog, int which) { 243 dismissDialog(DIALOG_DUPLICATE_ACCOUNT); 244 } 245 }) 246 .create(); 247 } 248 return null; 249 } 250 251 /** 252 * Update a cached dialog with current values (e.g. account name) 253 */ 254 @Override 255 public void onPrepareDialog(int id, Dialog dialog) { 256 switch (id) { 257 case DIALOG_DUPLICATE_ACCOUNT: 258 if (mDuplicateAccountName != null) { 259 AlertDialog alert = (AlertDialog) dialog; 260 alert.setMessage(getString(R.string.account_duplicate_dlg_message_fmt, 261 mDuplicateAccountName)); 262 } 263 break; 264 } 265 } 266 267 /** 268 * Copy mAccount's values into UI fields 269 */ 270 /* package */ void loadFields(Account account) { 271 HostAuth hostAuth = account.mHostAuthRecv; 272 273 String userName = hostAuth.mLogin; 274 if (userName != null) { 275 // Add a backslash to the start of the username, but only if the username has no 276 // backslash in it. 277 if (userName.indexOf('\\') < 0) { 278 userName = "\\" + userName; 279 } 280 mUsernameView.setText(userName); 281 } 282 283 if (hostAuth.mPassword != null) { 284 mPasswordView.setText(hostAuth.mPassword); 285 } 286 287 String protocol = hostAuth.mProtocol; 288 if (protocol == null || !protocol.startsWith("eas")) { 289 throw new Error("Unknown account type: " + account.getStoreUri(this)); 290 } 291 292 if (hostAuth.mAddress != null) { 293 mServerView.setText(hostAuth.mAddress); 294 } 295 296 boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL); 297 boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL_CERTIFICATES); 298 mSslSecurityView.setChecked(ssl); 299 mTrustCertificatesView.setChecked(trustCertificates); 300 mTrustCertificatesView.setVisibility(ssl ? View.VISIBLE : View.GONE); 301 } 302 303 /** 304 * Check the values in the fields and decide if it makes sense to enable the "next" button 305 * NOTE: Does it make sense to extract & combine with similar code in AccountSetupIncoming? 306 * @return true if all fields are valid, false if fields are incomplete 307 */ 308 private boolean validateFields() { 309 boolean enabled = usernameFieldValid(mUsernameView) 310 && Utility.requiredFieldValid(mPasswordView) 311 && Utility.requiredFieldValid(mServerView); 312 if (enabled) { 313 try { 314 URI uri = getUri(); 315 } catch (URISyntaxException use) { 316 enabled = false; 317 } 318 } 319 mNextButton.setEnabled(enabled); 320 Utility.setCompoundDrawablesAlpha(mNextButton, enabled ? 255 : 128); 321 return enabled; 322 } 323 324 private void doOptions(PolicySet policySet) { 325 boolean easFlowMode = getIntent().getBooleanExtra(EXTRA_EAS_FLOW, false); 326 AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault, easFlowMode, policySet); 327 finish(); 328 } 329 330 /** 331 * There are three cases handled here, so we split out into separate sections. 332 * 1. Validate existing account (edit) 333 * 2. Validate new account 334 * 3. Autodiscover for new account 335 * 336 * For each case, there are two or more paths for success or failure. 337 */ 338 @Override 339 public void onActivityResult(int requestCode, int resultCode, Intent data) { 340 if (requestCode == AccountSetupCheckSettings.REQUEST_CODE_VALIDATE) { 341 if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { 342 doActivityResultValidateExistingAccount(resultCode, data); 343 } else { 344 doActivityResultValidateNewAccount(resultCode, data); 345 } 346 } else if (requestCode == AccountSetupCheckSettings.REQUEST_CODE_AUTO_DISCOVER) { 347 doActivityResultAutoDiscoverNewAccount(resultCode, data); 348 } 349 } 350 351 /** 352 * Process activity result when validating existing account 353 */ 354 private void doActivityResultValidateExistingAccount(int resultCode, Intent data) { 355 if (resultCode == RESULT_OK) { 356 if (mAccount.isSaved()) { 357 // Account.update will NOT save the HostAuth's 358 mAccount.update(this, mAccount.toContentValues()); 359 mAccount.mHostAuthRecv.update(this, 360 mAccount.mHostAuthRecv.toContentValues()); 361 mAccount.mHostAuthSend.update(this, 362 mAccount.mHostAuthSend.toContentValues()); 363 if (mAccount.mHostAuthRecv.mProtocol.equals("eas")) { 364 // For EAS, notify SyncManager that the password has changed 365 try { 366 ExchangeUtils.getExchangeEmailService(this, null) 367 .hostChanged(mAccount.mId); 368 } catch (RemoteException e) { 369 // Nothing to be done if this fails 370 } 371 } 372 } else { 373 // Account.save will save the HostAuth's 374 mAccount.save(this); 375 } 376 // Update the backup (side copy) of the accounts 377 AccountBackupRestore.backupAccounts(this); 378 finish(); 379 } 380 // else (resultCode not OK) - just return into this activity for further editing 381 } 382 383 /** 384 * Process activity result when validating new account 385 */ 386 private void doActivityResultValidateNewAccount(int resultCode, Intent data) { 387 if (resultCode == RESULT_OK) { 388 // Go directly to next screen 389 PolicySet ps = null; 390 if ((data != null) && data.hasExtra(EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET)) { 391 ps = (PolicySet)data.getParcelableExtra( 392 EmailServiceProxy.VALIDATE_BUNDLE_POLICY_SET); 393 } 394 doOptions(ps); 395 } else if (resultCode == AccountSetupCheckSettings.RESULT_SECURITY_REQUIRED_USER_CANCEL) { 396 finish(); 397 } 398 // else (resultCode not OK) - just return into this activity for further editing 399 } 400 401 /** 402 * Process activity result when validating new account 403 */ 404 private void doActivityResultAutoDiscoverNewAccount(int resultCode, Intent data) { 405 // If authentication failed, exit immediately (to re-enter credentials) 406 if (resultCode == AccountSetupCheckSettings.RESULT_AUTO_DISCOVER_AUTH_FAILED) { 407 finish(); 408 return; 409 } 410 411 // If data was returned, populate the account & populate the UI fields and validate it 412 if (data != null) { 413 Parcelable p = data.getParcelableExtra("HostAuth"); 414 if (p != null) { 415 HostAuth hostAuth = (HostAuth)p; 416 mAccount.mHostAuthSend = hostAuth; 417 mAccount.mHostAuthRecv = hostAuth; 418 loadFields(mAccount); 419 if (validateFields()) { 420 // "click" next to launch server verification 421 onNext(); 422 } 423 } 424 } 425 // Otherwise, proceed into this activity for manual setup 426 } 427 428 /** 429 * Attempt to create a URI from the fields provided. Throws URISyntaxException if there's 430 * a problem with the user input. 431 * @return a URI built from the account setup fields 432 */ 433 private URI getUri() throws URISyntaxException { 434 boolean sslRequired = mSslSecurityView.isChecked(); 435 boolean trustCertificates = mTrustCertificatesView.isChecked(); 436 String scheme = (sslRequired) 437 ? (trustCertificates ? "eas+ssl+trustallcerts" : "eas+ssl+") 438 : "eas"; 439 String userName = mUsernameView.getText().toString().trim(); 440 // Remove a leading backslash, if there is one, since we now automatically put one at 441 // the start of the username field 442 if (userName.startsWith("\\")) { 443 userName = userName.substring(1); 444 } 445 mCacheLoginCredential = userName; 446 String userInfo = userName + ":" + mPasswordView.getText().toString().trim(); 447 String host = mServerView.getText().toString().trim(); 448 String path = null; 449 450 URI uri = new URI( 451 scheme, 452 userInfo, 453 host, 454 0, 455 path, 456 null, 457 null); 458 459 return uri; 460 } 461 462 /** 463 * Note, in EAS, store & sender are the same, so we always populate them together 464 */ 465 private void onNext() { 466 try { 467 URI uri = getUri(); 468 mAccount.setStoreUri(this, uri.toString()); 469 mAccount.setSenderUri(this, uri.toString()); 470 471 // Stop here if the login credentials duplicate an existing account 472 // (unless they duplicate the existing account, as they of course will) 473 Account account = Utility.findExistingAccount(this, mAccount.mId, 474 uri.getHost(), mCacheLoginCredential); 475 if (account != null) { 476 mDuplicateAccountName = account.mDisplayName; 477 this.showDialog(DIALOG_DUPLICATE_ACCOUNT); 478 return; 479 } 480 } catch (URISyntaxException use) { 481 /* 482 * It's unrecoverable if we cannot create a URI from components that 483 * we validated to be safe. 484 */ 485 throw new Error(use); 486 } 487 488 AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, false); 489 } 490 491 public void onClick(View v) { 492 switch (v.getId()) { 493 case R.id.next: 494 onNext(); 495 break; 496 } 497 } 498 499 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 500 if (buttonView.getId() == R.id.account_ssl) { 501 mTrustCertificatesView.setVisibility(isChecked ? View.VISIBLE : View.GONE); 502 } 503 } 504} 505