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