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 android.app.Activity; 20import android.content.Context; 21import android.content.Intent; 22import android.content.Loader; 23import android.os.Bundle; 24import android.os.Parcel; 25import android.text.Editable; 26import android.text.TextUtils; 27import android.text.TextWatcher; 28import android.text.method.DigitsKeyListener; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.AdapterView; 33import android.widget.ArrayAdapter; 34import android.widget.CheckBox; 35import android.widget.CompoundButton; 36import android.widget.CompoundButton.OnCheckedChangeListener; 37import android.widget.EditText; 38import android.widget.Spinner; 39import android.widget.TextView; 40 41import com.android.email.R; 42import com.android.email.activity.UiUtilities; 43import com.android.email.activity.setup.AuthenticationView.AuthenticationCallback; 44import com.android.email.provider.AccountBackupRestore; 45import com.android.emailcommon.VendorPolicyLoader; 46import com.android.emailcommon.provider.Account; 47import com.android.emailcommon.provider.Credential; 48import com.android.emailcommon.provider.HostAuth; 49import com.android.emailcommon.utility.Utility; 50import com.android.mail.ui.MailAsyncTaskLoader; 51import com.android.mail.utils.LogUtils; 52 53import java.util.List; 54 55/** 56 * Provides UI for SMTP account settings (for IMAP/POP accounts). 57 * 58 * This fragment is used by AccountSetupOutgoing (for creating accounts) and by AccountSettingsXL 59 * (for editing existing accounts). 60 */ 61public class AccountSetupOutgoingFragment extends AccountServerBaseFragment 62 implements OnCheckedChangeListener, AuthenticationCallback { 63 64 private static final int SIGN_IN_REQUEST = 1; 65 66 private final static String STATE_KEY_LOADED = "AccountSetupOutgoingFragment.loaded"; 67 68 private static final int SMTP_PORT_NORMAL = 587; 69 private static final int SMTP_PORT_SSL = 465; 70 71 private EditText mUsernameView; 72 private AuthenticationView mAuthenticationView; 73 private TextView mAuthenticationLabel; 74 private EditText mServerView; 75 private EditText mPortView; 76 private CheckBox mRequireLoginView; 77 private Spinner mSecurityTypeView; 78 79 // Support for lifecycle 80 private boolean mLoaded; 81 82 public static AccountSetupOutgoingFragment newInstance(boolean settingsMode) { 83 final AccountSetupOutgoingFragment f = new AccountSetupOutgoingFragment(); 84 f.setArguments(getArgs(settingsMode)); 85 return f; 86 } 87 88 // Public no-args constructor needed for fragment re-instantiation 89 public AccountSetupOutgoingFragment() {} 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 super.onCreate(savedInstanceState); 98 99 if (savedInstanceState != null) { 100 mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false); 101 } 102 mBaseScheme = HostAuth.LEGACY_SCHEME_SMTP; 103 } 104 105 @Override 106 public View onCreateView(LayoutInflater inflater, ViewGroup container, 107 Bundle savedInstanceState) { 108 final View view; 109 if (mSettingsMode) { 110 view = inflater.inflate(R.layout.account_settings_outgoing_fragment, container, false); 111 } else { 112 view = inflateTemplatedView(inflater, container, 113 R.layout.account_setup_outgoing_fragment, 114 R.string.account_setup_outgoing_headline); 115 } 116 117 mUsernameView = UiUtilities.getView(view, R.id.account_username); 118 mAuthenticationView = UiUtilities.getView(view, R.id.authentication_view); 119 mServerView = UiUtilities.getView(view, R.id.account_server); 120 mPortView = UiUtilities.getView(view, R.id.account_port); 121 mRequireLoginView = UiUtilities.getView(view, R.id.account_require_login); 122 mSecurityTypeView = UiUtilities.getView(view, R.id.account_security_type); 123 mRequireLoginView.setOnCheckedChangeListener(this); 124 // Don't use UiUtilities here. In some configurations this view does not exist, and 125 // UiUtilities throws an exception in this case. 126 mAuthenticationLabel = (TextView)view.findViewById(R.id.authentication_label); 127 128 // Updates the port when the user changes the security type. This allows 129 // us to show a reasonable default which the user can change. 130 mSecurityTypeView.post(new Runnable() { 131 @Override 132 public void run() { 133 mSecurityTypeView.setOnItemSelectedListener( 134 new AdapterView.OnItemSelectedListener() { 135 @Override 136 public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, 137 long arg3) { 138 updatePortFromSecurityType(); 139 } 140 141 @Override 142 public void onNothingSelected(AdapterView<?> arg0) { 143 } 144 }); 145 }}); 146 147 // Calls validateFields() which enables or disables the Next button 148 final TextWatcher validationTextWatcher = new TextWatcher() { 149 @Override 150 public void afterTextChanged(Editable s) { 151 validateFields(); 152 } 153 154 @Override 155 public void beforeTextChanged(CharSequence s, int start, int count, int after) { } 156 @Override 157 public void onTextChanged(CharSequence s, int start, int before, int count) { } 158 }; 159 mUsernameView.addTextChangedListener(validationTextWatcher); 160 mServerView.addTextChangedListener(validationTextWatcher); 161 mPortView.addTextChangedListener(validationTextWatcher); 162 163 // Only allow digits in the port field. 164 mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789")); 165 166 // Additional setup only used while in "settings" mode 167 onCreateViewSettingsMode(view); 168 169 mAuthenticationView.setAuthenticationCallback(this); 170 171 return view; 172 } 173 174 @Override 175 public void onActivityCreated(Bundle savedInstanceState) { 176 super.onActivityCreated(savedInstanceState); 177 178 final Context context = getActivity(); 179 // Note: Strings are shared with AccountSetupIncomingFragment 180 final SpinnerOption securityTypes[] = { 181 new SpinnerOption(HostAuth.FLAG_NONE, context.getString( 182 R.string.account_setup_incoming_security_none_label)), 183 new SpinnerOption(HostAuth.FLAG_SSL, context.getString( 184 R.string.account_setup_incoming_security_ssl_label)), 185 new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString( 186 R.string.account_setup_incoming_security_ssl_trust_certificates_label)), 187 new SpinnerOption(HostAuth.FLAG_TLS, context.getString( 188 R.string.account_setup_incoming_security_tls_label)), 189 new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, context.getString( 190 R.string.account_setup_incoming_security_tls_trust_certificates_label)), 191 }; 192 193 final ArrayAdapter<SpinnerOption> securityTypesAdapter = 194 new ArrayAdapter<SpinnerOption>(context, android.R.layout.simple_spinner_item, 195 securityTypes); 196 securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 197 mSecurityTypeView.setAdapter(securityTypesAdapter); 198 199 loadSettings(); 200 } 201 202 /** 203 * Called when the fragment is visible to the user and actively running. 204 */ 205 @Override 206 public void onResume() { 207 super.onResume(); 208 validateFields(); 209 } 210 211 @Override 212 public void onSaveInstanceState(Bundle outState) { 213 super.onSaveInstanceState(outState); 214 215 outState.putBoolean(STATE_KEY_LOADED, mLoaded); 216 } 217 218 /** 219 * Load the current settings into the UI 220 */ 221 private void loadSettings() { 222 if (mLoaded) return; 223 224 final HostAuth sendAuth = mSetupData.getAccount().getOrCreateHostAuthSend(mAppContext); 225 if (!mSetupData.isOutgoingCredLoaded()) { 226 sendAuth.setUserName(mSetupData.getEmail()); 227 AccountSetupCredentialsFragment.populateHostAuthWithResults(mAppContext, sendAuth, 228 mSetupData.getCredentialResults()); 229 final String[] emailParts = mSetupData.getEmail().split("@"); 230 final String domain = emailParts[1]; 231 sendAuth.setConnection(sendAuth.mProtocol, domain, HostAuth.PORT_UNKNOWN, 232 HostAuth.FLAG_NONE); 233 mSetupData.setOutgoingCredLoaded(true); 234 } 235 if ((sendAuth.mFlags & HostAuth.FLAG_AUTHENTICATE) != 0) { 236 final String username = sendAuth.mLogin; 237 if (username != null) { 238 mUsernameView.setText(username); 239 mRequireLoginView.setChecked(true); 240 } 241 242 final List<VendorPolicyLoader.OAuthProvider> oauthProviders = 243 AccountSettingsUtils.getAllOAuthProviders(getActivity()); 244 mAuthenticationView.setAuthInfo(oauthProviders.size() > 0, sendAuth); 245 if (mAuthenticationLabel != null) { 246 mAuthenticationLabel.setText(R.string.authentication_label); 247 } 248 } 249 250 final int flags = sendAuth.mFlags & HostAuth.FLAG_TRANSPORTSECURITY_MASK; 251 SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags); 252 253 final String hostname = sendAuth.mAddress; 254 if (hostname != null) { 255 mServerView.setText(hostname); 256 } 257 258 final int port = sendAuth.mPort; 259 if (port != -1) { 260 mPortView.setText(Integer.toString(port)); 261 } else { 262 updatePortFromSecurityType(); 263 } 264 265 // Make a deep copy of the HostAuth to compare with later 266 final Parcel parcel = Parcel.obtain(); 267 parcel.writeParcelable(sendAuth, sendAuth.describeContents()); 268 parcel.setDataPosition(0); 269 mLoadedSendAuth = parcel.readParcelable(HostAuth.class.getClassLoader()); 270 parcel.recycle(); 271 272 mLoaded = true; 273 validateFields(); 274 } 275 276 /** 277 * Preflight the values in the fields and decide if it makes sense to enable the "next" button 278 */ 279 private void validateFields() { 280 if (!mLoaded) return; 281 boolean enabled = 282 Utility.isServerNameValid(mServerView) && Utility.isPortFieldValid(mPortView); 283 284 if (enabled && mRequireLoginView.isChecked()) { 285 enabled = !TextUtils.isEmpty(mUsernameView.getText()) 286 && mAuthenticationView.getAuthValid(); 287 } 288 enableNextButton(enabled); 289 } 290 291 /** 292 * implements OnCheckedChangeListener 293 */ 294 @Override 295 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 296 final HostAuth sendAuth = mSetupData.getAccount().getOrCreateHostAuthSend(mAppContext); 297 mAuthenticationView.setAuthInfo(true, sendAuth); 298 final int visibility = isChecked ? View.VISIBLE : View.GONE; 299 UiUtilities.setVisibilitySafe(getView(), R.id.account_require_login_settings, visibility); 300 UiUtilities.setVisibilitySafe(getView(), R.id.account_require_login_settings_2, visibility); 301 validateFields(); 302 } 303 304 private int getPortFromSecurityType() { 305 final int securityType = 306 (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 307 return (securityType & HostAuth.FLAG_SSL) != 0 ? SMTP_PORT_SSL : SMTP_PORT_NORMAL; 308 } 309 310 private void updatePortFromSecurityType() { 311 final int port = getPortFromSecurityType(); 312 mPortView.setText(Integer.toString(port)); 313 } 314 315 private static class SaveSettingsLoader extends MailAsyncTaskLoader<Boolean> { 316 private final SetupDataFragment mSetupData; 317 private final boolean mSettingsMode; 318 319 private SaveSettingsLoader(Context context, SetupDataFragment setupData, 320 boolean settingsMode) { 321 super(context); 322 mSetupData = setupData; 323 mSettingsMode = settingsMode; 324 } 325 326 @Override 327 public Boolean loadInBackground() { 328 if (mSettingsMode) { 329 saveSettingsAfterEdit(getContext(), mSetupData); 330 } else { 331 saveSettingsAfterSetup(getContext(), mSetupData); 332 } 333 return true; 334 } 335 336 @Override 337 protected void onDiscardResult(Boolean result) {} 338 } 339 340 @Override 341 public Loader<Boolean> getSaveSettingsLoader() { 342 return new SaveSettingsLoader(mAppContext, mSetupData, mSettingsMode); 343 } 344 345 /** 346 * Entry point from Activity after editing settings and verifying them. Must be FLOW_MODE_EDIT. 347 * Blocking - do not call from UI Thread. 348 */ 349 public static void saveSettingsAfterEdit(Context context, SetupDataFragment setupData) { 350 final Account account = setupData.getAccount(); 351 final Credential cred = account.mHostAuthSend.mCredential; 352 if (cred != null) { 353 if (cred.isSaved()) { 354 cred.update(context, cred.toContentValues()); 355 } else { 356 cred.save(context); 357 account.mHostAuthSend.mCredentialKey = cred.mId; 358 } 359 } 360 account.mHostAuthSend.update(context, account.mHostAuthSend.toContentValues()); 361 // Update the backup (side copy) of the accounts 362 AccountBackupRestore.backup(context); 363 } 364 365 /** 366 * Entry point from Activity after entering new settings and verifying them. For setup mode. 367 */ 368 @SuppressWarnings("unused") 369 public static void saveSettingsAfterSetup(Context context, SetupDataFragment setupData) { 370 // No need to do anything here 371 } 372 373 /** 374 * Entry point from Activity, when "next" button is clicked 375 */ 376 @Override 377 public int collectUserInputInternal() { 378 final Account account = mSetupData.getAccount(); 379 final HostAuth sendAuth = account.getOrCreateHostAuthSend(mAppContext); 380 381 if (mRequireLoginView.isChecked()) { 382 final String userName = mUsernameView.getText().toString().trim(); 383 final String userPassword = mAuthenticationView.getPassword(); 384 sendAuth.setLogin(userName, userPassword); 385 } else { 386 sendAuth.setLogin(null, null); 387 } 388 389 final String serverAddress = mServerView.getText().toString().trim(); 390 int serverPort; 391 try { 392 serverPort = Integer.parseInt(mPortView.getText().toString().trim()); 393 } catch (NumberFormatException e) { 394 serverPort = getPortFromSecurityType(); 395 LogUtils.d(LogUtils.TAG, "Non-integer server port; using '" + serverPort + "'"); 396 } 397 final int securityType = 398 (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value; 399 sendAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType); 400 sendAuth.mDomain = null; 401 402 return SetupDataFragment.CHECK_OUTGOING; 403 } 404 405 @Override 406 public void onValidateStateChanged() { 407 validateFields(); 408 } 409 410 @Override 411 public void onRequestSignIn() { 412 // Launch the credential activity. 413 // Use HostAuthRecv here because we want to know if this is account is IMAP (offer OAuth) or 414 // if it's POP (password only) 415 final String protocol = 416 mSetupData.getAccount().getOrCreateHostAuthRecv(mAppContext).mProtocol; 417 final Intent intent = AccountCredentials.getAccountCredentialsIntent(getActivity(), 418 mUsernameView.getText().toString(), protocol); 419 startActivityForResult(intent, SIGN_IN_REQUEST); 420 } 421 422 @Override 423 public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { 424 if (requestCode == SIGN_IN_REQUEST && resultCode == Activity.RESULT_OK) { 425 final Account account = mSetupData.getAccount(); 426 final HostAuth sendAuth = account.getOrCreateHostAuthSend(getActivity()); 427 AccountSetupCredentialsFragment.populateHostAuthWithResults(mAppContext, sendAuth, 428 data.getExtras()); 429 mAuthenticationView.setAuthInfo(true, sendAuth); 430 } 431 } 432} 433