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