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