AccountSetupOutgoingFragment.java revision 983e1ad53b3ca3105655bf6d961713c61060a7f8
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.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        mLoaded = true;
304        validateFields();
305    }
306
307    /**
308     * Preflight the values in the fields and decide if it makes sense to enable the "next" button
309     */
310    private void validateFields() {
311        if (!mLoaded) return;
312        boolean enabled =
313            Utility.isTextViewNotEmpty(mServerView) && Utility.isPortFieldValid(mPortView);
314
315        if (enabled && mRequireLoginView.isChecked()) {
316            enabled = (Utility.isTextViewNotEmpty(mUsernameView)
317                    && Utility.isTextViewNotEmpty(mPasswordView));
318        }
319
320        if (enabled) {
321            try {
322                URI uri = getUri();
323            } catch (URISyntaxException use) {
324                enabled = false;
325            }
326        }
327        enableNextButton(enabled);
328   }
329
330    /**
331     * implements OnCheckedChangeListener
332     */
333    @Override
334    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
335        mRequireLoginSettingsView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
336        if (mRequireLoginSettingsView2 != null) {
337            mRequireLoginSettingsView2.setVisibility(isChecked ? View.VISIBLE : View.GONE);
338        }
339        validateFields();
340    }
341
342    private void updatePortFromSecurityType() {
343        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
344        mPortView.setText(Integer.toString(SMTP_PORTS[securityType]));
345    }
346
347    /**
348     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
349     * Blocking - do not call from UI Thread.
350     */
351    @Override
352    public void saveSettingsAfterEdit() {
353        Account account = SetupData.getAccount();
354        account.mHostAuthSend.update(mContext, account.mHostAuthSend.toContentValues());
355        // Update the backup (side copy) of the accounts
356        AccountBackupRestore.backupAccounts(mContext);
357    }
358
359    /**
360     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
361     */
362    @Override
363    public void saveSettingsAfterSetup() {
364    }
365
366    /**
367     * Attempt to create a URI from the fields provided.  Throws URISyntaxException if there's
368     * a problem with the user input.
369     * @return a URI built from the account setup fields
370     */
371    /* package */ URI getUri() throws URISyntaxException {
372        int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
373        String userInfo = null;
374        if (mRequireLoginView.isChecked()) {
375            userInfo = mUsernameView.getText().toString().trim() + ":" + mPasswordView.getText();
376        }
377        URI uri = new URI(
378                SMTP_SCHEMES[securityType],
379                userInfo,
380                mServerView.getText().toString().trim(),
381                Integer.parseInt(mPortView.getText().toString().trim()),
382                null, null, null);
383        return uri;
384    }
385
386    /**
387     * Entry point from Activity, when "next" button is clicked
388     */
389    @Override
390    public void onNext() {
391        EmailContent.Account account = SetupData.getAccount();
392        try {
393            // TODO this should be accessed directly via the HostAuth structure
394            URI uri = getUri();
395            account.setSenderUri(mContext, uri.toString());
396        } catch (URISyntaxException use) {
397            /*
398             * It's unrecoverable if we cannot create a URI from components that
399             * we validated to be safe.
400             */
401            throw new Error(use);
402        }
403
404        mCallback.onProceedNext(SetupData.CHECK_OUTGOING, this);
405    }
406}
407