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