AccountSetupIncomingFragment.java revision 31d9acbf0623872f9d4a2b3210b5970854b654c7
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.emailcommon.Logging; 23import com.android.emailcommon.provider.EmailContent; 24import com.android.emailcommon.provider.EmailContent.Account; 25import com.android.emailcommon.utility.Utility; 26 27import android.app.Activity; 28import android.content.Context; 29import android.os.Bundle; 30import android.text.Editable; 31import android.text.TextWatcher; 32import android.text.method.DigitsKeyListener; 33import android.util.Log; 34import android.view.LayoutInflater; 35import android.view.View; 36import android.view.ViewGroup; 37import android.widget.AdapterView; 38import android.widget.ArrayAdapter; 39import android.widget.EditText; 40import android.widget.Spinner; 41import android.widget.TextView; 42 43import java.net.URI; 44import java.net.URISyntaxException; 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 */ 52public 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 POP_PORTS[] = { 58 110, 995, 995, 110, 110 59 }; 60 private static final String POP_SCHEMES[] = { 61 "pop3", "pop3+ssl+", "pop3+ssl+trustallcerts", "pop3+tls+", "pop3+tls+trustallcerts" 62 }; 63 private static final int IMAP_PORTS[] = { 64 143, 993, 993, 143, 143 65 }; 66 private static final String IMAP_SCHEMES[] = { 67 "imap", "imap+ssl+", "imap+ssl+trustallcerts", "imap+tls+", "imap+tls+trustallcerts" 68 }; 69 70 private int mAccountPorts[]; 71 private String mAccountSchemes[]; 72 private EditText mUsernameView; 73 private EditText mPasswordView; 74 private TextView mServerLabelView; 75 private EditText mServerView; 76 private EditText mPortView; 77 private Spinner mSecurityTypeView; 78 private TextView mDeletePolicyLabelView; 79 private Spinner mDeletePolicyView; 80 private View mImapPathPrefixSectionView; 81 private EditText mImapPathPrefixView; 82 // Delete policy as loaded from the device 83 private int mLoadedDeletePolicy; 84 85 // Support for lifecycle 86 private boolean mStarted; 87 private boolean mConfigured; 88 private boolean mLoaded; 89 private String mCacheLoginCredential; 90 91 /** 92 * Called to do initial creation of a fragment. This is called after 93 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 94 */ 95 @Override 96 public void onCreate(Bundle savedInstanceState) { 97 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 98 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate"); 99 } 100 super.onCreate(savedInstanceState); 101 102 if (savedInstanceState != null) { 103 mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL); 104 mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false); 105 } 106 } 107 108 @Override 109 public View onCreateView(LayoutInflater inflater, ViewGroup container, 110 Bundle savedInstanceState) { 111 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 112 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView"); 113 } 114 int layoutId = mSettingsMode 115 ? R.layout.account_settings_incoming_fragment 116 : R.layout.account_setup_incoming_fragment; 117 118 View view = inflater.inflate(layoutId, container, false); 119 Context context = getActivity(); 120 121 mUsernameView = (EditText) view.findViewById(R.id.account_username); 122 mPasswordView = (EditText) view.findViewById(R.id.account_password); 123 mServerLabelView = (TextView) view.findViewById(R.id.account_server_label); 124 mServerView = (EditText) view.findViewById(R.id.account_server); 125 mPortView = (EditText) view.findViewById(R.id.account_port); 126 mSecurityTypeView = (Spinner) view.findViewById(R.id.account_security_type); 127 mDeletePolicyLabelView = (TextView) view.findViewById(R.id.account_delete_policy_label); 128 mDeletePolicyView = (Spinner) view.findViewById(R.id.account_delete_policy); 129 mImapPathPrefixSectionView = view.findViewById(R.id.imap_path_prefix_section); 130 mImapPathPrefixView = (EditText) view.findViewById(R.id.imap_path_prefix); 131 132 // Set up spinners 133 SpinnerOption securityTypes[] = { 134 new SpinnerOption(0, 135 context.getString(R.string.account_setup_incoming_security_none_label)), 136 new SpinnerOption(1, 137 context.getString(R.string.account_setup_incoming_security_ssl_label)), 138 new SpinnerOption(2, 139 context.getString( 140 R.string.account_setup_incoming_security_ssl_trust_certificates_label)), 141 new SpinnerOption(3, 142 context.getString(R.string.account_setup_incoming_security_tls_label)), 143 new SpinnerOption(4, 144 context.getString( 145 R.string.account_setup_incoming_security_tls_trust_certificates_label)), 146 }; 147 148 SpinnerOption deletePolicies[] = { 149 new SpinnerOption(Account.DELETE_POLICY_NEVER, 150 context.getString(R.string.account_setup_incoming_delete_policy_never_label)), 151 new SpinnerOption(Account.DELETE_POLICY_ON_DELETE, 152 context.getString(R.string.account_setup_incoming_delete_policy_delete_label)), 153 }; 154 155 ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context, 156 android.R.layout.simple_spinner_item, securityTypes); 157 securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 158 mSecurityTypeView.setAdapter(securityTypesAdapter); 159 160 ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(context, 161 android.R.layout.simple_spinner_item, deletePolicies); 162 deletePoliciesAdapter.setDropDownViewResource( 163 android.R.layout.simple_spinner_dropdown_item); 164 mDeletePolicyView.setAdapter(deletePoliciesAdapter); 165 166 // Updates the port when the user changes the security type. This allows 167 // us to show a reasonable default which the user can change. 168 mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 169 public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { 170 updatePortFromSecurityType(); 171 } 172 173 public void onNothingSelected(AdapterView<?> arg0) { } 174 }); 175 176 // After any text edits, call validateFields() which enables or disables the Next button 177 TextWatcher validationTextWatcher = new TextWatcher() { 178 public void afterTextChanged(Editable s) { 179 validateFields(); 180 } 181 182 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 183 public void onTextChanged(CharSequence s, int start, int before, int count) { } 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 200 public void onActivityCreated(Bundle savedInstanceState) { 201 if (Email.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 211 public void onStart() { 212 if (Email.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 225 public void onResume() { 226 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 227 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume"); 228 } 229 super.onResume(); 230 validateFields(); 231 } 232 233 @Override 234 public void onPause() { 235 if (Email.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 245 public void onStop() { 246 if (Email.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 257 public void onDestroy() { 258 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 259 Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy"); 260 } 261 super.onDestroy(); 262 } 263 264 @Override 265 public void onSaveInstanceState(Bundle outState) { 266 if (Email.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 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 */ 290 private void configureEditor() { 291 if (mConfigured) return; 292 Account account = SetupData.getAccount(); 293 String protocol = account.mHostAuthRecv.mProtocol; 294 if (protocol.startsWith("pop3")) { 295 mServerLabelView.setText(R.string.account_setup_incoming_pop_server_label); 296 mAccountPorts = POP_PORTS; 297 mAccountSchemes = POP_SCHEMES; 298 mImapPathPrefixSectionView.setVisibility(View.GONE); 299 } else if (protocol.startsWith("imap")) { 300 mServerLabelView.setText(R.string.account_setup_incoming_imap_server_label); 301 mAccountPorts = IMAP_PORTS; 302 mAccountSchemes = IMAP_SCHEMES; 303 mDeletePolicyLabelView.setVisibility(View.GONE); 304 mDeletePolicyView.setVisibility(View.GONE); 305 } else { 306 throw new Error("Unknown account type: " + account); 307 } 308 mConfigured = true; 309 } 310 311 /** 312 * Load the current settings into the UI 313 */ 314 private void loadSettings() { 315 if (mLoaded) return; 316 try { 317 // TODO this should be accessed directly via the HostAuth structure 318 EmailContent.Account account = SetupData.getAccount(); 319 URI uri = new URI(account.getStoreUri(mContext)); 320 String username = null; 321 String password = null; 322 if (uri.getUserInfo() != null) { 323 String[] userInfoParts = uri.getUserInfo().split(":", 2); 324 username = userInfoParts[0]; 325 if (userInfoParts.length > 1) { 326 password = userInfoParts[1]; 327 } 328 } 329 330 if (username != null) { 331 mUsernameView.setText(username); 332 } 333 334 if (password != null) { 335 mPasswordView.setText(password); 336 } 337 338 if (uri.getScheme().startsWith("pop3")) { 339 mLoadedDeletePolicy = account.getDeletePolicy(); 340 SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy); 341 } else if (uri.getScheme().startsWith("imap")) { 342 if (uri.getPath() != null && uri.getPath().length() > 0) { 343 mImapPathPrefixView.setText(uri.getPath().substring(1)); 344 } 345 } else { 346 throw new Error("Unknown account type: " + account.getStoreUri(mContext)); 347 } 348 349 for (int i = 0; i < mAccountSchemes.length; i++) { 350 if (mAccountSchemes[i].equals(uri.getScheme())) { 351 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i); 352 } 353 } 354 355 if (uri.getHost() != null) { 356 mServerView.setText(uri.getHost()); 357 } 358 359 if (uri.getPort() != -1) { 360 mPortView.setText(Integer.toString(uri.getPort())); 361 } else { 362 updatePortFromSecurityType(); 363 } 364 } catch (URISyntaxException use) { 365 /* 366 * We should always be able to parse our own settings. 367 */ 368 throw new Error(use); 369 } 370 371 try { 372 mLoadedUri = getUri(); 373 } catch (URISyntaxException ignore) { 374 // ignore; should not happen 375 } 376 377 mLoaded = true; 378 validateFields(); 379 } 380 381 /** 382 * Check the values in the fields and decide if it makes sense to enable the "next" button 383 */ 384 private void validateFields() { 385 if (!mConfigured || !mLoaded) return; 386 boolean enabled = Utility.isTextViewNotEmpty(mUsernameView) 387 && Utility.isTextViewNotEmpty(mPasswordView) 388 && Utility.isTextViewNotEmpty(mServerView) 389 && Utility.isPortFieldValid(mPortView); 390 if (enabled) { 391 try { 392 URI uri = getUri(); 393 } catch (URISyntaxException use) { 394 enabled = false; 395 } 396 } 397 enableNextButton(enabled); 398 399 // Warn (but don't prevent) if password has leading/trailing spaces 400 AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView); 401 } 402 403 private void updatePortFromSecurityType() { 404 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 405 mPortView.setText(Integer.toString(mAccountPorts[securityType])); 406 } 407 408 /** 409 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 410 * Note, we update account here (as well as the account.mHostAuthRecv) because we edit 411 * account's delete policy here. 412 * Blocking - do not call from UI Thread. 413 */ 414 @Override 415 public void saveSettingsAfterEdit() { 416 Account account = SetupData.getAccount(); 417 account.update(mContext, account.toContentValues()); 418 account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues()); 419 // Update the backup (side copy) of the accounts 420 AccountBackupRestore.backupAccounts(mContext); 421 } 422 423 /** 424 * Entry point from Activity after entering new settings and verifying them. For setup mode. 425 */ 426 @Override 427 public void saveSettingsAfterSetup() { 428 EmailContent.Account account = SetupData.getAccount(); 429 430 // Set the username and password for the outgoing settings to the username and 431 // password the user just set for incoming. Use the verified host address to try and 432 // pick a smarter outgoing address. 433 try { 434 String hostName = 435 AccountSettingsUtils.inferServerName(account.mHostAuthRecv.mAddress, null, "smtp"); 436 URI oldUri = new URI(account.getSenderUri(mContext)); 437 URI uri = new URI( 438 oldUri.getScheme(), 439 mUsernameView.getText().toString().trim() + ":" 440 + mPasswordView.getText().toString(), 441 hostName, 442 oldUri.getPort(), 443 null, 444 null, 445 null); 446 account.setSenderUri(mContext, uri.toString()); 447 } catch (URISyntaxException use) { 448 // If we can't set up the URL we just continue. It's only for convenience. 449 } 450 } 451 452 /** 453 * Attempt to create a URI from the fields provided. Throws URISyntaxException if there's 454 * a problem with the user input. 455 * @return a URI built from the account setup fields 456 */ 457 @Override 458 protected URI getUri() throws URISyntaxException { 459 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 460 String path = null; 461 if (mAccountSchemes[securityType].startsWith("imap")) { 462 path = "/" + mImapPathPrefixView.getText().toString().trim(); 463 } 464 String userName = mUsernameView.getText().toString().trim(); 465 mCacheLoginCredential = userName; 466 URI uri = new URI( 467 mAccountSchemes[securityType], 468 userName + ":" + mPasswordView.getText(), 469 mServerView.getText().toString().trim(), 470 Integer.parseInt(mPortView.getText().toString().trim()), 471 path, // path 472 null, // query 473 null); 474 475 return uri; 476 } 477 478 /** 479 * Entry point from Activity, when "next" button is clicked 480 */ 481 @Override 482 public void onNext() { 483 EmailContent.Account setupAccount = SetupData.getAccount(); 484 485 setupAccount.setDeletePolicy( 486 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value); 487 488 try { 489 URI uri = getUri(); 490 setupAccount.setStoreUri(mContext, uri.toString()); 491 492 // Check for a duplicate account (requires async DB work) and if OK, proceed with check 493 startDuplicateTaskCheck(setupAccount.mId, uri.getHost(), mCacheLoginCredential, 494 SetupData.CHECK_INCOMING); 495 } catch (URISyntaxException use) { 496 /* 497 * It's unrecoverable if we cannot create a URI from components that 498 * we validated to be safe. 499 */ 500 throw new Error(use); 501 } 502 } 503 504 @Override 505 public boolean haveSettingsChanged() { 506 boolean deletePolicyChanged = false; 507 508 // Only verify the delete policy if the control is visible (i.e. is a pop3 account) 509 if (mDeletePolicyView.getVisibility() == View.VISIBLE) { 510 int newDeletePolicy = 511 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value; 512 deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy; 513 } 514 515 return deletePolicyChanged || super.haveSettingsChanged(); 516 } 517} 518