1 /* 2 * Copyright (C) 2014 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 package com.android.email.activity.setup; 17 18 import android.app.Activity; 19 import android.content.Context; 20 import android.content.Intent; 21 import android.os.Bundle; 22 import android.text.Editable; 23 import android.text.TextUtils; 24 import android.text.TextWatcher; 25 import android.text.format.DateUtils; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.View.OnClickListener; 29 import android.view.ViewGroup; 30 import android.widget.EditText; 31 import android.widget.TextView; 32 33 import com.android.email.R; 34 import com.android.email.activity.UiUtilities; 35 import com.android.email.service.EmailServiceUtils; 36 import com.android.email.service.EmailServiceUtils.EmailServiceInfo; 37 import com.android.email.view.CertificateSelector; 38 import com.android.email.view.CertificateSelector.HostCallback; 39 import com.android.emailcommon.Device; 40 import com.android.emailcommon.VendorPolicyLoader.OAuthProvider; 41 import com.android.emailcommon.provider.Credential; 42 import com.android.emailcommon.provider.HostAuth; 43 import com.android.emailcommon.utility.CertificateRequestor; 44 import com.android.mail.utils.LogUtils; 45 46 import java.io.IOException; 47 import java.util.List; 48 49 public class AccountSetupCredentialsFragment extends AccountSetupFragment 50 implements OnClickListener, HostCallback { 51 52 private static final int CERTIFICATE_REQUEST = 1000; 53 54 private static final String EXTRA_EMAIL = "email"; 55 private static final String EXTRA_PROTOCOL = "protocol"; 56 private static final String EXTRA_PASSWORD_FAILED = "password_failed"; 57 private static final String EXTRA_STANDALONE = "standalone"; 58 59 public static final String EXTRA_PASSWORD = "password"; 60 public static final String EXTRA_CLIENT_CERT = "certificate"; 61 public static final String EXTRA_OAUTH_PROVIDER = "provider"; 62 public static final String EXTRA_OAUTH_ACCESS_TOKEN = "accessToken"; 63 public static final String EXTRA_OAUTH_REFRESH_TOKEN = "refreshToken"; 64 public static final String EXTRA_OAUTH_EXPIRES_IN_SECONDS = "expiresInSeconds"; 65 66 private View mOAuthGroup; 67 private View mOAuthButton; 68 private EditText mImapPasswordText; 69 private EditText mRegularPasswordText; 70 private TextWatcher mValidationTextWatcher; 71 private TextView mPasswordWarningLabel; 72 private TextView mEmailConfirmationLabel; 73 private TextView mEmailConfirmation; 74 private CertificateSelector mClientCertificateSelector; 75 private View mDeviceIdSection; 76 private TextView mDeviceId; 77 78 private String mEmailAddress; 79 private boolean mOfferOAuth; 80 private boolean mOfferCerts; 81 private String mProviderId; 82 List<OAuthProvider> mOauthProviders; 83 84 private Context mAppContext; 85 86 private Bundle mResults; 87 88 public interface Callback extends AccountSetupFragment.Callback { onCredentialsComplete(Bundle results)89 void onCredentialsComplete(Bundle results); 90 } 91 92 /** 93 * Create a new instance of this fragment with the appropriate email and protocol 94 * @param email login address for OAuth purposes 95 * @param protocol protocol of the service we're gathering credentials for 96 * @param clientCert alias of existing client cert 97 * @param passwordFailed true if the password attempt previously failed 98 * @param standalone true if this is not being inserted in the setup flow 99 * @return new fragment instance 100 */ newInstance(final String email, final String protocol, final String clientCert, final boolean passwordFailed, final boolean standalone)101 public static AccountSetupCredentialsFragment newInstance(final String email, 102 final String protocol, final String clientCert, final boolean passwordFailed, 103 final boolean standalone) { 104 final AccountSetupCredentialsFragment f = new AccountSetupCredentialsFragment(); 105 final Bundle b = new Bundle(5); 106 b.putString(EXTRA_EMAIL, email); 107 b.putString(EXTRA_PROTOCOL, protocol); 108 b.putString(EXTRA_CLIENT_CERT, clientCert); 109 b.putBoolean(EXTRA_PASSWORD_FAILED, passwordFailed); 110 b.putBoolean(EXTRA_STANDALONE, standalone); 111 f.setArguments(b); 112 return f; 113 } 114 115 @Override onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState)116 public View onCreateView(final LayoutInflater inflater, final ViewGroup container, 117 final Bundle savedInstanceState) { 118 final boolean standalone = getArguments().getBoolean(EXTRA_STANDALONE); 119 final View view; 120 if (standalone) { 121 view = inflater.inflate(R.layout.account_credentials_fragment, container, false); 122 mNextButton = UiUtilities.getView(view, R.id.done); 123 mNextButton.setOnClickListener(this); 124 mPreviousButton = UiUtilities.getView(view, R.id.cancel); 125 mPreviousButton.setOnClickListener(this); 126 } else { 127 // TODO: real headline string instead of sign_in_title 128 view = inflateTemplatedView(inflater, container, 129 R.layout.account_setup_credentials_fragment, R.string.sign_in_title); 130 } 131 132 mImapPasswordText = UiUtilities.getView(view, R.id.imap_password); 133 mRegularPasswordText = UiUtilities.getView(view, R.id.regular_password); 134 mOAuthGroup = UiUtilities.getView(view, R.id.oauth_group); 135 mOAuthButton = UiUtilities.getView(view, R.id.sign_in_with_oauth); 136 mOAuthButton.setOnClickListener(this); 137 mClientCertificateSelector = UiUtilities.getView(view, R.id.client_certificate_selector); 138 mDeviceIdSection = UiUtilities.getView(view, R.id.device_id_section); 139 mDeviceId = UiUtilities.getView(view, R.id.device_id); 140 mPasswordWarningLabel = UiUtilities.getView(view, R.id.wrong_password_warning_label); 141 mEmailConfirmationLabel = UiUtilities.getView(view, R.id.email_confirmation_label); 142 mEmailConfirmation = UiUtilities.getView(view, R.id.email_confirmation); 143 144 mClientCertificateSelector.setHostCallback(this); 145 mClientCertificateSelector.setCertificate(getArguments().getString(EXTRA_CLIENT_CERT)); 146 147 // After any text edits, call validateFields() which enables or disables the Next button 148 mValidationTextWatcher = new TextWatcher() { 149 @Override 150 public void afterTextChanged(Editable s) { 151 validatePassword(); 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 mImapPasswordText.addTextChangedListener(mValidationTextWatcher); 160 mRegularPasswordText.addTextChangedListener(mValidationTextWatcher); 161 162 return view; 163 } 164 165 @Override onActivityCreated(final Bundle savedInstanceState)166 public void onActivityCreated(final Bundle savedInstanceState) { 167 super.onActivityCreated(savedInstanceState); 168 169 mAppContext = getActivity().getApplicationContext(); 170 mEmailAddress = getArguments().getString(EXTRA_EMAIL); 171 final String protocol = getArguments().getString(EXTRA_PROTOCOL); 172 mOauthProviders = AccountSettingsUtils.getAllOAuthProviders(mAppContext); 173 mOfferCerts = true; 174 if (protocol != null) { 175 final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mAppContext, protocol); 176 if (info != null) { 177 if (mOauthProviders.size() > 0) { 178 mOfferOAuth = info.offerOAuth; 179 } 180 mOfferCerts = info.offerCerts; 181 } 182 } else { 183 // For now, we might not know what protocol we're using, so just default to 184 // offering oauth 185 if (mOauthProviders.size() > 0) { 186 mOfferOAuth = true; 187 } 188 } 189 // We may want to disable OAuth during the new account setup flow, but allow it elsewhere 190 final boolean standalone = getArguments().getBoolean(EXTRA_STANDALONE); 191 final boolean skipOAuth = !standalone && 192 getActivity().getResources().getBoolean(R.bool.skip_oauth_on_setup); 193 mOfferOAuth = mOfferOAuth && !skipOAuth; 194 195 mOAuthGroup.setVisibility(mOfferOAuth ? View.VISIBLE : View.GONE); 196 mRegularPasswordText.setVisibility(mOfferOAuth ? View.GONE : View.VISIBLE); 197 198 if (mOfferCerts) { 199 // TODO: Here we always offer certificates for any protocol that allows them (i.e. 200 // Exchange). But they will really only be available if we are using SSL security. 201 // Trouble is, first time through here, we haven't offered the user the choice of 202 // which security type to use. 203 mClientCertificateSelector.setVisibility(mOfferCerts ? View.VISIBLE : View.GONE); 204 mDeviceIdSection.setVisibility(mOfferCerts ? View.VISIBLE : View.GONE); 205 String deviceId = ""; 206 try { 207 deviceId = Device.getDeviceId(getActivity()); 208 } catch (IOException e) { 209 // Not required 210 } 211 mDeviceId.setText(deviceId); 212 } 213 final boolean passwordFailed = getArguments().getBoolean(EXTRA_PASSWORD_FAILED, false); 214 setPasswordFailed(passwordFailed); 215 validatePassword(); 216 } 217 218 @Override onDestroy()219 public void onDestroy() { 220 super.onDestroy(); 221 if (mImapPasswordText != null) { 222 mImapPasswordText.removeTextChangedListener(mValidationTextWatcher); 223 mImapPasswordText = null; 224 } 225 if (mRegularPasswordText != null) { 226 mRegularPasswordText.removeTextChangedListener(mValidationTextWatcher); 227 mRegularPasswordText = null; 228 } 229 } 230 setPasswordFailed(final boolean failed)231 public void setPasswordFailed(final boolean failed) { 232 if (failed) { 233 mPasswordWarningLabel.setVisibility(View.VISIBLE); 234 mEmailConfirmationLabel.setVisibility(View.VISIBLE); 235 mEmailConfirmation.setVisibility(View.VISIBLE); 236 mEmailConfirmation.setText(mEmailAddress); 237 } else { 238 mPasswordWarningLabel.setVisibility(View.GONE); 239 mEmailConfirmationLabel.setVisibility(View.GONE); 240 mEmailConfirmation.setVisibility(View.GONE); 241 } 242 } 243 validatePassword()244 public void validatePassword() { 245 setNextButtonEnabled(!TextUtils.isEmpty(getPassword())); 246 // Warn (but don't prevent) if password has leading/trailing spaces 247 AccountSettingsUtils.checkPasswordSpaces(mAppContext, mImapPasswordText); 248 AccountSettingsUtils.checkPasswordSpaces(mAppContext, mRegularPasswordText); 249 } 250 251 @Override onActivityResult(final int requestCode, final int resultCode, final Intent data)252 public void onActivityResult(final int requestCode, final int resultCode, final Intent data) { 253 if (requestCode == CERTIFICATE_REQUEST) { 254 if (resultCode == Activity.RESULT_OK) { 255 final String certAlias = data.getStringExtra(CertificateRequestor.RESULT_ALIAS); 256 if (certAlias != null) { 257 mClientCertificateSelector.setCertificate(certAlias); 258 } 259 } else { 260 LogUtils.e(LogUtils.TAG, "Unknown result from certificate request %d", 261 resultCode); 262 } 263 } else if (requestCode == OAuthAuthenticationActivity.REQUEST_OAUTH) { 264 if (resultCode == OAuthAuthenticationActivity.RESULT_OAUTH_SUCCESS) { 265 final String accessToken = data.getStringExtra( 266 OAuthAuthenticationActivity.EXTRA_OAUTH_ACCESS_TOKEN); 267 final String refreshToken = data.getStringExtra( 268 OAuthAuthenticationActivity.EXTRA_OAUTH_REFRESH_TOKEN); 269 final int expiresInSeconds = data.getIntExtra( 270 OAuthAuthenticationActivity.EXTRA_OAUTH_EXPIRES_IN, 0); 271 final Bundle results = new Bundle(4); 272 results.putString(EXTRA_OAUTH_PROVIDER, mProviderId); 273 results.putString(EXTRA_OAUTH_ACCESS_TOKEN, accessToken); 274 results.putString(EXTRA_OAUTH_REFRESH_TOKEN, refreshToken); 275 results.putInt(EXTRA_OAUTH_EXPIRES_IN_SECONDS, expiresInSeconds); 276 mResults = results; 277 final Callback callback = (Callback) getActivity(); 278 callback.onCredentialsComplete(results); 279 } else if (resultCode == OAuthAuthenticationActivity.RESULT_OAUTH_FAILURE 280 || resultCode == OAuthAuthenticationActivity.RESULT_OAUTH_USER_CANCELED) { 281 LogUtils.i(LogUtils.TAG, "Result from oauth %d", resultCode); 282 } else { 283 LogUtils.wtf(LogUtils.TAG, "Unknown result code from OAUTH: %d", resultCode); 284 } 285 } else { 286 LogUtils.e(LogUtils.TAG, "Unknown request code for onActivityResult in" 287 + " AccountSetupBasics: %d", requestCode); 288 } 289 } 290 291 @Override onClick(final View view)292 public void onClick(final View view) { 293 final int viewId = view.getId(); 294 if (viewId == R.id.sign_in_with_oauth) { 295 // TODO currently the only oauth provider we support is google. 296 // If we ever have more than 1 oauth provider, then we need to implement some sort 297 // of picker UI. For now, just always take the first oauth provider. 298 if (mOauthProviders.size() > 0) { 299 mProviderId = mOauthProviders.get(0).id; 300 final Intent i = new Intent(getActivity(), OAuthAuthenticationActivity.class); 301 i.putExtra(OAuthAuthenticationActivity.EXTRA_EMAIL_ADDRESS, mEmailAddress); 302 i.putExtra(OAuthAuthenticationActivity.EXTRA_PROVIDER, mProviderId); 303 startActivityForResult(i, OAuthAuthenticationActivity.REQUEST_OAUTH); 304 } 305 } else if (viewId == R.id.done) { 306 final Callback callback = (Callback) getActivity(); 307 callback.onNextButton(); 308 } else if (viewId == R.id.cancel) { 309 final Callback callback = (Callback) getActivity(); 310 callback.onBackPressed(); 311 } else { 312 super.onClick(view); 313 } 314 } 315 getPassword()316 public String getPassword() { 317 if (mOfferOAuth) { 318 return mImapPasswordText.getText().toString(); 319 } else { 320 return mRegularPasswordText.getText().toString(); 321 } 322 } 323 getCredentialResults()324 public Bundle getCredentialResults() { 325 if (mResults != null) { 326 return mResults; 327 } 328 329 final Bundle results = new Bundle(2); 330 results.putString(EXTRA_PASSWORD, getPassword()); 331 results.putString(EXTRA_CLIENT_CERT, getClientCertificate()); 332 return results; 333 } 334 populateHostAuthWithResults(final Context context, final HostAuth hostAuth, final Bundle results)335 public static void populateHostAuthWithResults(final Context context, final HostAuth hostAuth, 336 final Bundle results) { 337 if (results == null) { 338 return; 339 } 340 final String password = results.getString(AccountSetupCredentialsFragment.EXTRA_PASSWORD); 341 if (!TextUtils.isEmpty(password)) { 342 hostAuth.mPassword = password; 343 hostAuth.removeCredential(); 344 } else { 345 Credential cred = hostAuth.getOrCreateCredential(context); 346 cred.mProviderId = results.getString( 347 AccountSetupCredentialsFragment.EXTRA_OAUTH_PROVIDER); 348 cred.mAccessToken = results.getString( 349 AccountSetupCredentialsFragment.EXTRA_OAUTH_ACCESS_TOKEN); 350 cred.mRefreshToken = results.getString( 351 AccountSetupCredentialsFragment.EXTRA_OAUTH_REFRESH_TOKEN); 352 cred.mExpiration = System.currentTimeMillis() 353 + results.getInt( 354 AccountSetupCredentialsFragment.EXTRA_OAUTH_EXPIRES_IN_SECONDS, 0) 355 * DateUtils.SECOND_IN_MILLIS; 356 hostAuth.mPassword = null; 357 } 358 hostAuth.mClientCertAlias = results.getString(EXTRA_CLIENT_CERT); 359 } 360 getClientCertificate()361 public String getClientCertificate() { 362 return mClientCertificateSelector.getCertificate(); 363 } 364 365 @Override onCertificateRequested()366 public void onCertificateRequested() { 367 final Intent intent = new Intent(getString(R.string.intent_exchange_cert_action)); 368 intent.setData(CertificateRequestor.CERTIFICATE_REQUEST_URI); 369 // We don't set EXTRA_HOST or EXTRA_PORT here because we don't know the final host/port 370 // that we're connecting to yet, and autodiscover might point us somewhere else 371 startActivityForResult(intent, CERTIFICATE_REQUEST); 372 } 373 } 374