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 PasswordTextWatcher();
149         mImapPasswordText.addTextChangedListener(mValidationTextWatcher);
150         mRegularPasswordText.addTextChangedListener(mValidationTextWatcher);
151 
152         return view;
153     }
154 
155     private class PasswordTextWatcher implements TextWatcher {
156         @Override
afterTextChanged(Editable s)157         public void afterTextChanged(Editable s) {
158             validatePassword();
159         }
160 
161         @Override
beforeTextChanged(CharSequence s, int start, int count, int after)162         public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
163         @Override
onTextChanged(CharSequence s, int start, int before, int count)164         public void onTextChanged(CharSequence s, int start, int before, int count) { }
165     }
166 
167     @Override
onActivityCreated(final Bundle savedInstanceState)168     public void onActivityCreated(final Bundle savedInstanceState) {
169         super.onActivityCreated(savedInstanceState);
170 
171         mAppContext = getActivity().getApplicationContext();
172         mEmailAddress = getArguments().getString(EXTRA_EMAIL);
173         final String protocol = getArguments().getString(EXTRA_PROTOCOL);
174         mOauthProviders = AccountSettingsUtils.getAllOAuthProviders(mAppContext);
175         mOfferCerts = true;
176         if (protocol != null) {
177             final EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mAppContext, protocol);
178             if (info != null) {
179                 if (mOauthProviders.size() > 0) {
180                     mOfferOAuth = info.offerOAuth;
181                 }
182                 mOfferCerts = info.offerCerts;
183             }
184         } else {
185             // For now, we might not know what protocol we're using, so just default to
186             // offering oauth
187             if (mOauthProviders.size() > 0) {
188                 mOfferOAuth = true;
189             }
190         }
191         // We may want to disable OAuth during the new account setup flow, but allow it elsewhere
192         final boolean standalone = getArguments().getBoolean(EXTRA_STANDALONE);
193         final boolean skipOAuth = !standalone &&
194                 getActivity().getResources().getBoolean(R.bool.skip_oauth_on_setup);
195         mOfferOAuth = mOfferOAuth && !skipOAuth;
196 
197         mOAuthGroup.setVisibility(mOfferOAuth ? View.VISIBLE : View.GONE);
198         mRegularPasswordText.setVisibility(mOfferOAuth ? View.GONE : View.VISIBLE);
199 
200         if (mOfferCerts) {
201             // TODO: Here we always offer certificates for any protocol that allows them (i.e.
202             // Exchange). But they will really only be available if we are using SSL security.
203             // Trouble is, first time through here, we haven't offered the user the choice of
204             // which security type to use.
205             mClientCertificateSelector.setVisibility(mOfferCerts ? View.VISIBLE : View.GONE);
206             mDeviceIdSection.setVisibility(mOfferCerts ? View.VISIBLE : View.GONE);
207             String deviceId = "";
208             try {
209                 deviceId = Device.getDeviceId(getActivity());
210             } catch (IOException e) {
211                 // Not required
212             }
213             mDeviceId.setText(deviceId);
214         }
215         final boolean passwordFailed = getArguments().getBoolean(EXTRA_PASSWORD_FAILED, false);
216         setPasswordFailed(passwordFailed);
217         validatePassword();
218     }
219 
220     @Override
onDestroy()221     public void onDestroy() {
222         super.onDestroy();
223         if (mImapPasswordText != null) {
224             mImapPasswordText.removeTextChangedListener(mValidationTextWatcher);
225             mImapPasswordText = null;
226         }
227         if (mRegularPasswordText != null) {
228             mRegularPasswordText.removeTextChangedListener(mValidationTextWatcher);
229             mRegularPasswordText = null;
230         }
231     }
232 
setPasswordFailed(final boolean failed)233     public void setPasswordFailed(final boolean failed) {
234         if (failed) {
235             mPasswordWarningLabel.setVisibility(View.VISIBLE);
236             mEmailConfirmationLabel.setVisibility(View.VISIBLE);
237             mEmailConfirmation.setVisibility(View.VISIBLE);
238             mEmailConfirmation.setText(mEmailAddress);
239         } else {
240             mPasswordWarningLabel.setVisibility(View.GONE);
241             mEmailConfirmationLabel.setVisibility(View.GONE);
242             mEmailConfirmation.setVisibility(View.GONE);
243         }
244     }
245 
validatePassword()246     public void validatePassword() {
247         setNextButtonEnabled(!TextUtils.isEmpty(getPassword()));
248     }
249 
250     @Override
onActivityResult(final int requestCode, final int resultCode, final Intent data)251     public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
252         if (requestCode == CERTIFICATE_REQUEST) {
253             if (resultCode == Activity.RESULT_OK) {
254                 final String certAlias = data.getStringExtra(CertificateRequestor.RESULT_ALIAS);
255                 if (certAlias != null) {
256                     mClientCertificateSelector.setCertificate(certAlias);
257                 }
258             } else {
259                 LogUtils.e(LogUtils.TAG, "Unknown result from certificate request %d",
260                         resultCode);
261             }
262         } else if (requestCode == OAuthAuthenticationActivity.REQUEST_OAUTH) {
263             if (resultCode == OAuthAuthenticationActivity.RESULT_OAUTH_SUCCESS) {
264                 final String accessToken = data.getStringExtra(
265                         OAuthAuthenticationActivity.EXTRA_OAUTH_ACCESS_TOKEN);
266                 final String refreshToken = data.getStringExtra(
267                         OAuthAuthenticationActivity.EXTRA_OAUTH_REFRESH_TOKEN);
268                 final int expiresInSeconds = data.getIntExtra(
269                         OAuthAuthenticationActivity.EXTRA_OAUTH_EXPIRES_IN, 0);
270                 final Bundle results = new Bundle(4);
271                 results.putString(EXTRA_OAUTH_PROVIDER, mProviderId);
272                 results.putString(EXTRA_OAUTH_ACCESS_TOKEN, accessToken);
273                 results.putString(EXTRA_OAUTH_REFRESH_TOKEN, refreshToken);
274                 results.putInt(EXTRA_OAUTH_EXPIRES_IN_SECONDS, expiresInSeconds);
275                 mResults = results;
276                 final Callback callback = (Callback) getActivity();
277                 callback.onCredentialsComplete(results);
278             } else if (resultCode == OAuthAuthenticationActivity.RESULT_OAUTH_FAILURE
279                     || resultCode == OAuthAuthenticationActivity.RESULT_OAUTH_USER_CANCELED) {
280                 LogUtils.i(LogUtils.TAG, "Result from oauth %d", resultCode);
281             } else {
282                 LogUtils.wtf(LogUtils.TAG, "Unknown result code from OAUTH: %d", resultCode);
283             }
284         } else {
285             LogUtils.e(LogUtils.TAG, "Unknown request code for onActivityResult in"
286                     + " AccountSetupBasics: %d", requestCode);
287         }
288     }
289 
290     @Override
onClick(final View view)291     public void onClick(final View view) {
292         final int viewId = view.getId();
293         if (viewId == R.id.sign_in_with_oauth) {
294             // TODO currently the only oauth provider we support is google.
295             // If we ever have more than 1 oauth provider, then we need to implement some sort
296             // of picker UI. For now, just always take the first oauth provider.
297             if (mOauthProviders.size() > 0) {
298                 mProviderId = mOauthProviders.get(0).id;
299                 final Intent i = new Intent(getActivity(), OAuthAuthenticationActivity.class);
300                 i.putExtra(OAuthAuthenticationActivity.EXTRA_EMAIL_ADDRESS, mEmailAddress);
301                 i.putExtra(OAuthAuthenticationActivity.EXTRA_PROVIDER, mProviderId);
302                 startActivityForResult(i, OAuthAuthenticationActivity.REQUEST_OAUTH);
303             }
304         } else if (viewId == R.id.done) {
305             final Callback callback = (Callback) getActivity();
306             callback.onNextButton();
307         } else if (viewId == R.id.cancel) {
308             final Callback callback = (Callback) getActivity();
309             callback.onBackPressed();
310         } else {
311             super.onClick(view);
312         }
313     }
314 
getPassword()315     public String getPassword() {
316         if (mOfferOAuth) {
317             return mImapPasswordText.getText().toString();
318         } else {
319             return mRegularPasswordText.getText().toString();
320         }
321     }
322 
getCredentialResults()323     public Bundle getCredentialResults() {
324         if (mResults != null) {
325             return mResults;
326         }
327 
328         final Bundle results = new Bundle(2);
329         results.putString(EXTRA_PASSWORD, getPassword());
330         results.putString(EXTRA_CLIENT_CERT, getClientCertificate());
331         return results;
332     }
333 
populateHostAuthWithResults(final Context context, final HostAuth hostAuth, final Bundle results)334     public static void populateHostAuthWithResults(final Context context, final HostAuth hostAuth,
335             final Bundle results) {
336         if (results == null) {
337             return;
338         }
339         final String password = results.getString(AccountSetupCredentialsFragment.EXTRA_PASSWORD);
340         if (!TextUtils.isEmpty(password)) {
341             hostAuth.mPassword = password;
342             hostAuth.removeCredential();
343         } else {
344             Credential cred = hostAuth.getOrCreateCredential(context);
345             cred.mProviderId = results.getString(
346                     AccountSetupCredentialsFragment.EXTRA_OAUTH_PROVIDER);
347             cred.mAccessToken = results.getString(
348                     AccountSetupCredentialsFragment.EXTRA_OAUTH_ACCESS_TOKEN);
349             cred.mRefreshToken = results.getString(
350                     AccountSetupCredentialsFragment.EXTRA_OAUTH_REFRESH_TOKEN);
351             cred.mExpiration = System.currentTimeMillis()
352                     + results.getInt(
353                     AccountSetupCredentialsFragment.EXTRA_OAUTH_EXPIRES_IN_SECONDS, 0)
354                     * DateUtils.SECOND_IN_MILLIS;
355             hostAuth.mPassword = null;
356         }
357         hostAuth.mClientCertAlias = results.getString(EXTRA_CLIENT_CERT);
358     }
359 
getClientCertificate()360     public String getClientCertificate() {
361         return mClientCertificateSelector.getCertificate();
362     }
363 
364     @Override
onCertificateRequested()365     public void onCertificateRequested() {
366         final Intent intent = new Intent(getString(R.string.intent_exchange_cert_action));
367         intent.setData(CertificateRequestor.CERTIFICATE_REQUEST_URI);
368         // We don't set EXTRA_HOST or EXTRA_PORT here because we don't know the final host/port
369         // that we're connecting to yet, and autodiscover might point us somewhere else
370         startActivityForResult(intent, CERTIFICATE_REQUEST);
371     }
372 }
373