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