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