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.content.Context; 21import android.content.Intent; 22import android.net.Uri; 23import android.os.Bundle; 24import android.os.RemoteException; 25import android.text.Editable; 26import android.text.TextWatcher; 27import android.util.Log; 28import android.view.LayoutInflater; 29import android.view.View; 30import android.view.ViewGroup; 31import android.widget.CheckBox; 32import android.widget.CompoundButton; 33import android.widget.CompoundButton.OnCheckedChangeListener; 34import android.widget.EditText; 35import android.widget.TextView; 36 37import com.android.email.Email; 38import com.android.email.R; 39import com.android.email.activity.UiUtilities; 40import com.android.email.provider.AccountBackupRestore; 41import com.android.email.service.EmailServiceUtils; 42import com.android.email.view.CertificateSelector; 43import com.android.email.view.CertificateSelector.HostCallback; 44import com.android.emailcommon.Device; 45import com.android.emailcommon.Logging; 46import com.android.emailcommon.provider.Account; 47import com.android.emailcommon.provider.HostAuth; 48import com.android.emailcommon.utility.CertificateRequestor; 49import com.android.emailcommon.utility.Utility; 50 51import java.io.IOException; 52 53/** 54 * Provides generic setup for Exchange accounts. 55 * 56 * This fragment is used by AccountSetupExchange (for creating accounts) and by AccountSettingsXL 57 * (for editing existing accounts). 58 */ 59public class AccountSetupExchangeFragment extends AccountServerBaseFragment 60 implements OnCheckedChangeListener, HostCallback { 61 62 private static final int CERTIFICATE_REQUEST = 0; 63 private final static String STATE_KEY_CREDENTIAL = "AccountSetupExchangeFragment.credential"; 64 private final static String STATE_KEY_LOADED = "AccountSetupExchangeFragment.loaded"; 65 66 private EditText mUsernameView; 67 private EditText mPasswordView; 68 private EditText mServerView; 69 private CheckBox mSslSecurityView; 70 private CheckBox mTrustCertificatesView; 71 private CertificateSelector mClientCertificateSelector; 72 73 // Support for lifecycle 74 private boolean mStarted; 75 /* package */ boolean mLoaded; 76 private String mCacheLoginCredential; 77 78 /** 79 * Called to do initial creation of a fragment. This is called after 80 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 81 */ 82 @Override 83 public void onCreate(Bundle savedInstanceState) { 84 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 85 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreate"); 86 } 87 super.onCreate(savedInstanceState); 88 89 if (savedInstanceState != null) { 90 mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL); 91 mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false); 92 } 93 mBaseScheme = HostAuth.SCHEME_EAS; 94 } 95 96 @Override 97 public View onCreateView(LayoutInflater inflater, ViewGroup container, 98 Bundle savedInstanceState) { 99 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 100 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreateView"); 101 } 102 int layoutId = mSettingsMode 103 ? R.layout.account_settings_exchange_fragment 104 : R.layout.account_setup_exchange_fragment; 105 106 View view = inflater.inflate(layoutId, container, false); 107 final Context context = getActivity(); 108 109 mUsernameView = UiUtilities.getView(view, R.id.account_username); 110 mPasswordView = UiUtilities.getView(view, R.id.account_password); 111 mServerView = UiUtilities.getView(view, R.id.account_server); 112 mSslSecurityView = UiUtilities.getView(view, R.id.account_ssl); 113 mSslSecurityView.setOnCheckedChangeListener(this); 114 mTrustCertificatesView = UiUtilities.getView(view, R.id.account_trust_certificates); 115 mClientCertificateSelector = UiUtilities.getView(view, R.id.client_certificate_selector); 116 117 // Calls validateFields() which enables or disables the Next button 118 // based on the fields' validity. 119 TextWatcher validationTextWatcher = new TextWatcher() { 120 public void afterTextChanged(Editable s) { 121 validateFields(); 122 } 123 124 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 125 public void onTextChanged(CharSequence s, int start, int before, int count) { } 126 }; 127 // We're editing an existing account; don't allow modification of the user name 128 if (mSettingsMode) { 129 makeTextViewUneditable(mUsernameView, 130 getString(R.string.account_setup_username_uneditable_error)); 131 } 132 mUsernameView.addTextChangedListener(validationTextWatcher); 133 mPasswordView.addTextChangedListener(validationTextWatcher); 134 mServerView.addTextChangedListener(validationTextWatcher); 135 136 EditText lastView = mServerView; 137 lastView.setOnEditorActionListener(mDismissImeOnDoneListener); 138 139 String deviceId = ""; 140 try { 141 deviceId = Device.getDeviceId(context); 142 } catch (IOException e) { 143 // Not required 144 } 145 ((TextView) UiUtilities.getView(view, R.id.device_id)).setText(deviceId); 146 147 // Additional setup only used while in "settings" mode 148 onCreateViewSettingsMode(view); 149 150 return view; 151 } 152 153 @Override 154 public void onActivityCreated(Bundle savedInstanceState) { 155 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 156 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated"); 157 } 158 super.onActivityCreated(savedInstanceState); 159 mClientCertificateSelector.setHostActivity(this); 160 } 161 162 /** 163 * Called when the Fragment is visible to the user. 164 */ 165 @Override 166 public void onStart() { 167 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 168 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStart"); 169 } 170 super.onStart(); 171 mStarted = true; 172 loadSettings(SetupData.getAccount()); 173 } 174 175 /** 176 * Called when the fragment is visible to the user and actively running. 177 */ 178 @Override 179 public void onResume() { 180 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 181 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onResume"); 182 } 183 super.onResume(); 184 validateFields(); 185 } 186 187 @Override 188 public void onPause() { 189 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 190 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onPause"); 191 } 192 super.onPause(); 193 } 194 195 /** 196 * Called when the Fragment is no longer started. 197 */ 198 @Override 199 public void onStop() { 200 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 201 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStop"); 202 } 203 super.onStop(); 204 mStarted = false; 205 } 206 207 /** 208 * Called when the fragment is no longer in use. 209 */ 210 @Override 211 public void onDestroy() { 212 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 213 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onDestroy"); 214 } 215 super.onDestroy(); 216 } 217 218 @Override 219 public void onSaveInstanceState(Bundle outState) { 220 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 221 Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onSaveInstanceState"); 222 } 223 super.onSaveInstanceState(outState); 224 225 outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential); 226 outState.putBoolean(STATE_KEY_LOADED, mLoaded); 227 } 228 229 /** 230 * Activity provides callbacks here. This also triggers loading and setting up the UX 231 */ 232 @Override 233 public void setCallback(Callback callback) { 234 super.setCallback(callback); 235 if (mStarted) { 236 loadSettings(SetupData.getAccount()); 237 } 238 } 239 240 /** 241 * Force the given account settings to be loaded using {@link #loadSettings(Account)}. 242 * 243 * @return true if the loaded values pass validation 244 */ 245 private boolean forceLoadSettings(Account account) { 246 mLoaded = false; 247 return loadSettings(account); 248 } 249 250 /** 251 * Load the given account settings into the UI and then ensure the settings are valid. 252 * As an optimization, if the settings have already been loaded, the UI will not be 253 * updated, but, the account fields will still be validated. 254 * 255 * @return true if the loaded values pass validation 256 */ 257 /*package*/ boolean loadSettings(Account account) { 258 if (mLoaded) return validateFields(); 259 260 HostAuth hostAuth = account.mHostAuthRecv; 261 262 String userName = hostAuth.mLogin; 263 if (userName != null) { 264 // Add a backslash to the start of the username, but only if the username has no 265 // backslash in it. 266 if (userName.indexOf('\\') < 0) { 267 userName = "\\" + userName; 268 } 269 mUsernameView.setText(userName); 270 } 271 272 if (hostAuth.mPassword != null) { 273 mPasswordView.setText(hostAuth.mPassword); 274 // Since username is uneditable, focus on the next editable field 275 if (mSettingsMode) { 276 mPasswordView.requestFocus(); 277 } 278 } 279 280 String protocol = hostAuth.mProtocol; 281 if (protocol == null || !protocol.startsWith("eas")) { 282 throw new Error("Unknown account type: " + protocol); 283 } 284 285 if (hostAuth.mAddress != null) { 286 mServerView.setText(hostAuth.mAddress); 287 } 288 289 boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL); 290 boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL); 291 mSslSecurityView.setChecked(ssl); 292 mTrustCertificatesView.setChecked(trustCertificates); 293 if (hostAuth.mClientCertAlias != null) { 294 mClientCertificateSelector.setCertificate(hostAuth.mClientCertAlias); 295 } 296 onUseSslChanged(ssl); 297 298 mLoadedRecvAuth = hostAuth; 299 mLoaded = true; 300 return validateFields(); 301 } 302 303 private boolean usernameFieldValid(EditText usernameView) { 304 return Utility.isTextViewNotEmpty(usernameView) && 305 !usernameView.getText().toString().equals("\\"); 306 } 307 308 /** 309 * Check the values in the fields and decide if it makes sense to enable the "next" button 310 * @return true if all fields are valid, false if any fields are incomplete 311 */ 312 private boolean validateFields() { 313 if (!mLoaded) return false; 314 boolean enabled = usernameFieldValid(mUsernameView) 315 && Utility.isTextViewNotEmpty(mPasswordView) 316 && Utility.isServerNameValid(mServerView); 317 enableNextButton(enabled); 318 319 // Warn (but don't prevent) if password has leading/trailing spaces 320 AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView); 321 322 return enabled; 323 } 324 325 @Override 326 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 327 if (buttonView.getId() == R.id.account_ssl) { 328 onUseSslChanged(isChecked); 329 } 330 } 331 332 public void onUseSslChanged(boolean useSsl) { 333 int mode = useSsl ? View.VISIBLE : View.GONE; 334 mTrustCertificatesView.setVisibility(mode); 335 UiUtilities.setVisibilitySafe(getView(), R.id.account_trust_certificates_divider, mode); 336 mClientCertificateSelector.setVisibility(mode); 337 UiUtilities.setVisibilitySafe(getView(), R.id.client_certificate_divider, mode); 338 } 339 340 @Override 341 public void onCheckSettingsComplete(final int result) { 342 if (result == AccountCheckSettingsFragment.CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED) { 343 mSslSecurityView.setChecked(true); 344 onCertificateRequested(); 345 return; 346 } 347 super.onCheckSettingsComplete(result); 348 } 349 350 351 /** 352 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 353 * Blocking - do not call from UI Thread. 354 */ 355 @Override 356 public void saveSettingsAfterEdit() { 357 Account account = SetupData.getAccount(); 358 account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues()); 359 account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues()); 360 // For EAS, notify ExchangeService that the password has changed 361 try { 362 EmailServiceUtils.getExchangeService(mContext, null).hostChanged(account.mId); 363 } catch (RemoteException e) { 364 // Nothing to be done if this fails 365 } 366 // Update the backup (side copy) of the accounts 367 AccountBackupRestore.backup(mContext); 368 } 369 370 /** 371 * Entry point from Activity after entering new settings and verifying them. For setup mode. 372 */ 373 @Override 374 public void saveSettingsAfterSetup() { 375 } 376 377 /** 378 * Entry point from Activity after entering new settings and verifying them. For setup mode. 379 */ 380 public boolean setHostAuthFromAutodiscover(HostAuth newHostAuth) { 381 Account account = SetupData.getAccount(); 382 account.mHostAuthSend = newHostAuth; 383 account.mHostAuthRecv = newHostAuth; 384 // Auto discovery may have changed the auth settings; force load them 385 return forceLoadSettings(account); 386 } 387 388 /** 389 * Implements AccountCheckSettingsFragment.Callbacks 390 */ 391 @Override 392 public void onAutoDiscoverComplete(int result, HostAuth hostAuth) { 393 AccountSetupExchange activity = (AccountSetupExchange) getActivity(); 394 activity.onAutoDiscoverComplete(result, hostAuth); 395 } 396 397 /** 398 * Entry point from Activity, when "next" button is clicked 399 */ 400 @Override 401 public void onNext() { 402 Account account = SetupData.getAccount(); 403 404 String userName = mUsernameView.getText().toString().trim(); 405 if (userName.startsWith("\\")) { 406 userName = userName.substring(1); 407 } 408 mCacheLoginCredential = userName; 409 String userPassword = mPasswordView.getText().toString(); 410 411 int flags = 0; 412 if (mSslSecurityView.isChecked()) { 413 flags |= HostAuth.FLAG_SSL; 414 } 415 if (mTrustCertificatesView.isChecked()) { 416 flags |= HostAuth.FLAG_TRUST_ALL; 417 } 418 String certAlias = mClientCertificateSelector.getCertificate(); 419 String serverAddress = mServerView.getText().toString().trim(); 420 421 int port = mSslSecurityView.isChecked() ? 443 : 80; 422 HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext); 423 sendAuth.setLogin(userName, userPassword); 424 sendAuth.setConnection(mBaseScheme, serverAddress, port, flags, certAlias); 425 sendAuth.mDomain = null; 426 427 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 428 recvAuth.setLogin(userName, userPassword); 429 recvAuth.setConnection(mBaseScheme, serverAddress, port, flags, certAlias); 430 recvAuth.mDomain = null; 431 432 // Check for a duplicate account (requires async DB work) and if OK, proceed with check 433 startDuplicateTaskCheck(account.mId, serverAddress, mCacheLoginCredential, 434 SetupData.CHECK_INCOMING); 435 } 436 437 @Override 438 public void onCertificateRequested() { 439 Intent intent = new Intent(CertificateRequestor.ACTION_REQUEST_CERT); 440 intent.setData(Uri.parse("eas://com.android.emailcommon/certrequest")); 441 startActivityForResult(intent, CERTIFICATE_REQUEST); 442 } 443 444 @Override 445 public void onActivityResult(int requestCode, int resultCode, Intent data) { 446 if (requestCode == CERTIFICATE_REQUEST && resultCode == Activity.RESULT_OK) { 447 String certAlias = data.getStringExtra(CertificateRequestor.RESULT_ALIAS); 448 if (certAlias != null) { 449 mClientCertificateSelector.setCertificate(certAlias); 450 } 451 } 452 } 453 454} 455