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