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 PasswordTextWatcher();
149        mImapPasswordText.addTextChangedListener(mValidationTextWatcher);
150        mRegularPasswordText.addTextChangedListener(mValidationTextWatcher);
151
152        return view;
153    }
154
155    private class PasswordTextWatcher implements TextWatcher {
156        @Override
157        public void afterTextChanged(Editable s) {
158            validatePassword();
159        }
160
161        @Override
162        public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
163        @Override
164        public void onTextChanged(CharSequence s, int start, int before, int count) { }
165    }
166
167    @Override
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
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
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
246    public void validatePassword() {
247        setNextButtonEnabled(!TextUtils.isEmpty(getPassword()));
248    }
249
250    @Override
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
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
315    public String getPassword() {
316        if (mOfferOAuth) {
317            return mImapPasswordText.getText().toString();
318        } else {
319            return mRegularPasswordText.getText().toString();
320        }
321    }
322
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
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
360    public String getClientCertificate() {
361        return mClientCertificateSelector.getCertificate();
362    }
363
364    @Override
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