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.net.Uri;
23import android.os.Bundle;
24import android.os.RemoteException;
25import android.text.Editable;
26import android.text.TextWatcher;
27import android.util.Log;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.CheckBox;
32import android.widget.CompoundButton;
33import android.widget.CompoundButton.OnCheckedChangeListener;
34import android.widget.EditText;
35import android.widget.TextView;
36
37import com.android.email.Email;
38import com.android.email.R;
39import com.android.email.activity.UiUtilities;
40import com.android.email.provider.AccountBackupRestore;
41import com.android.email.service.EmailServiceUtils;
42import com.android.email.view.CertificateSelector;
43import com.android.email.view.CertificateSelector.HostCallback;
44import com.android.emailcommon.Device;
45import com.android.emailcommon.Logging;
46import com.android.emailcommon.provider.Account;
47import com.android.emailcommon.provider.HostAuth;
48import com.android.emailcommon.utility.CertificateRequestor;
49import com.android.emailcommon.utility.Utility;
50
51import java.io.IOException;
52
53/**
54 * Provides generic setup for Exchange accounts.
55 *
56 * This fragment is used by AccountSetupExchange (for creating accounts) and by AccountSettingsXL
57 * (for editing existing accounts).
58 */
59public class AccountSetupExchangeFragment extends AccountServerBaseFragment
60        implements OnCheckedChangeListener, HostCallback {
61
62    private static final int CERTIFICATE_REQUEST = 0;
63    private final static String STATE_KEY_CREDENTIAL = "AccountSetupExchangeFragment.credential";
64    private final static String STATE_KEY_LOADED = "AccountSetupExchangeFragment.loaded";
65
66    private EditText mUsernameView;
67    private EditText mPasswordView;
68    private EditText mServerView;
69    private CheckBox mSslSecurityView;
70    private CheckBox mTrustCertificatesView;
71    private CertificateSelector mClientCertificateSelector;
72
73    // Support for lifecycle
74    private boolean mStarted;
75    /* package */ boolean mLoaded;
76    private String mCacheLoginCredential;
77
78    /**
79     * Called to do initial creation of a fragment.  This is called after
80     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
81     */
82    @Override
83    public void onCreate(Bundle savedInstanceState) {
84        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
85            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreate");
86        }
87        super.onCreate(savedInstanceState);
88
89        if (savedInstanceState != null) {
90            mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
91            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
92        }
93        mBaseScheme = HostAuth.SCHEME_EAS;
94    }
95
96    @Override
97    public View onCreateView(LayoutInflater inflater, ViewGroup container,
98            Bundle savedInstanceState) {
99        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
100            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onCreateView");
101        }
102        int layoutId = mSettingsMode
103                ? R.layout.account_settings_exchange_fragment
104                : R.layout.account_setup_exchange_fragment;
105
106        View view = inflater.inflate(layoutId, container, false);
107        final Context context = getActivity();
108
109        mUsernameView = UiUtilities.getView(view, R.id.account_username);
110        mPasswordView = UiUtilities.getView(view, R.id.account_password);
111        mServerView = UiUtilities.getView(view, R.id.account_server);
112        mSslSecurityView = UiUtilities.getView(view, R.id.account_ssl);
113        mSslSecurityView.setOnCheckedChangeListener(this);
114        mTrustCertificatesView = UiUtilities.getView(view, R.id.account_trust_certificates);
115        mClientCertificateSelector = UiUtilities.getView(view, R.id.client_certificate_selector);
116
117        // Calls validateFields() which enables or disables the Next button
118        // based on the fields' validity.
119        TextWatcher validationTextWatcher = new TextWatcher() {
120            public void afterTextChanged(Editable s) {
121                validateFields();
122            }
123
124            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
125            public void onTextChanged(CharSequence s, int start, int before, int count) { }
126        };
127        // We're editing an existing account; don't allow modification of the user name
128        if (mSettingsMode) {
129            makeTextViewUneditable(mUsernameView,
130                    getString(R.string.account_setup_username_uneditable_error));
131        }
132        mUsernameView.addTextChangedListener(validationTextWatcher);
133        mPasswordView.addTextChangedListener(validationTextWatcher);
134        mServerView.addTextChangedListener(validationTextWatcher);
135
136        EditText lastView = mServerView;
137        lastView.setOnEditorActionListener(mDismissImeOnDoneListener);
138
139        String deviceId = "";
140        try {
141            deviceId = Device.getDeviceId(context);
142        } catch (IOException e) {
143            // Not required
144        }
145        ((TextView) UiUtilities.getView(view, R.id.device_id)).setText(deviceId);
146
147        // Additional setup only used while in "settings" mode
148        onCreateViewSettingsMode(view);
149
150        return view;
151    }
152
153    @Override
154    public void onActivityCreated(Bundle savedInstanceState) {
155        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
156            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onActivityCreated");
157        }
158        super.onActivityCreated(savedInstanceState);
159        mClientCertificateSelector.setHostActivity(this);
160    }
161
162    /**
163     * Called when the Fragment is visible to the user.
164     */
165    @Override
166    public void onStart() {
167        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
168            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStart");
169        }
170        super.onStart();
171        mStarted = true;
172        loadSettings(SetupData.getAccount());
173    }
174
175    /**
176     * Called when the fragment is visible to the user and actively running.
177     */
178    @Override
179    public void onResume() {
180        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
181            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onResume");
182        }
183        super.onResume();
184        validateFields();
185    }
186
187    @Override
188    public void onPause() {
189        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
190            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onPause");
191        }
192        super.onPause();
193    }
194
195    /**
196     * Called when the Fragment is no longer started.
197     */
198    @Override
199    public void onStop() {
200        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
201            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onStop");
202        }
203        super.onStop();
204        mStarted = false;
205    }
206
207    /**
208     * Called when the fragment is no longer in use.
209     */
210    @Override
211    public void onDestroy() {
212        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
213            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onDestroy");
214        }
215        super.onDestroy();
216    }
217
218    @Override
219    public void onSaveInstanceState(Bundle outState) {
220        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
221            Log.d(Logging.LOG_TAG, "AccountSetupExchangeFragment onSaveInstanceState");
222        }
223        super.onSaveInstanceState(outState);
224
225        outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential);
226        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
227    }
228
229    /**
230     * Activity provides callbacks here.  This also triggers loading and setting up the UX
231     */
232    @Override
233    public void setCallback(Callback callback) {
234        super.setCallback(callback);
235        if (mStarted) {
236            loadSettings(SetupData.getAccount());
237        }
238    }
239
240    /**
241     * Force the given account settings to be loaded using {@link #loadSettings(Account)}.
242     *
243     * @return true if the loaded values pass validation
244     */
245    private boolean forceLoadSettings(Account account) {
246        mLoaded = false;
247        return loadSettings(account);
248    }
249
250    /**
251     * Load the given account settings into the UI and then ensure the settings are valid.
252     * As an optimization, if the settings have already been loaded, the UI will not be
253     * updated, but, the account fields will still be validated.
254     *
255     * @return true if the loaded values pass validation
256     */
257    /*package*/ boolean loadSettings(Account account) {
258        if (mLoaded) return validateFields();
259
260        HostAuth hostAuth = account.mHostAuthRecv;
261
262        String userName = hostAuth.mLogin;
263        if (userName != null) {
264            // Add a backslash to the start of the username, but only if the username has no
265            // backslash in it.
266            if (userName.indexOf('\\') < 0) {
267                userName = "\\" + userName;
268            }
269            mUsernameView.setText(userName);
270        }
271
272        if (hostAuth.mPassword != null) {
273            mPasswordView.setText(hostAuth.mPassword);
274            // Since username is uneditable, focus on the next editable field
275            if (mSettingsMode) {
276                mPasswordView.requestFocus();
277            }
278        }
279
280        String protocol = hostAuth.mProtocol;
281        if (protocol == null || !protocol.startsWith("eas")) {
282            throw new Error("Unknown account type: " + protocol);
283        }
284
285        if (hostAuth.mAddress != null) {
286            mServerView.setText(hostAuth.mAddress);
287        }
288
289        boolean ssl = 0 != (hostAuth.mFlags & HostAuth.FLAG_SSL);
290        boolean trustCertificates = 0 != (hostAuth.mFlags & HostAuth.FLAG_TRUST_ALL);
291        mSslSecurityView.setChecked(ssl);
292        mTrustCertificatesView.setChecked(trustCertificates);
293        if (hostAuth.mClientCertAlias != null) {
294            mClientCertificateSelector.setCertificate(hostAuth.mClientCertAlias);
295        }
296        onUseSslChanged(ssl);
297
298        mLoadedRecvAuth = hostAuth;
299        mLoaded = true;
300        return validateFields();
301    }
302
303    private boolean usernameFieldValid(EditText usernameView) {
304        return Utility.isTextViewNotEmpty(usernameView) &&
305            !usernameView.getText().toString().equals("\\");
306    }
307
308    /**
309     * Check the values in the fields and decide if it makes sense to enable the "next" button
310     * @return true if all fields are valid, false if any fields are incomplete
311     */
312    private boolean validateFields() {
313        if (!mLoaded) return false;
314        boolean enabled = usernameFieldValid(mUsernameView)
315                && Utility.isTextViewNotEmpty(mPasswordView)
316                && Utility.isServerNameValid(mServerView);
317        enableNextButton(enabled);
318
319        // Warn (but don't prevent) if password has leading/trailing spaces
320        AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView);
321
322        return enabled;
323    }
324
325    @Override
326    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
327        if (buttonView.getId() == R.id.account_ssl) {
328            onUseSslChanged(isChecked);
329        }
330    }
331
332    public void onUseSslChanged(boolean useSsl) {
333        int mode = useSsl ? View.VISIBLE : View.GONE;
334        mTrustCertificatesView.setVisibility(mode);
335        UiUtilities.setVisibilitySafe(getView(), R.id.account_trust_certificates_divider, mode);
336        mClientCertificateSelector.setVisibility(mode);
337        UiUtilities.setVisibilitySafe(getView(), R.id.client_certificate_divider, mode);
338    }
339
340    @Override
341    public void onCheckSettingsComplete(final int result) {
342        if (result == AccountCheckSettingsFragment.CHECK_SETTINGS_CLIENT_CERTIFICATE_NEEDED) {
343            mSslSecurityView.setChecked(true);
344            onCertificateRequested();
345            return;
346        }
347        super.onCheckSettingsComplete(result);
348    }
349
350
351    /**
352     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
353     * Blocking - do not call from UI Thread.
354     */
355    @Override
356    public void saveSettingsAfterEdit() {
357        Account account = SetupData.getAccount();
358        account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
359        account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
360        // For EAS, notify ExchangeService that the password has changed
361        try {
362            EmailServiceUtils.getExchangeService(mContext, null).hostChanged(account.mId);
363        } catch (RemoteException e) {
364            // Nothing to be done if this fails
365        }
366        // Update the backup (side copy) of the accounts
367        AccountBackupRestore.backup(mContext);
368    }
369
370    /**
371     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
372     */
373    @Override
374    public void saveSettingsAfterSetup() {
375    }
376
377    /**
378     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
379     */
380    public boolean setHostAuthFromAutodiscover(HostAuth newHostAuth) {
381        Account account = SetupData.getAccount();
382        account.mHostAuthSend = newHostAuth;
383        account.mHostAuthRecv = newHostAuth;
384        // Auto discovery may have changed the auth settings; force load them
385        return forceLoadSettings(account);
386    }
387
388    /**
389     * Implements AccountCheckSettingsFragment.Callbacks
390     */
391    @Override
392    public void onAutoDiscoverComplete(int result, HostAuth hostAuth) {
393        AccountSetupExchange activity = (AccountSetupExchange) getActivity();
394        activity.onAutoDiscoverComplete(result, hostAuth);
395    }
396
397    /**
398     * Entry point from Activity, when "next" button is clicked
399     */
400    @Override
401    public void onNext() {
402        Account account = SetupData.getAccount();
403
404        String userName = mUsernameView.getText().toString().trim();
405        if (userName.startsWith("\\")) {
406            userName = userName.substring(1);
407        }
408        mCacheLoginCredential = userName;
409        String userPassword = mPasswordView.getText().toString();
410
411        int flags = 0;
412        if (mSslSecurityView.isChecked()) {
413            flags |= HostAuth.FLAG_SSL;
414        }
415        if (mTrustCertificatesView.isChecked()) {
416            flags |= HostAuth.FLAG_TRUST_ALL;
417        }
418        String certAlias = mClientCertificateSelector.getCertificate();
419        String serverAddress = mServerView.getText().toString().trim();
420
421        int port = mSslSecurityView.isChecked() ? 443 : 80;
422        HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext);
423        sendAuth.setLogin(userName, userPassword);
424        sendAuth.setConnection(mBaseScheme, serverAddress, port, flags, certAlias);
425        sendAuth.mDomain = null;
426
427        HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
428        recvAuth.setLogin(userName, userPassword);
429        recvAuth.setConnection(mBaseScheme, serverAddress, port, flags, certAlias);
430        recvAuth.mDomain = null;
431
432        // Check for a duplicate account (requires async DB work) and if OK, proceed with check
433        startDuplicateTaskCheck(account.mId, serverAddress, mCacheLoginCredential,
434                SetupData.CHECK_INCOMING);
435    }
436
437    @Override
438    public void onCertificateRequested() {
439        Intent intent = new Intent(CertificateRequestor.ACTION_REQUEST_CERT);
440        intent.setData(Uri.parse("eas://com.android.emailcommon/certrequest"));
441        startActivityForResult(intent, CERTIFICATE_REQUEST);
442    }
443
444    @Override
445    public void onActivityResult(int requestCode, int resultCode, Intent data) {
446        if (requestCode == CERTIFICATE_REQUEST && resultCode == Activity.RESULT_OK) {
447            String certAlias = data.getStringExtra(CertificateRequestor.RESULT_ALIAS);
448            if (certAlias != null) {
449                mClientCertificateSelector.setCertificate(certAlias);
450            }
451        }
452    }
453
454}
455