AccountSetupIncomingFragment.java revision ed4accb24facd1324948abc61b0791eb3dc97cc4
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.text.Editable; 25import android.text.TextUtils; 26import android.text.TextWatcher; 27import android.text.method.DigitsKeyListener; 28import android.view.LayoutInflater; 29import android.view.View; 30import android.view.ViewGroup; 31import android.view.inputmethod.EditorInfo; 32import android.widget.AdapterView; 33import android.widget.ArrayAdapter; 34import android.widget.EditText; 35import android.widget.Spinner; 36import android.widget.TextView; 37 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.service.EmailServiceUtils.EmailServiceInfo; 43import com.android.email.view.CertificateSelector; 44import com.android.email.view.CertificateSelector.HostCallback; 45import com.android.email2.ui.MailActivityEmail; 46import com.android.emailcommon.Device; 47import com.android.emailcommon.Logging; 48import com.android.emailcommon.provider.Account; 49import com.android.emailcommon.provider.HostAuth; 50import com.android.emailcommon.utility.CertificateRequestor; 51import com.android.emailcommon.utility.Utility; 52import com.android.mail.utils.LogUtils; 53 54import java.io.IOException; 55import java.util.ArrayList; 56 57/** 58 * Provides UI for IMAP/POP account settings. 59 * 60 * This fragment is used by AccountSetupIncoming (for creating accounts) and by AccountSettingsXL 61 * (for editing existing accounts). 62 */ 63public class AccountSetupIncomingFragment extends AccountServerBaseFragment 64 implements HostCallback { 65 66 private static final int CERTIFICATE_REQUEST = 0; 67 private final static String STATE_KEY_CREDENTIAL = "AccountSetupIncomingFragment.credential"; 68 private final static String STATE_KEY_LOADED = "AccountSetupIncomingFragment.loaded"; 69 70 private EditText mUsernameView; 71 private EditText mPasswordView; 72 private TextView mServerLabelView; 73 private EditText mServerView; 74 private EditText mPortView; 75 private Spinner mSecurityTypeView; 76 private TextView mDeletePolicyLabelView; 77 private Spinner mDeletePolicyView; 78 private View mImapPathPrefixSectionView; 79 private View mDeviceIdSectionView; 80 private EditText mImapPathPrefixView; 81 private CertificateSelector mClientCertificateSelector; 82 // Delete policy as loaded from the device 83 private int mLoadedDeletePolicy; 84 85 // Support for lifecycle 86 private boolean mStarted; 87 private boolean mLoaded; 88 private String mCacheLoginCredential; 89 private EmailServiceInfo mServiceInfo; 90 91 // Public no-args constructor needed for fragment re-instantiation 92 public AccountSetupIncomingFragment() {} 93 94 /** 95 * Called to do initial creation of a fragment. This is called after 96 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 97 */ 98 @Override 99 public void onCreate(Bundle savedInstanceState) { 100 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 101 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate"); 102 } 103 super.onCreate(savedInstanceState); 104 105 if (savedInstanceState != null) { 106 mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL); 107 mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false); 108 } 109 } 110 111 @Override 112 public View onCreateView(LayoutInflater inflater, ViewGroup container, 113 Bundle savedInstanceState) { 114 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 115 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView"); 116 } 117 int layoutId = mSettingsMode 118 ? R.layout.account_settings_incoming_fragment 119 : R.layout.account_setup_incoming_fragment; 120 121 View view = inflater.inflate(layoutId, container, false); 122 Context context = getActivity(); 123 124 final HostAuth recvAuth = SetupData.getAccount().mHostAuthRecv; 125 mServiceInfo = EmailServiceUtils.getServiceInfo(mContext, recvAuth.mProtocol); 126 127 mUsernameView = UiUtilities.getView(view, R.id.account_username); 128 mPasswordView = UiUtilities.getView(view, R.id.account_password); 129 mServerLabelView = UiUtilities.getView(view, R.id.account_server_label); 130 mServerView = UiUtilities.getView(view, R.id.account_server); 131 mPortView = UiUtilities.getView(view, R.id.account_port); 132 mSecurityTypeView = UiUtilities.getView(view, R.id.account_security_type); 133 mDeletePolicyLabelView = UiUtilities.getView(view, R.id.account_delete_policy_label); 134 mDeletePolicyView = UiUtilities.getView(view, R.id.account_delete_policy); 135 mImapPathPrefixSectionView = UiUtilities.getView(view, R.id.imap_path_prefix_section); 136 mDeviceIdSectionView = UiUtilities.getView(view, R.id.device_id_section); 137 mImapPathPrefixView = UiUtilities.getView(view, R.id.imap_path_prefix); 138 mClientCertificateSelector = UiUtilities.getView(view, R.id.client_certificate_selector); 139 140 // Set up security type spinner 141 ArrayList<SpinnerOption> securityTypes = new ArrayList<SpinnerOption>(); 142 securityTypes.add( 143 new SpinnerOption(HostAuth.FLAG_NONE, context.getString( 144 R.string.account_setup_incoming_security_none_label))); 145 securityTypes.add( 146 new SpinnerOption(HostAuth.FLAG_SSL, context.getString( 147 R.string.account_setup_incoming_security_ssl_label))); 148 securityTypes.add( 149 new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString( 150 R.string.account_setup_incoming_security_ssl_trust_certificates_label))); 151 if (mServiceInfo.offerTls) { 152 securityTypes.add( 153 new SpinnerOption(HostAuth.FLAG_TLS, context.getString( 154 R.string.account_setup_incoming_security_tls_label))); 155 securityTypes.add( 156 new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, 157 context.getString( 158 R.string.account_setup_incoming_security_tls_trust_certificates_label))); 159 } 160 ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>( 161 context, android.R.layout.simple_spinner_item, securityTypes); 162 securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 163 mSecurityTypeView.setAdapter(securityTypesAdapter); 164 165 if (mServiceInfo.offerLocalDeletes) { 166 SpinnerOption deletePolicies[] = { 167 new SpinnerOption(Account.DELETE_POLICY_NEVER, 168 context.getString( 169 R.string.account_setup_incoming_delete_policy_never_label)), 170 new SpinnerOption(Account.DELETE_POLICY_ON_DELETE, 171 context.getString( 172 R.string.account_setup_incoming_delete_policy_delete_label)), 173 }; 174 ArrayAdapter<SpinnerOption> deletePoliciesAdapter = 175 new ArrayAdapter<SpinnerOption>(context, 176 android.R.layout.simple_spinner_item, deletePolicies); 177 deletePoliciesAdapter.setDropDownViewResource( 178 android.R.layout.simple_spinner_dropdown_item); 179 mDeletePolicyView.setAdapter(deletePoliciesAdapter); 180 } 181 182 // Updates the port when the user changes the security type. This allows 183 // us to show a reasonable default which the user can change. 184 mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 185 @Override 186 public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { 187 updatePortFromSecurityType(); 188 } 189 190 @Override 191 public void onNothingSelected(AdapterView<?> arg0) { } 192 }); 193 194 // After any text edits, call validateFields() which enables or disables the Next button 195 TextWatcher validationTextWatcher = new TextWatcher() { 196 @Override 197 public void afterTextChanged(Editable s) { 198 validateFields(); 199 } 200 201 @Override 202 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 203 @Override 204 public void onTextChanged(CharSequence s, int start, int before, int count) { } 205 }; 206 // We're editing an existing account; don't allow modification of the user name 207 if (mSettingsMode) { 208 makeTextViewUneditable(mUsernameView, 209 getString(R.string.account_setup_username_uneditable_error)); 210 } 211 mUsernameView.addTextChangedListener(validationTextWatcher); 212 mPasswordView.addTextChangedListener(validationTextWatcher); 213 mServerView.addTextChangedListener(validationTextWatcher); 214 mPortView.addTextChangedListener(validationTextWatcher); 215 216 // Only allow digits in the port field. 217 mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); 218 219 // Additional setup only used while in "settings" mode 220 onCreateViewSettingsMode(view); 221 222 return view; 223 } 224 225 @Override 226 public void onActivityCreated(Bundle savedInstanceState) { 227 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 228 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated"); 229 } 230 super.onActivityCreated(savedInstanceState); 231 mClientCertificateSelector.setHostActivity(this); 232 } 233 234 /** 235 * Called when the Fragment is visible to the user. 236 */ 237 @Override 238 public void onStart() { 239 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 240 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart"); 241 } 242 super.onStart(); 243 mStarted = true; 244 configureEditor(); 245 loadSettings(); 246 } 247 248 /** 249 * Called when the fragment is visible to the user and actively running. 250 */ 251 @Override 252 public void onResume() { 253 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 254 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume"); 255 } 256 super.onResume(); 257 validateFields(); 258 } 259 260 @Override 261 public void onPause() { 262 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 263 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause"); 264 } 265 super.onPause(); 266 } 267 268 /** 269 * Called when the Fragment is no longer started. 270 */ 271 @Override 272 public void onStop() { 273 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 274 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop"); 275 } 276 super.onStop(); 277 mStarted = false; 278 } 279 280 /** 281 * Called when the fragment is no longer in use. 282 */ 283 @Override 284 public void onDestroy() { 285 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 286 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy"); 287 } 288 super.onDestroy(); 289 } 290 291 @Override 292 public void onSaveInstanceState(Bundle outState) { 293 if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) { 294 LogUtils.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState"); 295 } 296 super.onSaveInstanceState(outState); 297 298 outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential); 299 outState.putBoolean(STATE_KEY_LOADED, mLoaded); 300 } 301 302 /** 303 * Activity provides callbacks here. This also triggers loading and setting up the UX 304 */ 305 @Override 306 public void setCallback(Callback callback) { 307 super.setCallback(callback); 308 if (mStarted) { 309 configureEditor(); 310 loadSettings(); 311 } 312 } 313 314 /** 315 * Configure the editor for the account type 316 */ 317 private void configureEditor() { 318 Account account = SetupData.getAccount(); 319 if (account == null || account.mHostAuthRecv == null) { 320 LogUtils.e(Logging.LOG_TAG, 321 "null account or host auth. account null: %b host auth null: %b", 322 account == null, account == null || account.mHostAuthRecv == null); 323 return; 324 } 325 TextView lastView = mImapPathPrefixView; 326 mBaseScheme = account.mHostAuthRecv.mProtocol; 327 mServerLabelView.setText(R.string.account_setup_incoming_server_label); 328 mServerView.setContentDescription(getResources().getText( 329 R.string.account_setup_incoming_server_label)); 330 if (!mServiceInfo.offerPrefix) { 331 mImapPathPrefixSectionView.setVisibility(View.GONE); 332 lastView = mPortView; 333 } 334 if (!mServiceInfo.offerLocalDeletes) { 335 mDeletePolicyLabelView.setVisibility(View.GONE); 336 mDeletePolicyView.setVisibility(View.GONE); 337 mPortView.setImeOptions(EditorInfo.IME_ACTION_NEXT); 338 } 339 lastView.setOnEditorActionListener(mDismissImeOnDoneListener); 340 } 341 342 /** 343 * Load the current settings into the UI 344 */ 345 private void loadSettings() { 346 if (mLoaded) return; 347 348 Account account = SetupData.getAccount(); 349 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 350 351 String username = recvAuth.mLogin; 352 if (username != null) { 353 //*** For eas? 354 // Add a backslash to the start of the username, but only if the username has no 355 // backslash in it. 356 //if (userName.indexOf('\\') < 0) { 357 // userName = "\\" + userName; 358 //} 359 mUsernameView.setText(username); 360 } 361 String password = recvAuth.mPassword; 362 if (password != null) { 363 mPasswordView.setText(password); 364 // Since username is uneditable, focus on the next editable field 365 if (mSettingsMode) { 366 mPasswordView.requestFocus(); 367 } 368 } 369 370 if (mServiceInfo.offerPrefix) { 371 String prefix = recvAuth.mDomain; 372 if (prefix != null && prefix.length() > 0) { 373 mImapPathPrefixView.setText(prefix.substring(1)); 374 } 375 } 376 377 // The delete policy is set for all legacy accounts. For POP3 accounts, the user sets 378 // the policy explicitly. For IMAP accounts, the policy is set when the Account object 379 // is created. @see AccountSetupBasics#populateSetupData 380 mLoadedDeletePolicy = account.getDeletePolicy(); 381 SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy); 382 383 int flags = recvAuth.mFlags; 384 flags &= ~HostAuth.FLAG_AUTHENTICATE; 385 if (mServiceInfo.defaultSsl) { 386 flags |= HostAuth.FLAG_SSL; 387 } 388 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags); 389 390 String hostname = recvAuth.mAddress; 391 if (hostname != null) { 392 mServerView.setText(hostname); 393 } 394 395 int port = recvAuth.mPort; 396 if (port != HostAuth.PORT_UNKNOWN) { 397 mPortView.setText(Integer.toString(port)); 398 } else { 399 updatePortFromSecurityType(); 400 } 401 402 mLoadedRecvAuth = recvAuth; 403 mLoaded = true; 404 validateFields(); 405 } 406 407 /** 408 * Check the values in the fields and decide if it makes sense to enable the "next" button 409 */ 410 private void validateFields() { 411 if (!mLoaded) return; 412 final boolean enabled = !TextUtils.isEmpty(mUsernameView.getText()) 413 && !TextUtils.isEmpty(mPasswordView.getText()) 414 && Utility.isServerNameValid(mServerView) 415 && Utility.isPortFieldValid(mPortView); 416 enableNextButton(enabled); 417 418 mCacheLoginCredential = mUsernameView.getText().toString().trim(); 419 420 // Warn (but don't prevent) if password has leading/trailing spaces 421 AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView); 422 } 423 424 private int getPortFromSecurityType(boolean useSsl) { 425 EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext, 426 SetupData.getAccount().mHostAuthRecv.mProtocol); 427 return useSsl ? info.portSsl : info.port; 428 } 429 430 private boolean getSslSelected() { 431 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 432 return ((securityType & HostAuth.FLAG_SSL) != 0); 433 } 434 435 public void onUseSslChanged(boolean useSsl) { 436 if (mServiceInfo.offerCerts) { 437 int mode = useSsl ? View.VISIBLE : View.GONE; 438 mClientCertificateSelector.setVisibility(mode); 439 String deviceId = ""; 440 try { 441 deviceId = Device.getDeviceId(mContext); 442 } catch (IOException e) { 443 // Not required 444 } 445 ((TextView) UiUtilities.getView(getView(), R.id.device_id)).setText(deviceId); 446 447 mDeviceIdSectionView.setVisibility(mode); 448 //UiUtilities.setVisibilitySafe(getView(), R.id.client_certificate_divider, mode); 449 } 450 } 451 452 private void updatePortFromSecurityType() { 453 boolean sslSelected = getSslSelected(); 454 int port = getPortFromSecurityType(sslSelected); 455 mPortView.setText(Integer.toString(port)); 456 onUseSslChanged(sslSelected); 457 } 458 459 /** 460 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 461 * Note, we update account here (as well as the account.mHostAuthRecv) because we edit 462 * account's delete policy here. 463 * Blocking - do not call from UI Thread. 464 */ 465 @Override 466 public void saveSettingsAfterEdit() { 467 Account account = SetupData.getAccount(); 468 account.update(mContext, account.toContentValues()); 469 account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues()); 470 // Update the backup (side copy) of the accounts 471 AccountBackupRestore.backup(mContext); 472 } 473 474 /** 475 * Entry point from Activity after entering new settings and verifying them. For setup mode. 476 */ 477 @Override 478 public void saveSettingsAfterSetup() { 479 Account account = SetupData.getAccount(); 480 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 481 HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext); 482 483 // Set the username and password for the outgoing settings to the username and 484 // password the user just set for incoming. Use the verified host address to try and 485 // pick a smarter outgoing address. 486 String hostName = 487 AccountSettingsUtils.inferServerName(mContext, recvAuth.mAddress, null, "smtp"); 488 sendAuth.setLogin(recvAuth.mLogin, recvAuth.mPassword); 489 sendAuth.setConnection(sendAuth.mProtocol, hostName, sendAuth.mPort, sendAuth.mFlags); 490 } 491 492 /** 493 * Entry point from Activity, when "next" button is clicked 494 */ 495 @Override 496 public void onNext() { 497 Account account = SetupData.getAccount(); 498 499 // Make sure delete policy is an valid option before using it; otherwise, the results are 500 // indeterminate, I suspect... 501 if (mDeletePolicyView.getVisibility() == View.VISIBLE) { 502 account.setDeletePolicy( 503 (Integer) ((SpinnerOption) mDeletePolicyView.getSelectedItem()).value); 504 } 505 506 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 507 String userName = mUsernameView.getText().toString().trim(); 508 String userPassword = mPasswordView.getText().toString(); 509 recvAuth.setLogin(userName, userPassword); 510 511 String serverAddress = mServerView.getText().toString().trim(); 512 int serverPort; 513 try { 514 serverPort = Integer.parseInt(mPortView.getText().toString().trim()); 515 } catch (NumberFormatException e) { 516 serverPort = getPortFromSecurityType(getSslSelected()); 517 LogUtils.d(Logging.LOG_TAG, "Non-integer server port; using '" + serverPort + "'"); 518 } 519 int securityType = (Integer) ((SpinnerOption) mSecurityTypeView.getSelectedItem()).value; 520 recvAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType); 521 if (mServiceInfo.offerPrefix) { 522 String prefix = mImapPathPrefixView.getText().toString().trim(); 523 recvAuth.mDomain = TextUtils.isEmpty(prefix) ? null : ("/" + prefix); 524 } else { 525 recvAuth.mDomain = null; 526 } 527 528 // Check for a duplicate account (requires async DB work) and if OK, 529 // proceed with check 530 startDuplicateTaskCheck( 531 account.mId, serverAddress, mCacheLoginCredential, SetupData.CHECK_INCOMING); 532 } 533 534 @Override 535 public boolean haveSettingsChanged() { 536 boolean deletePolicyChanged = false; 537 538 // Only verify the delete policy if the control is visible (i.e. is a pop3 account) 539 if (mDeletePolicyView.getVisibility() == View.VISIBLE) { 540 int newDeletePolicy = 541 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value; 542 deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy; 543 } 544 545 return deletePolicyChanged || super.haveSettingsChanged(); 546 } 547 548 /** 549 * Implements AccountCheckSettingsFragment.Callbacks 550 */ 551 @Override 552 public void onAutoDiscoverComplete(int result) { 553 AccountSetupIncoming activity = (AccountSetupIncoming) getActivity(); 554 activity.onAutoDiscoverComplete(result); 555 } 556 557 @Override 558 public void onCertificateRequested() { 559 Intent intent = new Intent(CertificateRequestor.ACTION_REQUEST_CERT); 560 intent.setData(Uri.parse("eas://com.android.emailcommon/certrequest")); 561 startActivityForResult(intent, CERTIFICATE_REQUEST); 562 } 563 564 @Override 565 public void onActivityResult(int requestCode, int resultCode, Intent data) { 566 if (requestCode == CERTIFICATE_REQUEST && resultCode == Activity.RESULT_OK) { 567 String certAlias = data.getStringExtra(CertificateRequestor.RESULT_ALIAS); 568 if (certAlias != null) { 569 mClientCertificateSelector.setCertificate(certAlias); 570 } 571 } 572 } 573} 574