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