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