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 android.app.Activity;
20import android.content.Context;
21import android.os.Bundle;
22import android.text.Editable;
23import android.text.TextWatcher;
24import android.text.method.DigitsKeyListener;
25import android.util.Log;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.widget.AdapterView;
30import android.widget.ArrayAdapter;
31import android.widget.CheckBox;
32import android.widget.CompoundButton;
33import android.widget.CompoundButton.OnCheckedChangeListener;
34import android.widget.EditText;
35import android.widget.Spinner;
36
37import com.android.email.Email;
38import com.android.email.R;
39import com.android.email.activity.UiUtilities;
40import com.android.email.provider.AccountBackupRestore;
41import com.android.emailcommon.Logging;
42import com.android.emailcommon.provider.Account;
43import com.android.emailcommon.provider.HostAuth;
44import com.android.emailcommon.utility.Utility;
45
46/**
47 * Provides UI for SMTP account settings (for IMAP/POP accounts).
48 *
49 * This fragment is used by AccountSetupOutgoing (for creating accounts) and by AccountSettingsXL
50 * (for editing existing accounts).
51 */
52public class AccountSetupOutgoingFragment extends AccountServerBaseFragment
53        implements OnCheckedChangeListener {
54
55    private final static String STATE_KEY_LOADED = "AccountSetupOutgoingFragment.loaded";
56
57    private static final int SMTP_PORT_NORMAL = 587;
58    private static final int SMTP_PORT_SSL    = 465;
59
60    private EditText mUsernameView;
61    private EditText mPasswordView;
62    private EditText mServerView;
63    private EditText mPortView;
64    private CheckBox mRequireLoginView;
65    private Spinner mSecurityTypeView;
66
67    // Support for lifecycle
68    private boolean mStarted;
69    private boolean mLoaded;
70
71    /**
72     * Called to do initial creation of a fragment.  This is called after
73     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
74     */
75    @Override
76    public void onCreate(Bundle savedInstanceState) {
77        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
78            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreate");
79        }
80        super.onCreate(savedInstanceState);
81
82        if (savedInstanceState != null) {
83            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
84        }
85        mBaseScheme = HostAuth.SCHEME_SMTP;
86    }
87
88    @Override
89    public View onCreateView(LayoutInflater inflater, ViewGroup container,
90            Bundle savedInstanceState) {
91        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
92            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onCreateView");
93        }
94        int layoutId = mSettingsMode
95                ? R.layout.account_settings_outgoing_fragment
96                : R.layout.account_setup_outgoing_fragment;
97
98        View view = inflater.inflate(layoutId, container, false);
99        Context context = getActivity();
100
101        mUsernameView = (EditText) UiUtilities.getView(view, R.id.account_username);
102        mPasswordView = (EditText) UiUtilities.getView(view, R.id.account_password);
103        mServerView = (EditText) UiUtilities.getView(view, R.id.account_server);
104        mPortView = (EditText) UiUtilities.getView(view, R.id.account_port);
105        mRequireLoginView = (CheckBox) UiUtilities.getView(view, R.id.account_require_login);
106        mSecurityTypeView = (Spinner) UiUtilities.getView(view, R.id.account_security_type);
107        mRequireLoginView.setOnCheckedChangeListener(this);
108
109        // Note:  Strings are shared with AccountSetupIncomingFragment
110        SpinnerOption securityTypes[] = {
111            new SpinnerOption(HostAuth.FLAG_NONE, context.getString(
112                    R.string.account_setup_incoming_security_none_label)),
113            new SpinnerOption(HostAuth.FLAG_SSL, context.getString(
114                    R.string.account_setup_incoming_security_ssl_label)),
115            new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString(
116                    R.string.account_setup_incoming_security_ssl_trust_certificates_label)),
117            new SpinnerOption(HostAuth.FLAG_TLS, context.getString(
118                    R.string.account_setup_incoming_security_tls_label)),
119            new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, context.getString(
120                    R.string.account_setup_incoming_security_tls_trust_certificates_label)),
121        };
122
123        ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context,
124                android.R.layout.simple_spinner_item, securityTypes);
125        securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
126        mSecurityTypeView.setAdapter(securityTypesAdapter);
127
128        // Updates the port when the user changes the security type. This allows
129        // us to show a reasonable default which the user can change.
130        mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
131            public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
132                updatePortFromSecurityType();
133            }
134
135            public void onNothingSelected(AdapterView<?> arg0) { }
136        });
137
138        // Calls validateFields() which enables or disables the Next button
139        TextWatcher validationTextWatcher = new TextWatcher() {
140            public void afterTextChanged(Editable s) {
141                validateFields();
142            }
143
144            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
145            public void onTextChanged(CharSequence s, int start, int before, int count) { }
146        };
147        mUsernameView.addTextChangedListener(validationTextWatcher);
148        mPasswordView.addTextChangedListener(validationTextWatcher);
149        mServerView.addTextChangedListener(validationTextWatcher);
150        mPortView.addTextChangedListener(validationTextWatcher);
151
152        // Only allow digits in the port field.
153        mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
154
155        // Additional setup only used while in "settings" mode
156        onCreateViewSettingsMode(view);
157
158        return view;
159    }
160
161    @Override
162    public void onActivityCreated(Bundle savedInstanceState) {
163        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
164            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onActivityCreated");
165        }
166        super.onActivityCreated(savedInstanceState);
167    }
168
169    /**
170     * Called when the Fragment is visible to the user.
171     */
172    @Override
173    public void onStart() {
174        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
175            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStart");
176        }
177        super.onStart();
178        mStarted = true;
179        loadSettings();
180    }
181
182    /**
183     * Called when the fragment is visible to the user and actively running.
184     */
185    @Override
186    public void onResume() {
187        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
188            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onResume");
189        }
190        super.onResume();
191        validateFields();
192    }
193
194    @Override
195    public void onPause() {
196        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
197            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onPause");
198        }
199        super.onPause();
200    }
201
202    /**
203     * Called when the Fragment is no longer started.
204     */
205    @Override
206    public void onStop() {
207        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
208            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onStop");
209        }
210        super.onStop();
211        mStarted = false;
212    }
213
214    /**
215     * Called when the fragment is no longer in use.
216     */
217    @Override
218    public void onDestroy() {
219        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
220            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onDestroy");
221        }
222        super.onDestroy();
223    }
224
225    @Override
226    public void onSaveInstanceState(Bundle outState) {
227        if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
228            Log.d(Logging.LOG_TAG, "AccountSetupOutgoingFragment onSaveInstanceState");
229        }
230        super.onSaveInstanceState(outState);
231
232        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
233    }
234
235    /**
236     * Activity provides callbacks here.  This also triggers loading and setting up the UX
237     */
238    @Override
239    public void setCallback(Callback callback) {
240        super.setCallback(callback);
241        if (mStarted) {
242            loadSettings();
243        }
244    }
245
246    /**
247     * Load the current settings into the UI
248     */
249    private void loadSettings() {
250        if (mLoaded) return;
251
252        HostAuth sendAuth = SetupData.getAccount().getOrCreateHostAuthSend(mContext);
253        if ((sendAuth.mFlags & HostAuth.FLAG_AUTHENTICATE) != 0) {
254            String username = sendAuth.mLogin;
255            if (username != null) {
256                mUsernameView.setText(username);
257                mRequireLoginView.setChecked(true);
258            }
259
260            String password = sendAuth.mPassword;
261            if (password != null) {
262                mPasswordView.setText(password);
263            }
264        }
265
266        int flags = sendAuth.mFlags & ~HostAuth.FLAG_AUTHENTICATE;
267        SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags);
268
269        String hostname = sendAuth.mAddress;
270        if (hostname != null) {
271            mServerView.setText(hostname);
272        }
273
274        int port = sendAuth.mPort;
275        if (port != -1) {
276            mPortView.setText(Integer.toString(port));
277        } else {
278            updatePortFromSecurityType();
279        }
280
281        mLoadedSendAuth = sendAuth;
282        mLoaded = true;
283        validateFields();
284    }
285
286    /**
287     * Preflight the values in the fields and decide if it makes sense to enable the "next" button
288     */
289    private void validateFields() {
290        if (!mLoaded) return;
291        boolean enabled =
292            Utility.isServerNameValid(mServerView) && Utility.isPortFieldValid(mPortView);
293
294        if (enabled && mRequireLoginView.isChecked()) {
295            enabled = (Utility.isTextViewNotEmpty(mUsernameView)
296                    && Utility.isTextViewNotEmpty(mPasswordView));
297        }
298        enableNextButton(enabled);
299        // Warn (but don't prevent) if password has leading/trailing spaces
300        AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView);
301   }
302
303    /**
304     * implements OnCheckedChangeListener
305     */
306    @Override
307    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
308        final int visibility = isChecked ? View.VISIBLE : View.GONE;
309        UiUtilities.setVisibilitySafe(getView(), R.id.account_require_login_settings, visibility);
310        UiUtilities.setVisibilitySafe(getView(), R.id.account_require_login_settings_2, visibility);
311        validateFields();
312    }
313
314    private int getPortFromSecurityType() {
315        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
316        int port = (securityType & HostAuth.FLAG_SSL) != 0 ? SMTP_PORT_SSL : SMTP_PORT_NORMAL;
317        return port;
318    }
319
320    private void updatePortFromSecurityType() {
321        int port = getPortFromSecurityType();
322        mPortView.setText(Integer.toString(port));
323    }
324
325    /**
326     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
327     * Blocking - do not call from UI Thread.
328     */
329    @Override
330    public void saveSettingsAfterEdit() {
331        Account account = SetupData.getAccount();
332        account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
333        // Update the backup (side copy) of the accounts
334        AccountBackupRestore.backup(mContext);
335    }
336
337    /**
338     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
339     */
340    @Override
341    public void saveSettingsAfterSetup() {
342    }
343
344    /**
345     * Entry point from Activity, when "next" button is clicked
346     */
347    @Override
348    public void onNext() {
349        Account account = SetupData.getAccount();
350        HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext);
351
352        if (mRequireLoginView.isChecked()) {
353            String userName = mUsernameView.getText().toString().trim();
354            String userPassword = mPasswordView.getText().toString();
355            sendAuth.setLogin(userName, userPassword);
356        } else {
357            sendAuth.setLogin(null, null);
358        }
359
360        String serverAddress = mServerView.getText().toString().trim();
361        int serverPort;
362        try {
363            serverPort = Integer.parseInt(mPortView.getText().toString().trim());
364        } catch (NumberFormatException e) {
365            serverPort = getPortFromSecurityType();
366            Log.d(Logging.LOG_TAG, "Non-integer server port; using '" + serverPort + "'");
367        }
368        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
369        sendAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType);
370        sendAuth.mDomain = null;
371
372        mCallback.onProceedNext(SetupData.CHECK_OUTGOING, this);
373    }
374}
375