AccountSetupIncomingFragment.java revision 1f3e117951e5ffe97a30bdd61c1cf99759f15406
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.AccountBackupRestore;
20import com.android.email.Email;
21import com.android.email.R;
22import com.android.email.Utility;
23import com.android.email.provider.EmailContent;
24import com.android.email.provider.EmailContent.Account;
25
26import android.app.Activity;
27import android.content.Context;
28import android.os.Bundle;
29import android.text.Editable;
30import android.text.TextWatcher;
31import android.text.method.DigitsKeyListener;
32import android.util.Log;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.widget.AdapterView;
37import android.widget.ArrayAdapter;
38import android.widget.EditText;
39import android.widget.Spinner;
40import android.widget.TextView;
41
42import java.net.URI;
43import java.net.URISyntaxException;
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 POP_PORTS[] = {
57            110, 995, 995, 110, 110
58    };
59    private static final String POP_SCHEMES[] = {
60            "pop3", "pop3+ssl+", "pop3+ssl+trustallcerts", "pop3+tls+", "pop3+tls+trustallcerts"
61    };
62    private static final int IMAP_PORTS[] = {
63            143, 993, 993, 143, 143
64    };
65    private static final String IMAP_SCHEMES[] = {
66            "imap", "imap+ssl+", "imap+ssl+trustallcerts", "imap+tls+", "imap+tls+trustallcerts"
67    };
68
69    private int mAccountPorts[];
70    private String mAccountSchemes[];
71    private EditText mUsernameView;
72    private EditText mPasswordView;
73    private TextView mServerLabelView;
74    private EditText mServerView;
75    private EditText mPortView;
76    private Spinner mSecurityTypeView;
77    private TextView mDeletePolicyLabelView;
78    private Spinner mDeletePolicyView;
79    private View mImapPathPrefixSectionView;
80    private EditText mImapPathPrefixView;
81
82    // Support for lifecycle
83    private boolean mStarted;
84    private boolean mConfigured;
85    private boolean mLoaded;
86    private String mCacheLoginCredential;
87
88    /**
89     * Called to do initial creation of a fragment.  This is called after
90     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
91     */
92    @Override
93    public void onCreate(Bundle savedInstanceState) {
94        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
95            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onCreate");
96        }
97        super.onCreate(savedInstanceState);
98
99        if (savedInstanceState != null) {
100            mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
101            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
102        }
103    }
104
105    @Override
106    public View onCreateView(LayoutInflater inflater, ViewGroup container,
107            Bundle savedInstanceState) {
108        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
109            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onCreateView");
110        }
111        View view = inflater.inflate(R.layout.account_setup_incoming_fragment, container, false);
112        Context context = getActivity();
113
114        mUsernameView = (EditText) view.findViewById(R.id.account_username);
115        mPasswordView = (EditText) view.findViewById(R.id.account_password);
116        mServerLabelView = (TextView) view.findViewById(R.id.account_server_label);
117        mServerView = (EditText) view.findViewById(R.id.account_server);
118        mPortView = (EditText) view.findViewById(R.id.account_port);
119        mSecurityTypeView = (Spinner) view.findViewById(R.id.account_security_type);
120        mDeletePolicyLabelView = (TextView) view.findViewById(R.id.account_delete_policy_label);
121        mDeletePolicyView = (Spinner) view.findViewById(R.id.account_delete_policy);
122        mImapPathPrefixSectionView = view.findViewById(R.id.imap_path_prefix_section);
123        mImapPathPrefixView = (EditText) view.findViewById(R.id.imap_path_prefix);
124
125        // Set up spinners
126        SpinnerOption securityTypes[] = {
127            new SpinnerOption(0,
128                    context.getString(R.string.account_setup_incoming_security_none_label)),
129            new SpinnerOption(1,
130                    context.getString(R.string.account_setup_incoming_security_ssl_label)),
131            new SpinnerOption(2,
132                    context.getString(
133                            R.string.account_setup_incoming_security_ssl_trust_certificates_label)),
134            new SpinnerOption(3,
135                    context.getString(R.string.account_setup_incoming_security_tls_label)),
136            new SpinnerOption(4,
137                    context.getString(
138                            R.string.account_setup_incoming_security_tls_trust_certificates_label)),
139        };
140
141        SpinnerOption deletePolicies[] = {
142            new SpinnerOption(Account.DELETE_POLICY_NEVER,
143                    context.getString(R.string.account_setup_incoming_delete_policy_never_label)),
144            new SpinnerOption(Account.DELETE_POLICY_ON_DELETE,
145                    context.getString(R.string.account_setup_incoming_delete_policy_delete_label)),
146        };
147
148        ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context,
149                android.R.layout.simple_spinner_item, securityTypes);
150        securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
151        mSecurityTypeView.setAdapter(securityTypesAdapter);
152
153        ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(context,
154                android.R.layout.simple_spinner_item, deletePolicies);
155        deletePoliciesAdapter.setDropDownViewResource(
156                android.R.layout.simple_spinner_dropdown_item);
157        mDeletePolicyView.setAdapter(deletePoliciesAdapter);
158
159        // Updates the port when the user changes the security type. This allows
160        // us to show a reasonable default which the user can change.
161        mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
162            public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
163                updatePortFromSecurityType();
164            }
165
166            public void onNothingSelected(AdapterView<?> arg0) { }
167        });
168
169        // After any text edits, call validateFields() which enables or disables the Next button
170        TextWatcher validationTextWatcher = new TextWatcher() {
171            public void afterTextChanged(Editable s) {
172                validateFields();
173            }
174
175            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
176            public void onTextChanged(CharSequence s, int start, int before, int count) { }
177        };
178        mUsernameView.addTextChangedListener(validationTextWatcher);
179        mPasswordView.addTextChangedListener(validationTextWatcher);
180        mServerView.addTextChangedListener(validationTextWatcher);
181        mPortView.addTextChangedListener(validationTextWatcher);
182
183        // Only allow digits in the port field.
184        mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
185
186        return view;
187    }
188
189    @Override
190    public void onActivityCreated(Bundle savedInstanceState) {
191        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
192            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated");
193        }
194        super.onActivityCreated(savedInstanceState);
195    }
196
197    /**
198     * Called when the Fragment is visible to the user.
199     */
200    @Override
201    public void onStart() {
202        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
203            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onStart");
204        }
205        super.onStart();
206        mStarted = true;
207        configureEditor();
208        loadSettings();
209    }
210
211    /**
212     * Called when the fragment is visible to the user and actively running.
213     */
214    @Override
215    public void onResume() {
216        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
217            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onResume");
218        }
219        super.onResume();
220        validateFields();
221    }
222
223    @Override
224    public void onPause() {
225        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
226            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onPause");
227        }
228        super.onPause();
229    }
230
231    /**
232     * Called when the Fragment is no longer started.
233     */
234    @Override
235    public void onStop() {
236        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
237            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onStop");
238        }
239        super.onStop();
240        mStarted = false;
241    }
242
243    /**
244     * Called when the fragment is no longer in use.
245     */
246    @Override
247    public void onDestroy() {
248        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
249            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onDestroy");
250        }
251        super.onDestroy();
252    }
253
254    @Override
255    public void onSaveInstanceState(Bundle outState) {
256        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
257            Log.d(Email.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState");
258        }
259        super.onSaveInstanceState(outState);
260
261        outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential);
262        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
263    }
264
265    /**
266     * Activity provides callbacks here.  This also triggers loading and setting up the UX
267     */
268    @Override
269    public void setCallback(Callback callback) {
270        super.setCallback(callback);
271        if (mStarted) {
272            configureEditor();
273            loadSettings();
274        }
275    }
276
277    /**
278     * Configure the editor for the account type
279     */
280    private void configureEditor() {
281        if (mConfigured) return;
282        Account account = SetupData.getAccount();
283        String protocol = account.mHostAuthRecv.mProtocol;
284        if (protocol.startsWith("pop3")) {
285            mServerLabelView.setText(R.string.account_setup_incoming_pop_server_label);
286            mAccountPorts = POP_PORTS;
287            mAccountSchemes = POP_SCHEMES;
288            mImapPathPrefixSectionView.setVisibility(View.GONE);
289        } else if (protocol.startsWith("imap")) {
290            mServerLabelView.setText(R.string.account_setup_incoming_imap_server_label);
291            mAccountPorts = IMAP_PORTS;
292            mAccountSchemes = IMAP_SCHEMES;
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        try {
307            // TODO this should be accessed directly via the HostAuth structure
308            EmailContent.Account account = SetupData.getAccount();
309            URI uri = new URI(account.getStoreUri(mContext));
310            String username = null;
311            String password = null;
312            if (uri.getUserInfo() != null) {
313                String[] userInfoParts = uri.getUserInfo().split(":", 2);
314                username = userInfoParts[0];
315                if (userInfoParts.length > 1) {
316                    password = userInfoParts[1];
317                }
318            }
319
320            if (username != null) {
321                mUsernameView.setText(username);
322            }
323
324            if (password != null) {
325                mPasswordView.setText(password);
326            }
327
328            if (uri.getScheme().startsWith("pop3")) {
329                SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, account.getDeletePolicy());
330            } else if (uri.getScheme().startsWith("imap")) {
331                if (uri.getPath() != null && uri.getPath().length() > 0) {
332                    mImapPathPrefixView.setText(uri.getPath().substring(1));
333                }
334            } else {
335                throw new Error("Unknown account type: " + account.getStoreUri(mContext));
336            }
337
338            for (int i = 0; i < mAccountSchemes.length; i++) {
339                if (mAccountSchemes[i].equals(uri.getScheme())) {
340                    SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
341                }
342            }
343
344            if (uri.getHost() != null) {
345                mServerView.setText(uri.getHost());
346            }
347
348            if (uri.getPort() != -1) {
349                mPortView.setText(Integer.toString(uri.getPort()));
350            } else {
351                updatePortFromSecurityType();
352            }
353        } catch (URISyntaxException use) {
354            /*
355             * We should always be able to parse our own settings.
356             */
357            throw new Error(use);
358        }
359        mLoaded = true;
360        validateFields();
361    }
362
363    /**
364     * Check the values in the fields and decide if it makes sense to enable the "next" button
365     */
366    private void validateFields() {
367        if (!mConfigured || !mLoaded) return;
368        boolean enabled = Utility.isTextViewNotEmpty(mUsernameView)
369                && Utility.isTextViewNotEmpty(mPasswordView)
370                && Utility.isTextViewNotEmpty(mServerView)
371                && Utility.isPortFieldValid(mPortView);
372        if (enabled) {
373            try {
374                URI uri = getUri();
375            } catch (URISyntaxException use) {
376                enabled = false;
377            }
378        }
379        enableNextButton(enabled);
380    }
381
382    private void updatePortFromSecurityType() {
383        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
384        mPortView.setText(Integer.toString(mAccountPorts[securityType]));
385    }
386
387    /**
388     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
389     */
390    @Override
391    public void saveSettingsAfterEdit() {
392        EmailContent.Account account = SetupData.getAccount();
393        if (account.isSaved()) {
394            account.update(mContext, account.toContentValues());
395            account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
396        } else {
397            account.save(mContext);
398        }
399        // Update the backup (side copy) of the accounts
400        AccountBackupRestore.backupAccounts(mContext);
401    }
402
403    /**
404     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
405     */
406    @Override
407    public void saveSettingsAfterSetup() {
408        EmailContent.Account account = SetupData.getAccount();
409
410        // Set the username and password for the outgoing settings to the username and
411        // password the user just set for incoming.  Use the verified host address to try and
412        // pick a smarter outgoing address.
413        try {
414            String hostName =
415                AccountSettingsUtils.inferServerName(account.mHostAuthRecv.mAddress, null, "smtp");
416            URI oldUri = new URI(account.getSenderUri(mContext));
417            URI uri = new URI(
418                    oldUri.getScheme(),
419                    mUsernameView.getText().toString().trim() + ":"
420                            + mPasswordView.getText().toString(),
421                    hostName,
422                    oldUri.getPort(),
423                    null,
424                    null,
425                    null);
426            account.setSenderUri(mContext, uri.toString());
427        } catch (URISyntaxException use) {
428            // If we can't set up the URL we just continue. It's only for convenience.
429        }
430    }
431
432    /**
433     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
434     * a problem with the user input.
435     * @return a URI built from the account setup fields
436     */
437    /* package */ URI getUri() throws URISyntaxException {
438        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
439        String path = null;
440        if (mAccountSchemes[securityType].startsWith("imap")) {
441            path = "/" + mImapPathPrefixView.getText().toString().trim();
442        }
443        String userName = mUsernameView.getText().toString().trim();
444        mCacheLoginCredential = userName;
445        URI uri = new URI(
446                mAccountSchemes[securityType],
447                userName + ":" + mPasswordView.getText(),
448                mServerView.getText().toString().trim(),
449                Integer.parseInt(mPortView.getText().toString().trim()),
450                path, // path
451                null, // query
452                null);
453
454        return uri;
455    }
456
457    /**
458     * Entry point from Activity, when "next" button is clicked
459     */
460    @Override
461    public void onNext() {
462        EmailContent.Account setupAccount = SetupData.getAccount();
463        try {
464            URI uri = getUri();
465            setupAccount.setStoreUri(mContext, uri.toString());
466
467            // Stop here if the login credentials duplicate an existing account
468            // (unless they duplicate the existing account, as they of course will)
469            EmailContent.Account account = Utility.findExistingAccount(mContext, setupAccount.mId,
470                    uri.getHost(), mCacheLoginCredential);
471            if (account != null) {
472                DuplicateAccountDialogFragment dialogFragment =
473                    DuplicateAccountDialogFragment.newInstance(account.mDisplayName);
474                dialogFragment.show(getActivity(), DuplicateAccountDialogFragment.TAG);
475                return;
476            }
477        } catch (URISyntaxException use) {
478            /*
479             * It's unrecoverable if we cannot create a URI from components that
480             * we validated to be safe.
481             */
482            throw new Error(use);
483        }
484
485        setupAccount.setDeletePolicy(
486                (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value);
487
488        mCallback.onProceedNext(SetupData.CHECK_INCOMING, this);
489    }
490}
491