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