1 /* 2 * Copyright (C) 2008 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 com.android.email.Account; 20 import com.android.email.AccountBackupRestore; 21 import com.android.email.R; 22 import com.android.email.Utility; 23 import com.android.email.provider.EmailContent; 24 25 import android.app.Activity; 26 import android.app.AlertDialog; 27 import android.app.Dialog; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.os.Bundle; 31 import android.text.Editable; 32 import android.text.TextWatcher; 33 import android.text.method.DigitsKeyListener; 34 import android.view.View; 35 import android.view.View.OnClickListener; 36 import android.widget.AdapterView; 37 import android.widget.ArrayAdapter; 38 import android.widget.Button; 39 import android.widget.EditText; 40 import android.widget.Spinner; 41 import android.widget.TextView; 42 43 import java.net.URI; 44 import java.net.URISyntaxException; 45 46 public class AccountSetupIncoming extends Activity implements OnClickListener { 47 private static final String EXTRA_ACCOUNT = "account"; 48 private static final String EXTRA_MAKE_DEFAULT = "makeDefault"; 49 50 private static final int POP_PORTS[] = { 51 110, 995, 995, 110, 110 52 }; 53 private static final String POP_SCHEMES[] = { 54 "pop3", "pop3+ssl+", "pop3+ssl+trustallcerts", "pop3+tls+", "pop3+tls+trustallcerts" 55 }; 56 private static final int IMAP_PORTS[] = { 57 143, 993, 993, 143, 143 58 }; 59 private static final String IMAP_SCHEMES[] = { 60 "imap", "imap+ssl+", "imap+ssl+trustallcerts", "imap+tls+", "imap+tls+trustallcerts" 61 }; 62 63 private final static int DIALOG_DUPLICATE_ACCOUNT = 1; 64 65 private int mAccountPorts[]; 66 private String mAccountSchemes[]; 67 private EditText mUsernameView; 68 private EditText mPasswordView; 69 private EditText mServerView; 70 private EditText mPortView; 71 private Spinner mSecurityTypeView; 72 private Spinner mDeletePolicyView; 73 private EditText mImapPathPrefixView; 74 private Button mNextButton; 75 private EmailContent.Account mAccount; 76 private boolean mMakeDefault; 77 private String mCacheLoginCredential; 78 private String mDuplicateAccountName; 79 80 public static void actionIncomingSettings(Activity fromActivity, EmailContent.Account account, 81 boolean makeDefault) { 82 Intent i = new Intent(fromActivity, AccountSetupIncoming.class); 83 i.putExtra(EXTRA_ACCOUNT, account); 84 i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault); 85 fromActivity.startActivity(i); 86 } 87 88 public static void actionEditIncomingSettings(Activity fromActivity, EmailContent.Account account) 89 { 90 Intent i = new Intent(fromActivity, AccountSetupIncoming.class); 91 i.setAction(Intent.ACTION_EDIT); 92 i.putExtra(EXTRA_ACCOUNT, account); 93 fromActivity.startActivity(i); 94 } 95 96 @Override 97 public void onCreate(Bundle savedInstanceState) { 98 super.onCreate(savedInstanceState); 99 setContentView(R.layout.account_setup_incoming); 100 101 mUsernameView = (EditText)findViewById(R.id.account_username); 102 mPasswordView = (EditText)findViewById(R.id.account_password); 103 TextView serverLabelView = (TextView) findViewById(R.id.account_server_label); 104 mServerView = (EditText)findViewById(R.id.account_server); 105 mPortView = (EditText)findViewById(R.id.account_port); 106 mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type); 107 mDeletePolicyView = (Spinner)findViewById(R.id.account_delete_policy); 108 mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix); 109 mNextButton = (Button)findViewById(R.id.next); 110 111 mNextButton.setOnClickListener(this); 112 113 SpinnerOption securityTypes[] = { 114 new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)), 115 new SpinnerOption(1, getString(R.string.account_setup_incoming_security_ssl_label)), 116 new SpinnerOption(2, getString( 117 R.string.account_setup_incoming_security_ssl_trust_certificates_label)), 118 new SpinnerOption(3, getString(R.string.account_setup_incoming_security_tls_label)), 119 new SpinnerOption(4, getString( 120 R.string.account_setup_incoming_security_tls_trust_certificates_label)), 121 }; 122 123 SpinnerOption deletePolicies[] = { 124 new SpinnerOption(Account.DELETE_POLICY_NEVER, 125 getString(R.string.account_setup_incoming_delete_policy_never_label)), 126 new SpinnerOption(Account.DELETE_POLICY_ON_DELETE, 127 getString(R.string.account_setup_incoming_delete_policy_delete_label)), 128 }; 129 130 ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(this, 131 android.R.layout.simple_spinner_item, securityTypes); 132 securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 133 mSecurityTypeView.setAdapter(securityTypesAdapter); 134 135 ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(this, 136 android.R.layout.simple_spinner_item, deletePolicies); 137 deletePoliciesAdapter 138 .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 139 mDeletePolicyView.setAdapter(deletePoliciesAdapter); 140 141 /* 142 * Updates the port when the user changes the security type. This allows 143 * us to show a reasonable default which the user can change. 144 */ 145 mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 146 public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) { 147 updatePortFromSecurityType(); 148 } 149 150 public void onNothingSelected(AdapterView<?> arg0) { 151 } 152 }); 153 154 /* 155 * Calls validateFields() which enables or disables the Next button 156 * based on the fields' validity. 157 */ 158 TextWatcher validationTextWatcher = new TextWatcher() { 159 public void afterTextChanged(Editable s) { 160 validateFields(); 161 } 162 163 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 164 } 165 166 public void onTextChanged(CharSequence s, int start, int before, int count) { 167 } 168 }; 169 mUsernameView.addTextChangedListener(validationTextWatcher); 170 mPasswordView.addTextChangedListener(validationTextWatcher); 171 mServerView.addTextChangedListener(validationTextWatcher); 172 mPortView.addTextChangedListener(validationTextWatcher); 173 174 /* 175 * Only allow digits in the port field. 176 */ 177 mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); 178 179 mAccount = (EmailContent.Account)getIntent().getParcelableExtra(EXTRA_ACCOUNT); 180 mMakeDefault = getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false); 181 182 /* 183 * If we're being reloaded we override the original account with the one 184 * we saved 185 */ 186 if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) { 187 mAccount = (EmailContent.Account)savedInstanceState.getParcelable(EXTRA_ACCOUNT); 188 } 189 190 try { 191 // TODO this should be accessed directly via the HostAuth structure 192 URI uri = new URI(mAccount.getStoreUri(this)); 193 String username = null; 194 String password = null; 195 if (uri.getUserInfo() != null) { 196 String[] userInfoParts = uri.getUserInfo().split(":", 2); 197 username = userInfoParts[0]; 198 if (userInfoParts.length > 1) { 199 password = userInfoParts[1]; 200 } 201 } 202 203 if (username != null) { 204 mUsernameView.setText(username); 205 } 206 207 if (password != null) { 208 mPasswordView.setText(password); 209 } 210 211 if (uri.getScheme().startsWith("pop3")) { 212 serverLabelView.setText(R.string.account_setup_incoming_pop_server_label); 213 mAccountPorts = POP_PORTS; 214 mAccountSchemes = POP_SCHEMES; 215 216 findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE); 217 } else if (uri.getScheme().startsWith("imap")) { 218 serverLabelView.setText(R.string.account_setup_incoming_imap_server_label); 219 mAccountPorts = IMAP_PORTS; 220 mAccountSchemes = IMAP_SCHEMES; 221 222 findViewById(R.id.account_delete_policy_label).setVisibility(View.GONE); 223 mDeletePolicyView.setVisibility(View.GONE); 224 if (uri.getPath() != null && uri.getPath().length() > 0) { 225 mImapPathPrefixView.setText(uri.getPath().substring(1)); 226 } 227 } else { 228 throw new Error("Unknown account type: " + mAccount.getStoreUri(this)); 229 } 230 231 for (int i = 0; i < mAccountSchemes.length; i++) { 232 if (mAccountSchemes[i].equals(uri.getScheme())) { 233 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i); 234 } 235 } 236 237 SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mAccount.getDeletePolicy()); 238 239 if (uri.getHost() != null) { 240 mServerView.setText(uri.getHost()); 241 } 242 243 if (uri.getPort() != -1) { 244 mPortView.setText(Integer.toString(uri.getPort())); 245 } else { 246 updatePortFromSecurityType(); 247 } 248 } catch (URISyntaxException use) { 249 /* 250 * We should always be able to parse our own settings. 251 */ 252 throw new Error(use); 253 } 254 255 validateFields(); 256 } 257 258 @Override 259 public void onSaveInstanceState(Bundle outState) { 260 super.onSaveInstanceState(outState); 261 outState.putParcelable(EXTRA_ACCOUNT, mAccount); 262 } 263 264 /** 265 * Prepare a cached dialog with current values (e.g. account name) 266 */ 267 @Override 268 public Dialog onCreateDialog(int id) { 269 switch (id) { 270 case DIALOG_DUPLICATE_ACCOUNT: 271 return new AlertDialog.Builder(this) 272 .setIcon(android.R.drawable.ic_dialog_alert) 273 .setTitle(R.string.account_duplicate_dlg_title) 274 .setMessage(getString(R.string.account_duplicate_dlg_message_fmt, 275 mDuplicateAccountName)) 276 .setPositiveButton(R.string.okay_action, 277 new DialogInterface.OnClickListener() { 278 public void onClick(DialogInterface dialog, int which) { 279 dismissDialog(DIALOG_DUPLICATE_ACCOUNT); 280 } 281 }) 282 .create(); 283 } 284 return null; 285 } 286 287 /** 288 * Update a cached dialog with current values (e.g. account name) 289 */ 290 @Override 291 public void onPrepareDialog(int id, Dialog dialog) { 292 switch (id) { 293 case DIALOG_DUPLICATE_ACCOUNT: 294 if (mDuplicateAccountName != null) { 295 AlertDialog alert = (AlertDialog) dialog; 296 alert.setMessage(getString(R.string.account_duplicate_dlg_message_fmt, 297 mDuplicateAccountName)); 298 } 299 break; 300 } 301 } 302 303 /** 304 * Check the values in the fields and decide if it makes sense to enable the "next" button 305 * NOTE: Does it make sense to extract & combine with similar code in AccountSetupIncoming? 306 */ 307 private void validateFields() { 308 boolean enabled = Utility.requiredFieldValid(mUsernameView) 309 && Utility.requiredFieldValid(mPasswordView) 310 && Utility.requiredFieldValid(mServerView) 311 && Utility.isPortFieldValid(mPortView); 312 if (enabled) { 313 try { 314 URI uri = getUri(); 315 } catch (URISyntaxException use) { 316 enabled = false; 317 } 318 } 319 mNextButton.setEnabled(enabled); 320 Utility.setCompoundDrawablesAlpha(mNextButton, enabled ? 255 : 128); 321 } 322 323 private void updatePortFromSecurityType() { 324 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 325 mPortView.setText(Integer.toString(mAccountPorts[securityType])); 326 } 327 328 @Override 329 public void onActivityResult(int requestCode, int resultCode, Intent data) { 330 if (resultCode == RESULT_OK) { 331 if (Intent.ACTION_EDIT.equals(getIntent().getAction())) { 332 if (mAccount.isSaved()) { 333 mAccount.update(this, mAccount.toContentValues()); 334 mAccount.mHostAuthRecv.update(this, mAccount.mHostAuthRecv.toContentValues()); 335 } else { 336 mAccount.save(this); 337 } 338 // Update the backup (side copy) of the accounts 339 AccountBackupRestore.backupAccounts(this); 340 finish(); 341 } else { 342 /* 343 * Set the username and password for the outgoing settings to the username and 344 * password the user just set for incoming. 345 */ 346 try { 347 URI oldUri = new URI(mAccount.getSenderUri(this)); 348 URI uri = new URI( 349 oldUri.getScheme(), 350 mUsernameView.getText().toString().trim() + ":" 351 + mPasswordView.getText().toString().trim(), 352 oldUri.getHost(), 353 oldUri.getPort(), 354 null, 355 null, 356 null); 357 mAccount.setSenderUri(this, uri.toString()); 358 } catch (URISyntaxException use) { 359 /* 360 * If we can't set up the URL we just continue. It's only for 361 * convenience. 362 */ 363 } 364 365 AccountSetupOutgoing.actionOutgoingSettings(this, mAccount, mMakeDefault); 366 finish(); 367 } 368 } 369 } 370 371 /** 372 * Attempt to create a URI from the fields provided. Throws URISyntaxException if there's 373 * a problem with the user input. 374 * @return a URI built from the account setup fields 375 */ 376 /* package */ URI getUri() throws URISyntaxException { 377 int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 378 String path = null; 379 if (mAccountSchemes[securityType].startsWith("imap")) { 380 path = "/" + mImapPathPrefixView.getText().toString().trim(); 381 } 382 String userName = mUsernameView.getText().toString().trim(); 383 mCacheLoginCredential = userName; 384 URI uri = new URI( 385 mAccountSchemes[securityType], 386 userName + ":" + mPasswordView.getText(), 387 mServerView.getText().toString().trim(), 388 Integer.parseInt(mPortView.getText().toString().trim()), 389 path, // path 390 null, // query 391 null); 392 393 return uri; 394 } 395 396 private void onNext() { 397 try { 398 URI uri = getUri(); 399 mAccount.setStoreUri(this, uri.toString()); 400 401 // Stop here if the login credentials duplicate an existing account 402 // (unless they duplicate the existing account, as they of course will) 403 mDuplicateAccountName = Utility.findDuplicateAccount(this, mAccount.mId, 404 uri.getHost(), mCacheLoginCredential); 405 if (mDuplicateAccountName != null) { 406 this.showDialog(DIALOG_DUPLICATE_ACCOUNT); 407 return; 408 } 409 } catch (URISyntaxException use) { 410 /* 411 * It's unrecoverable if we cannot create a URI from components that 412 * we validated to be safe. 413 */ 414 throw new Error(use); 415 } 416 417 mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value); 418 AccountSetupCheckSettings.actionValidateSettings(this, mAccount, true, false); 419 } 420 421 public void onClick(View v) { 422 switch (v.getId()) { 423 case R.id.next: 424 onNext(); 425 break; 426 } 427 } 428 } 429