AccountSetupIncomingFragment.java revision 040ddf60cfef4aaecf4bfe1f897fce3248d777a4
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 mUsernameView.addTextChangedListener(validationTextWatcher); 176 mPasswordView.addTextChangedListener(validationTextWatcher); 177 mServerView.addTextChangedListener(validationTextWatcher); 178 mPortView.addTextChangedListener(validationTextWatcher); 179 180 // Only allow digits in the port field. 181 mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); 182 183 // Additional setup only used while in "settings" mode 184 onCreateViewSettingsMode(view); 185 186 return view; 187 } 188 189 @Override 190 public void onActivityCreated(Bundle savedInstanceState) { 191 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 192 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated"); 193 } 194 super.onActivityCreated(savedInstanceState); 195 } 196 197 /** 198 * Called when the Fragment is visible to the user. 199 */ 200 @Override 201 public void onStart() { 202 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 203 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart"); 204 } 205 super.onStart(); 206 mStarted = true; 207 configureEditor(); 208 loadSettings(); 209 } 210 211 /** 212 * Called when the fragment is visible to the user and actively running. 213 */ 214 @Override 215 public void onResume() { 216 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 217 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume"); 218 } 219 super.onResume(); 220 validateFields(); 221 } 222 223 @Override 224 public void onPause() { 225 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 226 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause"); 227 } 228 super.onPause(); 229 } 230 231 /** 232 * Called when the Fragment is no longer started. 233 */ 234 @Override 235 public void onStop() { 236 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 237 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop"); 238 } 239 super.onStop(); 240 mStarted = false; 241 } 242 243 /** 244 * Called when the fragment is no longer in use. 245 */ 246 @Override 247 public void onDestroy() { 248 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 249 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy"); 250 } 251 super.onDestroy(); 252 } 253 254 @Override 255 public void onSaveInstanceState(Bundle outState) { 256 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 257 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState"); 258 } 259 super.onSaveInstanceState(outState); 260 261 outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential); 262 outState.putBoolean(STATE_KEY_LOADED, mLoaded); 263 } 264 265 /** 266 * Activity provides callbacks here. This also triggers loading and setting up the UX 267 */ 268 @Override 269 public void setCallback(Callback callback) { 270 super.setCallback(callback); 271 if (mStarted) { 272 configureEditor(); 273 loadSettings(); 274 } 275 } 276 277 /** 278 * Configure the editor for the account type 279 */ 280 private void configureEditor() { 281 if (mConfigured) return; 282 Account account = SetupData.getAccount(); 283 mBaseScheme = account.mHostAuthRecv.mProtocol; 284 if (Store.STORE_SCHEME_POP3.equals(mBaseScheme)) { 285 mServerLabelView.setText(R.string.account_setup_incoming_pop_server_label); 286 mImapPathPrefixSectionView.setVisibility(View.GONE); 287 } else if (Store.STORE_SCHEME_IMAP.equals(mBaseScheme)) { 288 mServerLabelView.setText(R.string.account_setup_incoming_imap_server_label); 289 mDeletePolicyLabelView.setVisibility(View.GONE); 290 mDeletePolicyView.setVisibility(View.GONE); 291 } else { 292 throw new Error("Unknown account type: " + account); 293 } 294 mConfigured = true; 295 } 296 297 /** 298 * Load the current settings into the UI 299 */ 300 private void loadSettings() { 301 if (mLoaded) return; 302 303 Account account = SetupData.getAccount(); 304 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 305 306 String username = recvAuth.mLogin; 307 if (username != null) { 308 mUsernameView.setText(username); 309 } 310 String password = recvAuth.mPassword; 311 if (password != null) { 312 mPasswordView.setText(password); 313 } 314 315 if (Store.STORE_SCHEME_POP3.equals(recvAuth.mProtocol)) { 316 mLoadedDeletePolicy = account.getDeletePolicy(); 317 SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy); 318 } else if (Store.STORE_SCHEME_IMAP.equals(recvAuth.mProtocol)) { 319 String prefix = recvAuth.mDomain; 320 if (prefix != null && prefix.length() > 0) { 321 mImapPathPrefixView.setText(prefix.substring(1)); 322 } 323 } else { 324 throw new Error("Unknown account type: " + account.getStoreUri(mContext)); 325 } 326 327 int flags = recvAuth.mFlags; 328 flags &= ~HostAuth.FLAG_AUTHENTICATE; 329 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags); 330 331 String hostname = recvAuth.mAddress; 332 if (hostname != null) { 333 mServerView.setText(hostname); 334 } 335 336 int port = recvAuth.mPort; 337 if (port != HostAuth.PORT_UNKNOWN) { 338 mPortView.setText(Integer.toString(port)); 339 } else { 340 updatePortFromSecurityType(); 341 } 342 343 // TODO See how to get rid of this. Maybe define an "equals()" for HostAuth? 344 // used to determine if these settings have changed 345 try { 346 mLoadedUri = getUri(); 347 } catch (URISyntaxException ignore) { 348 // ignore; should not happen 349 } 350 351 mLoaded = true; 352 validateFields(); 353 } 354 355 /** 356 * Check the values in the fields and decide if it makes sense to enable the "next" button 357 */ 358 private void validateFields() { 359 if (!mConfigured || !mLoaded) return; 360 boolean enabled = Utility.isTextViewNotEmpty(mUsernameView) 361 && Utility.isTextViewNotEmpty(mPasswordView) 362 && Utility.isTextViewNotEmpty(mServerView) 363 && Utility.isPortFieldValid(mPortView); 364 if (enabled) { 365 try { 366 URI uri = getUri(); 367 } catch (URISyntaxException use) { 368 enabled = false; 369 } 370 } 371 enableNextButton(enabled); 372 373 // Warn (but don't prevent) if password has leading/trailing spaces 374 AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView); 375 } 376 377 private int getPortFromSecurityType() { 378 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 379 boolean useSsl = ((securityType & HostAuth.FLAG_SSL) != 0); 380 int port = useSsl ? IMAP_PORT_SSL : IMAP_PORT_NORMAL; // default to IMAP 381 if (Store.STORE_SCHEME_POP3.equals(mBaseScheme)) { 382 port = useSsl ? POP3_PORT_SSL : POP3_PORT_NORMAL; 383 } 384 return port; 385 } 386 387 private void updatePortFromSecurityType() { 388 int port = getPortFromSecurityType(); 389 mPortView.setText(Integer.toString(port)); 390 } 391 392 /** 393 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 394 * Note, we update account here (as well as the account.mHostAuthRecv) because we edit 395 * account's delete policy here. 396 * Blocking - do not call from UI Thread. 397 */ 398 @Override 399 public void saveSettingsAfterEdit() { 400 Account account = SetupData.getAccount(); 401 account.update(mContext, account.toContentValues()); 402 account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues()); 403 // Update the backup (side copy) of the accounts 404 AccountBackupRestore.backupAccounts(mContext); 405 } 406 407 /** 408 * Entry point from Activity after entering new settings and verifying them. For setup mode. 409 */ 410 @Override 411 public void saveSettingsAfterSetup() { 412 Account account = SetupData.getAccount(); 413 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 414 HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext); 415 416 // Set the username and password for the outgoing settings to the username and 417 // password the user just set for incoming. Use the verified host address to try and 418 // pick a smarter outgoing address. 419 String hostName = AccountSettingsUtils.inferServerName(recvAuth.mAddress, null, "smtp"); 420 sendAuth.setLogin(recvAuth.mLogin, recvAuth.mPassword); 421 sendAuth.setConnection(sendAuth.mProtocol, hostName, sendAuth.mPort, sendAuth.mFlags); 422 } 423 424 /** 425 * Attempt to create a URI from the fields provided. Throws URISyntaxException if there's 426 * a problem with the user input. 427 * @return a URI built from the account setup fields 428 */ 429 @Override 430 protected URI getUri() throws URISyntaxException { 431 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 432 String path = null; 433 if (Store.STORE_SCHEME_IMAP.equals(mBaseScheme)) { 434 path = "/" + mImapPathPrefixView.getText().toString().trim(); 435 } 436 String userName = mUsernameView.getText().toString().trim(); 437 mCacheLoginCredential = userName; 438 String userInfo = userName + ":" + mPasswordView.getText(); 439 String host = mServerView.getText().toString().trim(); 440 int port = Integer.parseInt(mPortView.getText().toString().trim()); 441 442 URI uri = new URI( 443 HostAuth.getSchemeString(mBaseScheme, securityType), 444 userInfo, 445 host, 446 port, 447 path, // path 448 null, // query 449 null); 450 return uri; 451 } 452 453 /** 454 * Entry point from Activity, when "next" button is clicked 455 */ 456 @Override 457 public void onNext() { 458 Account account = SetupData.getAccount(); 459 460 account.setDeletePolicy( 461 (Integer) ((SpinnerOption) mDeletePolicyView.getSelectedItem()).value); 462 463 HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext); 464 String userName = mUsernameView.getText().toString().trim(); 465 String userPassword = mPasswordView.getText().toString(); 466 recvAuth.setLogin(userName, userPassword); 467 468 String serverAddress = mServerView.getText().toString().trim(); 469 int serverPort; 470 try { 471 serverPort = Integer.parseInt(mPortView.getText().toString().trim()); 472 } catch (NumberFormatException e) { 473 serverPort = getPortFromSecurityType(); 474 Log.d(Logging.LOG_TAG, "Non-integer server port; using '" + serverPort + "'"); 475 } 476 int securityType = (Integer) ((SpinnerOption) mSecurityTypeView.getSelectedItem()).value; 477 recvAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType); 478 recvAuth.mDomain = null; 479 480 // Check for a duplicate account (requires async DB work) and if OK, 481 // proceed with check 482 startDuplicateTaskCheck( 483 account.mId, serverAddress, mCacheLoginCredential, SetupData.CHECK_INCOMING); 484 } 485 486 @Override 487 public boolean haveSettingsChanged() { 488 boolean deletePolicyChanged = false; 489 490 // Only verify the delete policy if the control is visible (i.e. is a pop3 account) 491 if (mDeletePolicyView.getVisibility() == View.VISIBLE) { 492 int newDeletePolicy = 493 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value; 494 deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy; 495 } 496 497 return deletePolicyChanged || super.haveSettingsChanged(); 498 } 499} 500