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