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 */
16package com.android.email.activity.setup;
17
18import android.app.Activity;
19import android.content.Context;
20import android.content.Intent;
21import android.os.Bundle;
22import android.text.Editable;
23import android.text.TextUtils;
24import android.text.TextWatcher;
25import android.text.format.DateUtils;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.View.OnClickListener;
29import android.view.ViewGroup;
30import android.widget.EditText;
31import android.widget.TextView;
32
33import com.android.email.R;
34import com.android.email.activity.UiUtilities;
35import com.android.email.service.EmailServiceUtils;
36import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
37import com.android.email.view.CertificateSelector;
38import com.android.email.view.CertificateSelector.HostCallback;
39import com.android.emailcommon.Device;
40import com.android.emailcommon.VendorPolicyLoader.OAuthProvider;
41import com.android.emailcommon.provider.Credential;
42import com.android.emailcommon.provider.HostAuth;
43import com.android.emailcommon.utility.CertificateRequestor;
44import com.android.mail.utils.LogUtils;
45
46import java.io.IOException;
47import java.util.List;
48
49public 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 {
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     */
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
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
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
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
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
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
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
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
316    public String getPassword() {
317        if (mOfferOAuth) {
318            return mImapPasswordText.getText().toString();
319        } else {
320            return mRegularPasswordText.getText().toString();
321        }
322    }
323
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
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
361    public String getClientCertificate() {
362        return mClientCertificateSelector.getCertificate();
363    }
364
365    @Override
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