AccountSetupExchangeFragment.java revision cc0185f07c9198008d8dc685ae9979f3e35e8539
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.text.Editable;
25import android.text.TextUtils;
26import android.text.TextWatcher;
27import android.text.method.DigitsKeyListener;
28import android.util.Log;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.inputmethod.EditorInfo;
33import android.widget.AdapterView;
34import android.widget.ArrayAdapter;
35import android.widget.EditText;
36import android.widget.Spinner;
37import android.widget.TextView;
38
39import com.android.email.R;
40import com.android.email.activity.UiUtilities;
41import com.android.email.provider.AccountBackupRestore;
42import com.android.email.service.EmailServiceUtils;
43import com.android.email.service.EmailServiceUtils.EmailServiceInfo;
44import com.android.email.view.CertificateSelector;
45import com.android.email.view.CertificateSelector.HostCallback;
46import com.android.email2.ui.MailActivityEmail;
47import com.android.emailcommon.Device;
48import com.android.emailcommon.Logging;
49import com.android.emailcommon.provider.Account;
50import com.android.emailcommon.provider.HostAuth;
51import com.android.emailcommon.utility.CertificateRequestor;
52import com.android.emailcommon.utility.Utility;
53
54import java.io.IOException;
55import java.util.ArrayList;
56
57/**
58 * Provides UI for IMAP/POP account settings.
59 *
60 * This fragment is used by AccountSetupIncoming (for creating accounts) and by AccountSettingsXL
61 * (for editing existing accounts).
62 */
63public class AccountSetupIncomingFragment extends AccountServerBaseFragment
64        implements HostCallback {
65
66    private static final int CERTIFICATE_REQUEST = 0;
67    private final static String STATE_KEY_CREDENTIAL = "AccountSetupIncomingFragment.credential";
68    private final static String STATE_KEY_LOADED = "AccountSetupIncomingFragment.loaded";
69
70    private EditText mUsernameView;
71    private EditText mPasswordView;
72    private TextView mServerLabelView;
73    private EditText mServerView;
74    private EditText mPortView;
75    private Spinner mSecurityTypeView;
76    private TextView mDeletePolicyLabelView;
77    private Spinner mDeletePolicyView;
78    private View mImapPathPrefixSectionView;
79    private View mDeviceIdSectionView;
80    private EditText mImapPathPrefixView;
81    private CertificateSelector mClientCertificateSelector;
82    // Delete policy as loaded from the device
83    private int mLoadedDeletePolicy;
84
85    // Support for lifecycle
86    private boolean mStarted;
87    private boolean mLoaded;
88    private String mCacheLoginCredential;
89    private HostAuth mRecvAuth;
90    private EmailServiceInfo mServiceInfo;
91
92    /**
93     * Called to do initial creation of a fragment.  This is called after
94     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
95     */
96    @Override
97    public void onCreate(Bundle savedInstanceState) {
98        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
99            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate");
100        }
101        super.onCreate(savedInstanceState);
102
103        if (savedInstanceState != null) {
104            mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
105            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
106        }
107    }
108
109    @Override
110    public View onCreateView(LayoutInflater inflater, ViewGroup container,
111            Bundle savedInstanceState) {
112        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
113            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView");
114        }
115        int layoutId = mSettingsMode
116                ? R.layout.account_settings_incoming_fragment
117                : R.layout.account_setup_incoming_fragment;
118
119        View view = inflater.inflate(layoutId, container, false);
120        Context context = getActivity();
121
122        mRecvAuth = SetupData.getAccount().mHostAuthRecv;
123        mServiceInfo = EmailServiceUtils.getServiceInfo(mContext, mRecvAuth.mProtocol);
124
125        mUsernameView = (EditText) UiUtilities.getView(view, R.id.account_username);
126        mPasswordView = (EditText) UiUtilities.getView(view, R.id.account_password);
127        mServerLabelView = (TextView) UiUtilities.getView(view, R.id.account_server_label);
128        mServerView = (EditText) UiUtilities.getView(view, R.id.account_server);
129        mPortView = (EditText) UiUtilities.getView(view, R.id.account_port);
130        mSecurityTypeView = (Spinner) UiUtilities.getView(view, R.id.account_security_type);
131        mDeletePolicyLabelView = (TextView) UiUtilities.getView(view,
132                R.id.account_delete_policy_label);
133        mDeletePolicyView = (Spinner) UiUtilities.getView(view, R.id.account_delete_policy);
134        mImapPathPrefixSectionView = UiUtilities.getView(view, R.id.imap_path_prefix_section);
135        mDeviceIdSectionView = UiUtilities.getView(view, R.id.device_id_section);
136        mImapPathPrefixView = (EditText) UiUtilities.getView(view, R.id.imap_path_prefix);
137        mClientCertificateSelector = UiUtilities.getView(view, R.id.client_certificate_selector);
138
139        // Set up security type spinner
140        ArrayList<SpinnerOption> securityTypes = new ArrayList<SpinnerOption>();
141        securityTypes.add(
142                new SpinnerOption(HostAuth.FLAG_NONE, context.getString(
143                        R.string.account_setup_incoming_security_none_label)));
144        securityTypes.add(
145                new SpinnerOption(HostAuth.FLAG_SSL, context.getString(
146                        R.string.account_setup_incoming_security_ssl_label)));
147        securityTypes.add(
148                new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString(
149                        R.string.account_setup_incoming_security_ssl_trust_certificates_label)));
150        if (mServiceInfo.offerTls) {
151            securityTypes.add(
152                    new SpinnerOption(HostAuth.FLAG_TLS, context.getString(
153                            R.string.account_setup_incoming_security_tls_label)));
154            securityTypes.add(
155                    new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL,
156                        context.getString(
157                           R.string.account_setup_incoming_security_tls_trust_certificates_label)));
158        }
159        ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(
160                context, android.R.layout.simple_spinner_item, securityTypes);
161        securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
162        mSecurityTypeView.setAdapter(securityTypesAdapter);
163
164        if (mServiceInfo.offerLocalDeletes) {
165            SpinnerOption deletePolicies[] = {
166                    new SpinnerOption(Account.DELETE_POLICY_NEVER,
167                            context.getString(
168                                    R.string.account_setup_incoming_delete_policy_never_label)),
169                    new SpinnerOption(Account.DELETE_POLICY_ON_DELETE,
170                            context.getString(
171                                    R.string.account_setup_incoming_delete_policy_delete_label)),
172            };
173            ArrayAdapter<SpinnerOption> deletePoliciesAdapter =
174                    new ArrayAdapter<SpinnerOption>(context,
175                            android.R.layout.simple_spinner_item, deletePolicies);
176            deletePoliciesAdapter.setDropDownViewResource(
177                    android.R.layout.simple_spinner_dropdown_item);
178            mDeletePolicyView.setAdapter(deletePoliciesAdapter);
179        }
180
181        // Updates the port when the user changes the security type. This allows
182        // us to show a reasonable default which the user can change.
183        mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
184            public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
185                updatePortFromSecurityType();
186            }
187
188            public void onNothingSelected(AdapterView<?> arg0) { }
189        });
190
191        // After any text edits, call validateFields() which enables or disables the Next button
192        TextWatcher validationTextWatcher = new TextWatcher() {
193            public void afterTextChanged(Editable s) {
194                validateFields();
195            }
196
197            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
198            public void onTextChanged(CharSequence s, int start, int before, int count) { }
199        };
200        // We're editing an existing account; don't allow modification of the user name
201        if (mSettingsMode) {
202            makeTextViewUneditable(mUsernameView,
203                    getString(R.string.account_setup_username_uneditable_error));
204        }
205        mUsernameView.addTextChangedListener(validationTextWatcher);
206        mPasswordView.addTextChangedListener(validationTextWatcher);
207        mServerView.addTextChangedListener(validationTextWatcher);
208        mPortView.addTextChangedListener(validationTextWatcher);
209
210        // Only allow digits in the port field.
211        mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
212
213        // Additional setup only used while in "settings" mode
214        onCreateViewSettingsMode(view);
215
216        return view;
217    }
218
219    @Override
220    public void onActivityCreated(Bundle savedInstanceState) {
221        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
222            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated");
223        }
224        super.onActivityCreated(savedInstanceState);
225        mClientCertificateSelector.setHostActivity(this);
226    }
227
228    /**
229     * Called when the Fragment is visible to the user.
230     */
231    @Override
232    public void onStart() {
233        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
234            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart");
235        }
236        super.onStart();
237        mStarted = true;
238        configureEditor();
239        loadSettings();
240    }
241
242    /**
243     * Called when the fragment is visible to the user and actively running.
244     */
245    @Override
246    public void onResume() {
247        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
248            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume");
249        }
250        super.onResume();
251        validateFields();
252    }
253
254    @Override
255    public void onPause() {
256        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
257            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause");
258        }
259        super.onPause();
260    }
261
262    /**
263     * Called when the Fragment is no longer started.
264     */
265    @Override
266    public void onStop() {
267        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
268            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop");
269        }
270        super.onStop();
271        mStarted = false;
272    }
273
274    /**
275     * Called when the fragment is no longer in use.
276     */
277    @Override
278    public void onDestroy() {
279        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
280            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy");
281        }
282        super.onDestroy();
283    }
284
285    @Override
286    public void onSaveInstanceState(Bundle outState) {
287        if (Logging.DEBUG_LIFECYCLE && MailActivityEmail.DEBUG) {
288            Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState");
289        }
290        super.onSaveInstanceState(outState);
291
292        outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential);
293        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
294    }
295
296    /**
297     * Activity provides callbacks here.  This also triggers loading and setting up the UX
298     */
299    @Override
300    public void setCallback(Callback callback) {
301        super.setCallback(callback);
302        if (mStarted) {
303            configureEditor();
304            loadSettings();
305        }
306    }
307
308    /**
309     * Configure the editor for the account type
310     */
311    private void configureEditor() {
312        Account account = SetupData.getAccount();
313        if (account == null) {
314            return;
315        }
316        TextView lastView = mImapPathPrefixView;
317        mBaseScheme = account.mHostAuthRecv.mProtocol;
318        mServerLabelView.setText(R.string.account_setup_incoming_server_label);
319        mServerView.setContentDescription(getResources().getText(
320                R.string.account_setup_incoming_server_label));
321        if (!mServiceInfo.offerPrefix) {
322            mImapPathPrefixSectionView.setVisibility(View.GONE);
323            lastView = mPortView;
324        }
325        if (!mServiceInfo.offerLocalDeletes) {
326            mDeletePolicyLabelView.setVisibility(View.GONE);
327            mDeletePolicyView.setVisibility(View.GONE);
328            mPortView.setImeOptions(EditorInfo.IME_ACTION_NEXT);
329        }
330        lastView.setOnEditorActionListener(mDismissImeOnDoneListener);
331    }
332
333    /**
334     * Load the current settings into the UI
335     */
336    private void loadSettings() {
337        if (mLoaded) return;
338
339        Account account = SetupData.getAccount();
340        HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
341
342        String username = recvAuth.mLogin;
343        if (username != null) {
344            //*** For eas?
345            // Add a backslash to the start of the username, but only if the username has no
346            // backslash in it.
347            //if (userName.indexOf('\\') < 0) {
348            //    userName = "\\" + userName;
349            //}
350            mUsernameView.setText(username);
351        }
352        String password = recvAuth.mPassword;
353        if (password != null) {
354            mPasswordView.setText(password);
355            // Since username is uneditable, focus on the next editable field
356            if (mSettingsMode) {
357                mPasswordView.requestFocus();
358            }
359        }
360
361        if (mServiceInfo.offerPrefix) {
362            String prefix = recvAuth.mDomain;
363            if (prefix != null && prefix.length() > 0) {
364                mImapPathPrefixView.setText(prefix.substring(1));
365            }
366        }
367
368        // The delete policy is set for all legacy accounts. For POP3 accounts, the user sets
369        // the policy explicitly. For IMAP accounts, the policy is set when the Account object
370        // is created. @see AccountSetupBasics#populateSetupData
371        mLoadedDeletePolicy = account.getDeletePolicy();
372        SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy);
373
374        int flags = recvAuth.mFlags;
375        flags &= ~HostAuth.FLAG_AUTHENTICATE;
376        if (mServiceInfo.defaultSsl) {
377            flags |= HostAuth.FLAG_SSL;
378        }
379        SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags);
380
381        String hostname = recvAuth.mAddress;
382        if (hostname != null) {
383            mServerView.setText(hostname);
384        }
385
386        int port = recvAuth.mPort;
387        if (port != HostAuth.PORT_UNKNOWN) {
388            mPortView.setText(Integer.toString(port));
389        } else {
390            updatePortFromSecurityType();
391        }
392
393        mLoadedRecvAuth = recvAuth;
394        mLoaded = true;
395        validateFields();
396    }
397
398    /**
399     * Check the values in the fields and decide if it makes sense to enable the "next" button
400     */
401    private void validateFields() {
402        if (!mLoaded) return;
403        boolean enabled = Utility.isTextViewNotEmpty(mUsernameView)
404                && Utility.isTextViewNotEmpty(mPasswordView)
405                && Utility.isServerNameValid(mServerView)
406                && Utility.isPortFieldValid(mPortView);
407        enableNextButton(enabled);
408
409        String userName = mUsernameView.getText().toString().trim();
410        mCacheLoginCredential = userName;
411
412        // Warn (but don't prevent) if password has leading/trailing spaces
413        AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView);
414    }
415
416    private int getPortFromSecurityType(boolean useSsl) {
417        EmailServiceInfo info = EmailServiceUtils.getServiceInfo(mContext,
418                SetupData.getAccount().mHostAuthRecv.mProtocol);
419        return useSsl ? info.portSsl : info.port;
420    }
421
422    private boolean getSslSelected() {
423        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
424        return ((securityType & HostAuth.FLAG_SSL) != 0);
425    }
426
427    public void onUseSslChanged(boolean useSsl) {
428        if (mServiceInfo.offerCerts) {
429            int mode = useSsl ? View.VISIBLE : View.GONE;
430            mClientCertificateSelector.setVisibility(mode);
431            String deviceId = "";
432            try {
433                deviceId = Device.getDeviceId(mContext);
434            } catch (IOException e) {
435                // Not required
436            }
437            ((TextView) UiUtilities.getView(getView(), R.id.device_id)).setText(deviceId);
438
439            mDeviceIdSectionView.setVisibility(mode);
440            //UiUtilities.setVisibilitySafe(getView(), R.id.client_certificate_divider, mode);
441        }
442    }
443
444    private void updatePortFromSecurityType() {
445        boolean sslSelected = getSslSelected();
446        int port = getPortFromSecurityType(sslSelected);
447        mPortView.setText(Integer.toString(port));
448        onUseSslChanged(sslSelected);
449    }
450
451    /**
452     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
453     * Note, we update account here (as well as the account.mHostAuthRecv) because we edit
454     * account's delete policy here.
455     * Blocking - do not call from UI Thread.
456     */
457    @Override
458    public void saveSettingsAfterEdit() {
459        Account account = SetupData.getAccount();
460        account.update(mContext, account.toContentValues());
461        account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
462        // Update the backup (side copy) of the accounts
463        AccountBackupRestore.backup(mContext);
464    }
465
466    /**
467     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
468     */
469    @Override
470    public void saveSettingsAfterSetup() {
471        Account account = SetupData.getAccount();
472        HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
473        HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext);
474
475        // Set the username and password for the outgoing settings to the username and
476        // password the user just set for incoming.  Use the verified host address to try and
477        // pick a smarter outgoing address.
478        String hostName =
479                AccountSettingsUtils.inferServerName(mContext, recvAuth.mAddress, null, "smtp");
480        sendAuth.setLogin(recvAuth.mLogin, recvAuth.mPassword);
481        sendAuth.setConnection(sendAuth.mProtocol, hostName, sendAuth.mPort, sendAuth.mFlags);
482    }
483
484    /**
485     * Entry point from Activity, when "next" button is clicked
486     */
487    @Override
488    public void onNext() {
489        Account account = SetupData.getAccount();
490
491        // Make sure delete policy is an valid option before using it; otherwise, the results are
492        // indeterminate, I suspect...
493        if (mDeletePolicyView.getVisibility() == View.VISIBLE) {
494            account.setDeletePolicy(
495                    (Integer) ((SpinnerOption) mDeletePolicyView.getSelectedItem()).value);
496        }
497
498        HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
499        String userName = mUsernameView.getText().toString().trim();
500        String userPassword = mPasswordView.getText().toString();
501        recvAuth.setLogin(userName, userPassword);
502
503        String serverAddress = mServerView.getText().toString().trim();
504        int serverPort;
505        try {
506            serverPort = Integer.parseInt(mPortView.getText().toString().trim());
507        } catch (NumberFormatException e) {
508            serverPort = getPortFromSecurityType(getSslSelected());
509            Log.d(Logging.LOG_TAG, "Non-integer server port; using '" + serverPort + "'");
510        }
511        int securityType = (Integer) ((SpinnerOption) mSecurityTypeView.getSelectedItem()).value;
512        recvAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType);
513        if (mServiceInfo.offerPrefix) {
514            String prefix = mImapPathPrefixView.getText().toString().trim();
515            recvAuth.mDomain = TextUtils.isEmpty(prefix) ? null : ("/" + prefix);
516        } else {
517            recvAuth.mDomain = null;
518        }
519
520        // Check for a duplicate account (requires async DB work) and if OK,
521        // proceed with check
522        startDuplicateTaskCheck(
523                account.mId, serverAddress, mCacheLoginCredential, SetupData.CHECK_INCOMING);
524    }
525
526    @Override
527    public boolean haveSettingsChanged() {
528        boolean deletePolicyChanged = false;
529
530        // Only verify the delete policy if the control is visible (i.e. is a pop3 account)
531        if (mDeletePolicyView.getVisibility() == View.VISIBLE) {
532            int newDeletePolicy =
533                (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value;
534            deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy;
535        }
536
537        return deletePolicyChanged || super.haveSettingsChanged();
538    }
539
540    /**
541     * Implements AccountCheckSettingsFragment.Callbacks
542     */
543    @Override
544    public void onAutoDiscoverComplete(int result, HostAuth hostAuth) {
545        AccountSetupIncoming activity = (AccountSetupIncoming) getActivity();
546        activity.onAutoDiscoverComplete(result, hostAuth);
547    }
548
549    @Override
550    public void onCertificateRequested() {
551        Intent intent = new Intent(CertificateRequestor.ACTION_REQUEST_CERT);
552        intent.setData(Uri.parse("eas://com.android.emailcommon/certrequest"));
553        startActivityForResult(intent, CERTIFICATE_REQUEST);
554    }
555
556    @Override
557    public void onActivityResult(int requestCode, int resultCode, Intent data) {
558        if (requestCode == CERTIFICATE_REQUEST && resultCode == Activity.RESULT_OK) {
559            String certAlias = data.getStringExtra(CertificateRequestor.RESULT_ALIAS);
560            if (certAlias != null) {
561                mClientCertificateSelector.setCertificate(certAlias);
562            }
563        }
564    }
565}
566