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