AccountSetupOutgoingFragment.java revision 31d9acbf0623872f9d4a2b3210b5970854b654c7
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.emailcommon.Logging;
23import com.android.emailcommon.provider.EmailContent;
24import com.android.emailcommon.provider.EmailContent.Account;
25import com.android.emailcommon.utility.Utility;
26
27import android.app.Activity;
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.CheckBox;
40import android.widget.CompoundButton;
41import android.widget.CompoundButton.OnCheckedChangeListener;
42import android.widget.EditText;
43import android.widget.Spinner;
44
45import java.net.URI;
46import java.net.URISyntaxException;
47
48/**
49 * Provides UI for SMTP account settings (for IMAP/POP accounts).
50 *
51 * This fragment is used by AccountSetupOutgoing (for creating accounts) and by AccountSettingsXL
52 * (for editing existing accounts).
53 */
54public class AccountSetupOutgoingFragment extends AccountServerBaseFragment
55        implements OnCheckedChangeListener {
56
57    private final static String STATE_KEY_LOADED = "AccountSetupOutgoingFragment.loaded";
58
59    private static final int SMTP_PORTS[] = {
60            587, 465, 465, 587, 587
61    };
62
63    private static final String SMTP_SCHEMES[] = {
64            "smtp", "smtp+ssl+", "smtp+ssl+trustallcerts", "smtp+tls+", "smtp+tls+trustallcerts"
65    };
66
67    private EditText mUsernameView;
68    private EditText mPasswordView;
69    private EditText mServerView;
70    private EditText mPortView;
71    private CheckBox mRequireLoginView;
72    private View mRequireLoginSettingsView;
73    private View mRequireLoginSettingsView2;
74    private Spinner mSecurityTypeView;
75
76    // Support for lifecycle
77    private boolean mStarted;
78    private boolean mLoaded;
79
80    /**
81     * Called to do initial creation of a fragment.  This is called after
82     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
83     */
84    @Override
85    public void onCreate(Bundle savedInstanceState) {
86        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
87            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreate");
88        }
89        super.onCreate(savedInstanceState);
90
91        if (savedInstanceState != null) {
92            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
93        }
94    }
95
96    @Override
97    public View onCreateView(LayoutInflater inflater, ViewGroup container,
98            Bundle savedInstanceState) {
99        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
100            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreateView");
101        }
102        int layoutId = mSettingsMode
103                ? R.layout.account_settings_outgoing_fragment
104                : R.layout.account_setup_outgoing_fragment;
105
106        View view = inflater.inflate(layoutId, container, false);
107        Context context = getActivity();
108
109        mUsernameView = (EditText) view.findViewById(R.id.account_username);
110        mPasswordView = (EditText) view.findViewById(R.id.account_password);
111        mServerView = (EditText) view.findViewById(R.id.account_server);
112        mPortView = (EditText) view.findViewById(R.id.account_port);
113        mRequireLoginView = (CheckBox) view.findViewById(R.id.account_require_login);
114        mRequireLoginSettingsView = view.findViewById(R.id.account_require_login_settings);
115        mRequireLoginSettingsView2 = view.findViewById(R.id.account_require_login_settings_2);
116        mSecurityTypeView = (Spinner) view.findViewById(R.id.account_security_type);
117        mRequireLoginView.setOnCheckedChangeListener(this);
118
119        // Note:  Strings are shared with AccountSetupIncomingFragment
120        SpinnerOption securityTypes[] = {
121            new SpinnerOption(0, context.getString(
122                    R.string.account_setup_incoming_security_none_label)),
123            new SpinnerOption(1, context.getString(
124                    R.string.account_setup_incoming_security_ssl_label)),
125            new SpinnerOption(2, context.getString(
126                    R.string.account_setup_incoming_security_ssl_trust_certificates_label)),
127            new SpinnerOption(3, context.getString(
128                    R.string.account_setup_incoming_security_tls_label)),
129            new SpinnerOption(4, context.getString(
130                    R.string.account_setup_incoming_security_tls_trust_certificates_label)),
131        };
132
133        ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context,
134                android.R.layout.simple_spinner_item, securityTypes);
135        securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
136        mSecurityTypeView.setAdapter(securityTypesAdapter);
137
138        // Updates the port when the user changes the security type. This allows
139        // us to show a reasonable default which the user can change.
140        mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
141            public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
142                updatePortFromSecurityType();
143            }
144
145            public void onNothingSelected(AdapterView<?> arg0) { }
146        });
147
148        // Calls validateFields() which enables or disables the Next button
149        TextWatcher validationTextWatcher = new TextWatcher() {
150            public void afterTextChanged(Editable s) {
151                validateFields();
152            }
153
154            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
155            public void onTextChanged(CharSequence s, int start, int before, int count) { }
156        };
157        mUsernameView.addTextChangedListener(validationTextWatcher);
158        mPasswordView.addTextChangedListener(validationTextWatcher);
159        mServerView.addTextChangedListener(validationTextWatcher);
160        mPortView.addTextChangedListener(validationTextWatcher);
161
162        // Only allow digits in the port field.
163        mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
164
165        // Additional setup only used while in "settings" mode
166        onCreateViewSettingsMode(view);
167
168        return view;
169    }
170
171    @Override
172    public void onActivityCreated(Bundle savedInstanceState) {
173        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
174            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onActivityCreated");
175        }
176        super.onActivityCreated(savedInstanceState);
177    }
178
179    /**
180     * Called when the Fragment is visible to the user.
181     */
182    @Override
183    public void onStart() {
184        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
185            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStart");
186        }
187        super.onStart();
188        mStarted = true;
189        loadSettings();
190    }
191
192    /**
193     * Called when the fragment is visible to the user and actively running.
194     */
195    @Override
196    public void onResume() {
197        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
198            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onResume");
199        }
200        super.onResume();
201        validateFields();
202    }
203
204    @Override
205    public void onPause() {
206        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
207            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onPause");
208        }
209        super.onPause();
210    }
211
212    /**
213     * Called when the Fragment is no longer started.
214     */
215    @Override
216    public void onStop() {
217        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
218            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStop");
219        }
220        super.onStop();
221        mStarted = false;
222    }
223
224    /**
225     * Called when the fragment is no longer in use.
226     */
227    @Override
228    public void onDestroy() {
229        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
230            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onDestroy");
231        }
232        super.onDestroy();
233    }
234
235    @Override
236    public void onSaveInstanceState(Bundle outState) {
237        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
238            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onSaveInstanceState");
239        }
240        super.onSaveInstanceState(outState);
241
242        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
243    }
244
245    /**
246     * Activity provides callbacks here.  This also triggers loading and setting up the UX
247     */
248    @Override
249    public void setCallback(Callback callback) {
250        super.setCallback(callback);
251        if (mStarted) {
252            loadSettings();
253        }
254    }
255
256    /**
257     * Load the current settings into the UI
258     */
259    private void loadSettings() {
260        if (mLoaded) return;
261        try {
262            // TODO this should be accessed directly via the HostAuth structure
263            URI uri = new URI(SetupData.getAccount().getSenderUri(mContext));
264            String username = null;
265            String password = null;
266            if (uri.getUserInfo() != null) {
267                String[] userInfoParts = uri.getUserInfo().split(":", 2);
268                username = userInfoParts[0];
269                if (userInfoParts.length > 1) {
270                    password = userInfoParts[1];
271                }
272            }
273
274            if (username != null) {
275                mUsernameView.setText(username);
276                mRequireLoginView.setChecked(true);
277            }
278
279            if (password != null) {
280                mPasswordView.setText(password);
281            }
282
283            for (int i = 0; i < SMTP_SCHEMES.length; i++) {
284                if (SMTP_SCHEMES[i].equals(uri.getScheme())) {
285                    SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
286                }
287            }
288
289            if (uri.getHost() != null) {
290                mServerView.setText(uri.getHost());
291            }
292
293            if (uri.getPort() != -1) {
294                mPortView.setText(Integer.toString(uri.getPort()));
295            } else {
296                updatePortFromSecurityType();
297            }
298        } catch (URISyntaxException use) {
299            /*
300             * We should always be able to parse our own settings.
301             */
302            throw new Error(use);
303        }
304
305        try {
306            mLoadedUri = getUri();
307        } catch (URISyntaxException ignore) {
308            // ignore; should not happen
309        }
310
311        mLoaded = true;
312        validateFields();
313    }
314
315    /**
316     * Preflight the values in the fields and decide if it makes sense to enable the "next" button
317     */
318    private void validateFields() {
319        if (!mLoaded) return;
320        boolean enabled =
321            Utility.isTextViewNotEmpty(mServerView) && Utility.isPortFieldValid(mPortView);
322
323        if (enabled && mRequireLoginView.isChecked()) {
324            enabled = (Utility.isTextViewNotEmpty(mUsernameView)
325                    && Utility.isTextViewNotEmpty(mPasswordView));
326        }
327
328        if (enabled) {
329            try {
330                URI uri = getUri();
331            } catch (URISyntaxException use) {
332                enabled = false;
333            }
334        }
335        enableNextButton(enabled);
336
337        // Warn (but don't prevent) if password has leading/trailing spaces
338        AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView);
339   }
340
341    /**
342     * implements OnCheckedChangeListener
343     */
344    @Override
345    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
346        mRequireLoginSettingsView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
347        if (mRequireLoginSettingsView2 != null) {
348            mRequireLoginSettingsView2.setVisibility(isChecked ? View.VISIBLE : View.GONE);
349        }
350        validateFields();
351    }
352
353    private void updatePortFromSecurityType() {
354        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
355        mPortView.setText(Integer.toString(SMTP_PORTS[securityType]));
356    }
357
358    /**
359     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
360     * Blocking - do not call from UI Thread.
361     */
362    @Override
363    public void saveSettingsAfterEdit() {
364        Account account = SetupData.getAccount();
365        account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
366        // Update the backup (side copy) of the accounts
367        AccountBackupRestore.backupAccounts(mContext);
368    }
369
370    /**
371     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
372     */
373    @Override
374    public void saveSettingsAfterSetup() {
375    }
376
377    /**
378     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
379     * a problem with the user input.
380     * @return a URI built from the account setup fields
381     */
382    @Override
383    protected URI getUri() throws URISyntaxException {
384        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
385        String userInfo = null;
386        if (mRequireLoginView.isChecked()) {
387            userInfo = mUsernameView.getText().toString().trim() + ":" + mPasswordView.getText();
388        }
389        URI uri = new URI(
390                SMTP_SCHEMES[securityType],
391                userInfo,
392                mServerView.getText().toString().trim(),
393                Integer.parseInt(mPortView.getText().toString().trim()),
394                null, null, null);
395        return uri;
396    }
397
398    /**
399     * Entry point from Activity, when "next" button is clicked
400     */
401    @Override
402    public void onNext() {
403        EmailContent.Account account = SetupData.getAccount();
404        try {
405            // TODO this should be accessed directly via the HostAuth structure
406            URI uri = getUri();
407            account.setSenderUri(mContext, uri.toString());
408        } catch (URISyntaxException use) {
409            /*
410             * It's unrecoverable if we cannot create a URI from components that
411             * we validated to be safe.
412             */
413            throw new Error(use);
414        }
415
416        mCallback.onProceedNext(SetupData.CHECK_OUTGOING, this);
417    }
418}
419