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