AccountSetupIncomingFragment.java revision 12b82d9374947c9268217f45befe8a74bd9b60d7
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 com.android.email.Email; 20import com.android.email.R; 21import com.android.email.activity.UiUtilities; 22import com.android.email.mail.Store; 23import com.android.email.provider.AccountBackupRestore; 24import com.android.emailcommon.Logging; 25import com.android.emailcommon.provider.EmailContent.Account; 26import com.android.emailcommon.provider.HostAuth; 27import com.android.emailcommon.utility.Utility; 28 29import android.app.Activity; 30import android.content.Context; 31import android.os.Bundle; 32import android.text.Editable; 33import android.text.TextWatcher; 34import android.text.method.DigitsKeyListener; 35import android.util.Log; 36import android.view.LayoutInflater; 37import android.view.View; 38import android.view.ViewGroup; 39import android.widget.AdapterView; 40import android.widget.ArrayAdapter; 41import android.widget.EditText; 42import android.widget.Spinner; 43import android.widget.TextView; 44 45import java.net.URI; 46import java.net.URISyntaxException; 47 48/** 49 * Provides UI for IMAP/POP account settings. 50 * 51 * This fragment is used by AccountSetupIncoming (for creating accounts) and by AccountSettingsXL 52 * (for editing existing accounts). 53 */ 54public class AccountSetupIncomingFragment extends AccountServerBaseFragment { 55 56 private final static String STATE_KEY_CREDENTIAL = "AccountSetupIncomingFragment.credential"; 57 private final static String STATE_KEY_LOADED = "AccountSetupIncomingFragment.loaded"; 58 59 private static final int POP3_PORT_NORMAL = 110; 60 private static final int POP3_PORT_SSL = 995; 61 62 private static final int IMAP_PORT_NORMAL = 143; 63 private static final int IMAP_PORT_SSL = 993; 64 65 private EditText mUsernameView; 66 private EditText mPasswordView; 67 private TextView mServerLabelView; 68 private EditText mServerView; 69 private EditText mPortView; 70 private Spinner mSecurityTypeView; 71 private TextView mDeletePolicyLabelView; 72 private Spinner mDeletePolicyView; 73 private View mImapPathPrefixSectionView; 74 private EditText mImapPathPrefixView; 75 // Delete policy as loaded from the device 76 private int mLoadedDeletePolicy; 77 78 // Support for lifecycle 79 private boolean mStarted; 80 private boolean mConfigured; 81 private boolean mLoaded; 82 private String mCacheLoginCredential; 83 84 /** 85 * Called to do initial creation of a fragment. This is called after 86 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 87 */ 88 @Override 89 public void onCreate(Bundle savedInstanceState) { 90 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 91 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate"); 92 } 93 super.onCreate(savedInstanceState); 94 95 if (savedInstanceState != null) { 96 mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL); 97 mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false); 98 } 99 } 100 101 @Override 102 public View onCreateView(LayoutInflater inflater, ViewGroup container, 103 Bundle savedInstanceState) { 104 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 105 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView"); 106 } 107 int layoutId = mSettingsMode 108 ? R.layout.account_settings_incoming_fragment 109 : R.layout.account_setup_incoming_fragment; 110 111 View view = inflater.inflate(layoutId, container, false); 112 Context context = getActivity(); 113 114 mUsernameView = (EditText) UiUtilities.getView(view, R.id.account_username); 115 mPasswordView = (EditText) UiUtilities.getView(view, R.id.account_password); 116 mServerLabelView = (TextView) UiUtilities.getView(view, R.id.account_server_label); 117 mServerView = (EditText) UiUtilities.getView(view, R.id.account_server); 118 mPortView = (EditText) UiUtilities.getView(view, R.id.account_port); 119 mSecurityTypeView = (Spinner) UiUtilities.getView(view, R.id.account_security_type); 120 mDeletePolicyLabelView = (TextView) UiUtilities.getView(view, 121 R.id.account_delete_policy_label); 122 mDeletePolicyView = (Spinner) UiUtilities.getView(view, R.id.account_delete_policy); 123 mImapPathPrefixSectionView = UiUtilities.getView(view, R.id.imap_path_prefix_section); 124 mImapPathPrefixView = (EditText) UiUtilities.getView(view, R.id.imap_path_prefix); 125 126 // Set up spinners 127 SpinnerOption securityTypes[] = { 128 new SpinnerOption(HostAuth.FLAG_NONE, context.getString( 129 R.string.account_setup_incoming_security_none_label)), 130 new SpinnerOption(HostAuth.FLAG_SSL, context.getString( 131 R.string.account_setup_incoming_security_ssl_label)), 132 new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString( 133 R.string.account_setup_incoming_security_ssl_trust_certificates_label)), 134 new SpinnerOption(HostAuth.FLAG_TLS, context.getString( 135 R.string.account_setup_incoming_security_tls_label)), 136 new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, context.getString( 137 R.string.account_setup_incoming_security_tls_trust_certificates_label)), 138 }; 139 140 SpinnerOption deletePolicies[] = { 141 new SpinnerOption(Account.DELETE_POLICY_NEVER, 142 context.getString(R.string.account_setup_incoming_delete_policy_never_label)), 143 new SpinnerOption(Account.DELETE_POLICY_ON_DELETE, 144 context.getString(R.string.account_setup_incoming_delete_policy_delete_label)), 145 }; 146 147 ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context, 148 android.R.layout.simple_spinner_item, securityTypes); 149 securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 150 mSecurityTypeView.setAdapter(securityTypesAdapter); 151 152 ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(context, 153 android.R.layout.simple_spinner_item, deletePolicies); 154 deletePoliciesAdapter.setDropDownViewResource( 155 android.R.layout.simple_spinner_dropdown_item); 156 mDeletePolicyView.setAdapter(deletePoliciesAdapter); 157 158 // Updates the port when the user changes the security type. This allows 159 // us to show a reasonable default which the user can change. 160 mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 161 public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { 162 updatePortFromSecurityType(); 163 } 164 165 public void onNothingSelected(AdapterView<?> arg0) { } 166 }); 167 168 // After any text edits, call validateFields() which enables or disables the Next button 169 TextWatcher validationTextWatcher = new TextWatcher() { 170 public void afterTextChanged(Editable s) { 171 validateFields(); 172 } 173 174 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 175 public void onTextChanged(CharSequence s, int start, int before, int count) { } 176 }; 177 // We're editing an existing account; don't allow modification of the user name 178 if (mSettingsMode) { 179 makeTextViewUneditable(mUsernameView, 180 getString(R.string.account_setup_username_uneditable_error)); 181 } 182 mUsernameView.addTextChangedListener(validationTextWatcher); 183 mPasswordView.addTextChangedListener(validationTextWatcher); 184 mServerView.addTextChangedListener(validationTextWatcher); 185 mPortView.addTextChangedListener(validationTextWatcher); 186 187 // Only allow digits in the port field. 188 mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); 189 190 // Additional setup only used while in "settings" mode 191 onCreateViewSettingsMode(view); 192 193 return view; 194 } 195 196 @Override 197 public void onActivityCreated(Bundle savedInstanceState) { 198 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 199 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated"); 200 } 201 super.onActivityCreated(savedInstanceState); 202 } 203 204 /** 205 * Called when the Fragment is visible to the user. 206 */ 207 @Override 208 public void onStart() { 209 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 210 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart"); 211 } 212 super.onStart(); 213 mStarted = true; 214 configureEditor(); 215 loadSettings(); 216 } 217 218 /** 219 * Called when the fragment is visible to the user and actively running. 220 */ 221 @Override 222 public void onResume() { 223 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 224 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume"); 225 } 226 super.onResume(); 227 validateFields(); 228 } 229 230 @Override 231 public void onPause() { 232 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 233 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause"); 234 } 235 super.onPause(); 236 } 237 238 /** 239 * Called when the Fragment is no longer started. 240 */ 241 @Override 242 public void onStop() { 243 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 244 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop"); 245 } 246 super.onStop(); 247 mStarted = false; 248 } 249 250 /** 251 * Called when the fragment is no longer in use. 252 */ 253 @Override 254 public void onDestroy() { 255 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 256 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy"); 257 } 258 super.onDestroy(); 259 } 260 261 @Override 262 public void onSaveInstanceState(Bundle outState) { 263 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 264 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState"); 265 } 266 super.onSaveInstanceState(outState); 267 268 outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential); 269 outState.putBoolean(STATE_KEY_LOADED, mLoaded); 270 } 271 272 /** 273 * Activity provides callbacks here. This also triggers loading and setting up the UX 274 */ 275 @Override 276 public void setCallback(Callback callback) { 277 super.setCallback(callback); 278 if (mStarted) { 279 configureEditor(); 280 loadSettings(); 281 } 282 } 283 284 /** 285 * Configure the editor for the account type 286 */ 287 private void configureEditor() { 288 if (mConfigured) return; 289 Account account = SetupData.getAccount(); 290 mBaseScheme = account.mHostAuthRecv.mProtocol; 291 if (Store.STORE_SCHEME_POP3.equals(mBaseScheme)) { 292 mServerLabelView.setText(R.string.account_setup_incoming_pop_server_label); 293 mImapPathPrefixSectionView.setVisibility(View.GONE); 294 } else if (Store.STORE_SCHEME_IMAP.equals(mBaseScheme)) { 295 mServerLabelView.setText(R.string.account_setup_incoming_imap_server_label); 296 mDeletePolicyLabelView.setVisibility(View.GONE); 297 mDeletePolicyView.setVisibility(View.GONE); 298 } else { 299 throw new Error("Unknown account type: " + account); 300 } 301 mConfigured = true; 302 } 303 304 /** 305 * Load the current settings into the UI 306 */ 307 private void loadSettings() { 308 if (mLoaded) return; 309 310 Account account = SetupData.getAccount(); 311 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 312 313 String username = recvAuth.mLogin; 314 if (username != null) { 315 mUsernameView.setText(username); 316 } 317 String password = recvAuth.mPassword; 318 if (password != null) { 319 mPasswordView.setText(password); 320 } 321 322 if (Store.STORE_SCHEME_IMAP.equals(recvAuth.mProtocol)) { 323 String prefix = recvAuth.mDomain; 324 if (prefix != null && prefix.length() > 0) { 325 mImapPathPrefixView.setText(prefix.substring(1)); 326 } 327 } else if (!Store.STORE_SCHEME_POP3.equals(recvAuth.mProtocol)) { 328 // Account must either be IMAP or POP3 329 throw new Error("Unknown account type: " + account.getStoreUri(mContext)); 330 } 331 332 // The delete policy is set for all legacy accounts. For POP3 accounts, the user sets 333 // the policy explicitly. For IMAP accounts, the policy is set when the Account object 334 // is created. @see AccountSetupBasics#populateSetupData 335 mLoadedDeletePolicy = account.getDeletePolicy(); 336 SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy); 337 338 int flags = recvAuth.mFlags; 339 flags &= ~HostAuth.FLAG_AUTHENTICATE; 340 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags); 341 342 String hostname = recvAuth.mAddress; 343 if (hostname != null) { 344 mServerView.setText(hostname); 345 } 346 347 int port = recvAuth.mPort; 348 if (port != HostAuth.PORT_UNKNOWN) { 349 mPortView.setText(Integer.toString(port)); 350 } else { 351 updatePortFromSecurityType(); 352 } 353 354 mLoadedRecvAuth = recvAuth; 355 mLoaded = true; 356 validateFields(); 357 } 358 359 /** 360 * Check the values in the fields and decide if it makes sense to enable the "next" button 361 */ 362 private void validateFields() { 363 if (!mConfigured || !mLoaded) return; 364 boolean enabled = Utility.isTextViewNotEmpty(mUsernameView) 365 && Utility.isTextViewNotEmpty(mPasswordView) 366 && Utility.isTextViewNotEmpty(mServerView) 367 && Utility.isPortFieldValid(mPortView); 368 if (enabled) { 369 try { 370 URI uri = getUri(); 371 } catch (URISyntaxException use) { 372 enabled = false; 373 } 374 } 375 enableNextButton(enabled); 376 377 // Warn (but don't prevent) if password has leading/trailing spaces 378 AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView); 379 } 380 381 private int getPortFromSecurityType() { 382 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 383 boolean useSsl = ((securityType & HostAuth.FLAG_SSL) != 0); 384 int port = useSsl ? IMAP_PORT_SSL : IMAP_PORT_NORMAL; // default to IMAP 385 if (Store.STORE_SCHEME_POP3.equals(mBaseScheme)) { 386 port = useSsl ? POP3_PORT_SSL : POP3_PORT_NORMAL; 387 } 388 return port; 389 } 390 391 private void updatePortFromSecurityType() { 392 int port = getPortFromSecurityType(); 393 mPortView.setText(Integer.toString(port)); 394 } 395 396 /** 397 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 398 * Note, we update account here (as well as the account.mHostAuthRecv) because we edit 399 * account's delete policy here. 400 * Blocking - do not call from UI Thread. 401 */ 402 @Override 403 public void saveSettingsAfterEdit() { 404 Account account = SetupData.getAccount(); 405 account.update(mContext, account.toContentValues()); 406 account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues()); 407 // Update the backup (side copy) of the accounts 408 AccountBackupRestore.backup(mContext); 409 } 410 411 /** 412 * Entry point from Activity after entering new settings and verifying them. For setup mode. 413 */ 414 @Override 415 public void saveSettingsAfterSetup() { 416 Account account = SetupData.getAccount(); 417 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 418 HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext); 419 420 // Set the username and password for the outgoing settings to the username and 421 // password the user just set for incoming. Use the verified host address to try and 422 // pick a smarter outgoing address. 423 String hostName = AccountSettingsUtils.inferServerName(recvAuth.mAddress, null, "smtp"); 424 sendAuth.setLogin(recvAuth.mLogin, recvAuth.mPassword); 425 sendAuth.setConnection(sendAuth.mProtocol, hostName, sendAuth.mPort, sendAuth.mFlags); 426 } 427 428 /** 429 * Attempt to create a URI from the fields provided. Throws URISyntaxException if there's 430 * a problem with the user input. 431 * @return a URI built from the account setup fields 432 */ 433 @Override 434 protected URI getUri() throws URISyntaxException { 435 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 436 String path = null; 437 if (Store.STORE_SCHEME_IMAP.equals(mBaseScheme)) { 438 path = "/" + mImapPathPrefixView.getText().toString().trim(); 439 } 440 String userName = mUsernameView.getText().toString().trim(); 441 mCacheLoginCredential = userName; 442 String userInfo = userName + ":" + mPasswordView.getText(); 443 String host = mServerView.getText().toString().trim(); 444 int port = Integer.parseInt(mPortView.getText().toString().trim()); 445 446 URI uri = new URI( 447 HostAuth.getSchemeString(mBaseScheme, securityType), 448 userInfo, 449 host, 450 port, 451 path, // path 452 null, // query 453 null); 454 return uri; 455 } 456 457 /** 458 * Entry point from Activity, when "next" button is clicked 459 */ 460 @Override 461 public void onNext() { 462 Account account = SetupData.getAccount(); 463 464 account.setDeletePolicy( 465 (Integer) ((SpinnerOption) mDeletePolicyView.getSelectedItem()).value); 466 467 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 468 String userName = mUsernameView.getText().toString().trim(); 469 String userPassword = mPasswordView.getText().toString(); 470 recvAuth.setLogin(userName, userPassword); 471 472 String serverAddress = mServerView.getText().toString().trim(); 473 int serverPort; 474 try { 475 serverPort = Integer.parseInt(mPortView.getText().toString().trim()); 476 } catch (NumberFormatException e) { 477 serverPort = getPortFromSecurityType(); 478 Log.d(Logging.LOG_TAG, "Non-integer server port; using '" + serverPort + "'"); 479 } 480 int securityType = (Integer) ((SpinnerOption) mSecurityTypeView.getSelectedItem()).value; 481 recvAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType); 482 recvAuth.mDomain = null; 483 484 // Check for a duplicate account (requires async DB work) and if OK, 485 // proceed with check 486 startDuplicateTaskCheck( 487 account.mId, serverAddress, mCacheLoginCredential, SetupData.CHECK_INCOMING); 488 } 489 490 @Override 491 public boolean haveSettingsChanged() { 492 boolean deletePolicyChanged = false; 493 494 // Only verify the delete policy if the control is visible (i.e. is a pop3 account) 495 if (mDeletePolicyView.getVisibility() == View.VISIBLE) { 496 int newDeletePolicy = 497 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value; 498 deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy; 499 } 500 501 return deletePolicyChanged || super.haveSettingsChanged(); 502 } 503} 504