AccountSetupIncomingFragment.java revision 1f3e117951e5ffe97a30bdd61c1cf99759f15406
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 82 // Support for lifecycle 83 private boolean mStarted; 84 private boolean mConfigured; 85 private boolean mLoaded; 86 private String mCacheLoginCredential; 87 88 /** 89 * Called to do initial creation of a fragment. This is called after 90 * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}. 91 */ 92 @Override 93 public void onCreate(Bundle savedInstanceState) { 94 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 95 Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onCreate"); 96 } 97 super.onCreate(savedInstanceState); 98 99 if (savedInstanceState != null) { 100 mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL); 101 mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false); 102 } 103 } 104 105 @Override 106 public View onCreateView(LayoutInflater inflater, ViewGroup container, 107 Bundle savedInstanceState) { 108 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 109 Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onCreateView"); 110 } 111 View view = inflater.inflate(R.layout.account_setup_incoming_fragment, container, false); 112 Context context = getActivity(); 113 114 mUsernameView = (EditText) view.findViewById(R.id.account_username); 115 mPasswordView = (EditText) view.findViewById(R.id.account_password); 116 mServerLabelView = (TextView) view.findViewById(R.id.account_server_label); 117 mServerView = (EditText) view.findViewById(R.id.account_server); 118 mPortView = (EditText) view.findViewById(R.id.account_port); 119 mSecurityTypeView = (Spinner) view.findViewById(R.id.account_security_type); 120 mDeletePolicyLabelView = (TextView) view.findViewById(R.id.account_delete_policy_label); 121 mDeletePolicyView = (Spinner) view.findViewById(R.id.account_delete_policy); 122 mImapPathPrefixSectionView = view.findViewById(R.id.imap_path_prefix_section); 123 mImapPathPrefixView = (EditText) view.findViewById(R.id.imap_path_prefix); 124 125 // Set up spinners 126 SpinnerOption securityTypes[] = { 127 new SpinnerOption(0, 128 context.getString(R.string.account_setup_incoming_security_none_label)), 129 new SpinnerOption(1, 130 context.getString(R.string.account_setup_incoming_security_ssl_label)), 131 new SpinnerOption(2, 132 context.getString( 133 R.string.account_setup_incoming_security_ssl_trust_certificates_label)), 134 new SpinnerOption(3, 135 context.getString(R.string.account_setup_incoming_security_tls_label)), 136 new SpinnerOption(4, 137 context.getString( 138 R.string.account_setup_incoming_security_tls_trust_certificates_label)), 139 }; 140 141 SpinnerOption deletePolicies[] = { 142 new SpinnerOption(Account.DELETE_POLICY_NEVER, 143 context.getString(R.string.account_setup_incoming_delete_policy_never_label)), 144 new SpinnerOption(Account.DELETE_POLICY_ON_DELETE, 145 context.getString(R.string.account_setup_incoming_delete_policy_delete_label)), 146 }; 147 148 ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context, 149 android.R.layout.simple_spinner_item, securityTypes); 150 securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 151 mSecurityTypeView.setAdapter(securityTypesAdapter); 152 153 ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(context, 154 android.R.layout.simple_spinner_item, deletePolicies); 155 deletePoliciesAdapter.setDropDownViewResource( 156 android.R.layout.simple_spinner_dropdown_item); 157 mDeletePolicyView.setAdapter(deletePoliciesAdapter); 158 159 // Updates the port when the user changes the security type. This allows 160 // us to show a reasonable default which the user can change. 161 mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 162 public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) { 163 updatePortFromSecurityType(); 164 } 165 166 public void onNothingSelected(AdapterView<?> arg0) { } 167 }); 168 169 // After any text edits, call validateFields() which enables or disables the Next button 170 TextWatcher validationTextWatcher = new TextWatcher() { 171 public void afterTextChanged(Editable s) { 172 validateFields(); 173 } 174 175 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 176 public void onTextChanged(CharSequence s, int start, int before, int count) { } 177 }; 178 mUsernameView.addTextChangedListener(validationTextWatcher); 179 mPasswordView.addTextChangedListener(validationTextWatcher); 180 mServerView.addTextChangedListener(validationTextWatcher); 181 mPortView.addTextChangedListener(validationTextWatcher); 182 183 // Only allow digits in the port field. 184 mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); 185 186 return view; 187 } 188 189 @Override 190 public void onActivityCreated(Bundle savedInstanceState) { 191 if (Email.DEBUG_LIFECYCLE && Email.DEBUG) { 192 Log.d(Email.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(Email.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(Email.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(Email.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(Email.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(Email.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(Email.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 String protocol = account.mHostAuthRecv.mProtocol; 284 if (protocol.startsWith("pop3")) { 285 mServerLabelView.setText(R.string.account_setup_incoming_pop_server_label); 286 mAccountPorts = POP_PORTS; 287 mAccountSchemes = POP_SCHEMES; 288 mImapPathPrefixSectionView.setVisibility(View.GONE); 289 } else if (protocol.startsWith("imap")) { 290 mServerLabelView.setText(R.string.account_setup_incoming_imap_server_label); 291 mAccountPorts = IMAP_PORTS; 292 mAccountSchemes = IMAP_SCHEMES; 293 mDeletePolicyLabelView.setVisibility(View.GONE); 294 mDeletePolicyView.setVisibility(View.GONE); 295 } else { 296 throw new Error("Unknown account type: " + account); 297 } 298 mConfigured = true; 299 } 300 301 /** 302 * Load the current settings into the UI 303 */ 304 private void loadSettings() { 305 if (mLoaded) return; 306 try { 307 // TODO this should be accessed directly via the HostAuth structure 308 EmailContent.Account account = SetupData.getAccount(); 309 URI uri = new URI(account.getStoreUri(mContext)); 310 String username = null; 311 String password = null; 312 if (uri.getUserInfo() != null) { 313 String[] userInfoParts = uri.getUserInfo().split(":", 2); 314 username = userInfoParts[0]; 315 if (userInfoParts.length > 1) { 316 password = userInfoParts[1]; 317 } 318 } 319 320 if (username != null) { 321 mUsernameView.setText(username); 322 } 323 324 if (password != null) { 325 mPasswordView.setText(password); 326 } 327 328 if (uri.getScheme().startsWith("pop3")) { 329 SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, account.getDeletePolicy()); 330 } else if (uri.getScheme().startsWith("imap")) { 331 if (uri.getPath() != null && uri.getPath().length() > 0) { 332 mImapPathPrefixView.setText(uri.getPath().substring(1)); 333 } 334 } else { 335 throw new Error("Unknown account type: " + account.getStoreUri(mContext)); 336 } 337 338 for (int i = 0; i < mAccountSchemes.length; i++) { 339 if (mAccountSchemes[i].equals(uri.getScheme())) { 340 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i); 341 } 342 } 343 344 if (uri.getHost() != null) { 345 mServerView.setText(uri.getHost()); 346 } 347 348 if (uri.getPort() != -1) { 349 mPortView.setText(Integer.toString(uri.getPort())); 350 } else { 351 updatePortFromSecurityType(); 352 } 353 } catch (URISyntaxException use) { 354 /* 355 * We should always be able to parse our own settings. 356 */ 357 throw new Error(use); 358 } 359 mLoaded = true; 360 validateFields(); 361 } 362 363 /** 364 * Check the values in the fields and decide if it makes sense to enable the "next" button 365 */ 366 private void validateFields() { 367 if (!mConfigured || !mLoaded) return; 368 boolean enabled = Utility.isTextViewNotEmpty(mUsernameView) 369 && Utility.isTextViewNotEmpty(mPasswordView) 370 && Utility.isTextViewNotEmpty(mServerView) 371 && Utility.isPortFieldValid(mPortView); 372 if (enabled) { 373 try { 374 URI uri = getUri(); 375 } catch (URISyntaxException use) { 376 enabled = false; 377 } 378 } 379 enableNextButton(enabled); 380 } 381 382 private void updatePortFromSecurityType() { 383 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 384 mPortView.setText(Integer.toString(mAccountPorts[securityType])); 385 } 386 387 /** 388 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 389 */ 390 @Override 391 public void saveSettingsAfterEdit() { 392 EmailContent.Account account = SetupData.getAccount(); 393 if (account.isSaved()) { 394 account.update(mContext, account.toContentValues()); 395 account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues()); 396 } else { 397 account.save(mContext); 398 } 399 // Update the backup (side copy) of the accounts 400 AccountBackupRestore.backupAccounts(mContext); 401 } 402 403 /** 404 * Entry point from Activity after entering new settings and verifying them. For setup mode. 405 */ 406 @Override 407 public void saveSettingsAfterSetup() { 408 EmailContent.Account account = SetupData.getAccount(); 409 410 // Set the username and password for the outgoing settings to the username and 411 // password the user just set for incoming. Use the verified host address to try and 412 // pick a smarter outgoing address. 413 try { 414 String hostName = 415 AccountSettingsUtils.inferServerName(account.mHostAuthRecv.mAddress, null, "smtp"); 416 URI oldUri = new URI(account.getSenderUri(mContext)); 417 URI uri = new URI( 418 oldUri.getScheme(), 419 mUsernameView.getText().toString().trim() + ":" 420 + mPasswordView.getText().toString(), 421 hostName, 422 oldUri.getPort(), 423 null, 424 null, 425 null); 426 account.setSenderUri(mContext, uri.toString()); 427 } catch (URISyntaxException use) { 428 // If we can't set up the URL we just continue. It's only for convenience. 429 } 430 } 431 432 /** 433 * Attempt to create a URI from the fields provided. Throws URISyntaxException if there's 434 * a problem with the user input. 435 * @return a URI built from the account setup fields 436 */ 437 /* package */ URI getUri() throws URISyntaxException { 438 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 439 String path = null; 440 if (mAccountSchemes[securityType].startsWith("imap")) { 441 path = "/" + mImapPathPrefixView.getText().toString().trim(); 442 } 443 String userName = mUsernameView.getText().toString().trim(); 444 mCacheLoginCredential = userName; 445 URI uri = new URI( 446 mAccountSchemes[securityType], 447 userName + ":" + mPasswordView.getText(), 448 mServerView.getText().toString().trim(), 449 Integer.parseInt(mPortView.getText().toString().trim()), 450 path, // path 451 null, // query 452 null); 453 454 return uri; 455 } 456 457 /** 458 * Entry point from Activity, when "next" button is clicked 459 */ 460 @Override 461 public void onNext() { 462 EmailContent.Account setupAccount = SetupData.getAccount(); 463 try { 464 URI uri = getUri(); 465 setupAccount.setStoreUri(mContext, uri.toString()); 466 467 // Stop here if the login credentials duplicate an existing account 468 // (unless they duplicate the existing account, as they of course will) 469 EmailContent.Account account = Utility.findExistingAccount(mContext, setupAccount.mId, 470 uri.getHost(), mCacheLoginCredential); 471 if (account != null) { 472 DuplicateAccountDialogFragment dialogFragment = 473 DuplicateAccountDialogFragment.newInstance(account.mDisplayName); 474 dialogFragment.show(getActivity(), DuplicateAccountDialogFragment.TAG); 475 return; 476 } 477 } catch (URISyntaxException use) { 478 /* 479 * It's unrecoverable if we cannot create a URI from components that 480 * we validated to be safe. 481 */ 482 throw new Error(use); 483 } 484 485 setupAccount.setDeletePolicy( 486 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value); 487 488 mCallback.onProceedNext(SetupData.CHECK_INCOMING, this); 489 } 490} 491