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