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