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