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.content.Intent;
22import android.content.Loader;
23import android.os.Bundle;
24import android.os.Parcel;
25import android.text.Editable;
26import android.text.TextUtils;
27import android.text.TextWatcher;
28import android.text.method.DigitsKeyListener;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.AdapterView;
33import android.widget.ArrayAdapter;
34import android.widget.CheckBox;
35import android.widget.CompoundButton;
36import android.widget.CompoundButton.OnCheckedChangeListener;
37import android.widget.EditText;
38import android.widget.Spinner;
39import android.widget.TextView;
40
41import com.android.email.R;
42import com.android.email.activity.UiUtilities;
43import com.android.email.activity.setup.AuthenticationView.AuthenticationCallback;
44import com.android.email.provider.AccountBackupRestore;
45import com.android.emailcommon.VendorPolicyLoader;
46import com.android.emailcommon.provider.Account;
47import com.android.emailcommon.provider.Credential;
48import com.android.emailcommon.provider.HostAuth;
49import com.android.emailcommon.utility.Utility;
50import com.android.mail.ui.MailAsyncTaskLoader;
51import com.android.mail.utils.LogUtils;
52
53import java.util.List;
54
55/**
56 * Provides UI for SMTP account settings (for IMAP/POP accounts).
57 *
58 * This fragment is used by AccountSetupOutgoing (for creating accounts) and by AccountSettingsXL
59 * (for editing existing accounts).
60 */
61public class AccountSetupOutgoingFragment extends AccountServerBaseFragment
62        implements OnCheckedChangeListener, AuthenticationCallback {
63
64    private static final int SIGN_IN_REQUEST = 1;
65
66    private final static String STATE_KEY_LOADED = "AccountSetupOutgoingFragment.loaded";
67
68    private static final int SMTP_PORT_NORMAL = 587;
69    private static final int SMTP_PORT_SSL    = 465;
70
71    private EditText mUsernameView;
72    private AuthenticationView mAuthenticationView;
73    private TextView mAuthenticationLabel;
74    private EditText mServerView;
75    private EditText mPortView;
76    private CheckBox mRequireLoginView;
77    private Spinner mSecurityTypeView;
78
79    // Support for lifecycle
80    private boolean mLoaded;
81
82    public static AccountSetupOutgoingFragment newInstance(boolean settingsMode) {
83        final AccountSetupOutgoingFragment f = new AccountSetupOutgoingFragment();
84        f.setArguments(getArgs(settingsMode));
85        return f;
86    }
87
88    // Public no-args constructor needed for fragment re-instantiation
89    public AccountSetupOutgoingFragment() {}
90
91    /**
92     * Called to do initial creation of a fragment.  This is called after
93     * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
94     */
95    @Override
96    public void onCreate(Bundle savedInstanceState) {
97        super.onCreate(savedInstanceState);
98
99        if (savedInstanceState != null) {
100            mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
101        }
102        mBaseScheme = HostAuth.LEGACY_SCHEME_SMTP;
103    }
104
105    @Override
106    public View onCreateView(LayoutInflater inflater, ViewGroup container,
107            Bundle savedInstanceState) {
108        final View view;
109        if (mSettingsMode) {
110            view = inflater.inflate(R.layout.account_settings_outgoing_fragment, container, false);
111        } else {
112            view = inflateTemplatedView(inflater, container,
113                    R.layout.account_setup_outgoing_fragment,
114                    R.string.account_setup_outgoing_headline);
115        }
116
117        mUsernameView = UiUtilities.getView(view, R.id.account_username);
118        mAuthenticationView = UiUtilities.getView(view, R.id.authentication_view);
119        mServerView = UiUtilities.getView(view, R.id.account_server);
120        mPortView = UiUtilities.getView(view, R.id.account_port);
121        mRequireLoginView = UiUtilities.getView(view, R.id.account_require_login);
122        mSecurityTypeView = UiUtilities.getView(view, R.id.account_security_type);
123        mRequireLoginView.setOnCheckedChangeListener(this);
124        // Don't use UiUtilities here. In some configurations this view does not exist, and
125        // UiUtilities throws an exception in this case.
126        mAuthenticationLabel = (TextView)view.findViewById(R.id.authentication_label);
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.post(new Runnable() {
131            @Override
132            public void run() {
133                mSecurityTypeView.setOnItemSelectedListener(
134                        new AdapterView.OnItemSelectedListener() {
135                            @Override
136                            public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2,
137                                    long arg3) {
138                                updatePortFromSecurityType();
139                            }
140
141                            @Override
142                            public void onNothingSelected(AdapterView<?> arg0) {
143                            }
144                        });
145            }});
146
147        // Calls validateFields() which enables or disables the Next button
148        final TextWatcher validationTextWatcher = new TextWatcher() {
149            @Override
150            public void afterTextChanged(Editable s) {
151                validateFields();
152            }
153
154            @Override
155            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
156            @Override
157            public void onTextChanged(CharSequence s, int start, int before, int count) { }
158        };
159        mUsernameView.addTextChangedListener(validationTextWatcher);
160        mServerView.addTextChangedListener(validationTextWatcher);
161        mPortView.addTextChangedListener(validationTextWatcher);
162
163        // Only allow digits in the port field.
164        mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
165
166        // Additional setup only used while in "settings" mode
167        onCreateViewSettingsMode(view);
168
169        mAuthenticationView.setAuthenticationCallback(this);
170
171        return view;
172    }
173
174    @Override
175    public void onActivityCreated(Bundle savedInstanceState) {
176        super.onActivityCreated(savedInstanceState);
177
178        final Context context = getActivity();
179        // Note:  Strings are shared with AccountSetupIncomingFragment
180        final SpinnerOption securityTypes[] = {
181                new SpinnerOption(HostAuth.FLAG_NONE, context.getString(
182                        R.string.account_setup_incoming_security_none_label)),
183                new SpinnerOption(HostAuth.FLAG_SSL, context.getString(
184                        R.string.account_setup_incoming_security_ssl_label)),
185                new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString(
186                        R.string.account_setup_incoming_security_ssl_trust_certificates_label)),
187                new SpinnerOption(HostAuth.FLAG_TLS, context.getString(
188                        R.string.account_setup_incoming_security_tls_label)),
189                new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, context.getString(
190                        R.string.account_setup_incoming_security_tls_trust_certificates_label)),
191        };
192
193        final ArrayAdapter<SpinnerOption> securityTypesAdapter =
194                new ArrayAdapter<SpinnerOption>(context, android.R.layout.simple_spinner_item,
195                        securityTypes);
196        securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
197        mSecurityTypeView.setAdapter(securityTypesAdapter);
198
199        loadSettings();
200    }
201
202    /**
203     * Called when the fragment is visible to the user and actively running.
204     */
205    @Override
206    public void onResume() {
207        super.onResume();
208        validateFields();
209    }
210
211    @Override
212    public void onSaveInstanceState(Bundle outState) {
213        super.onSaveInstanceState(outState);
214
215        outState.putBoolean(STATE_KEY_LOADED, mLoaded);
216    }
217
218    /**
219     * Load the current settings into the UI
220     */
221    private void loadSettings() {
222        if (mLoaded) return;
223
224        final HostAuth sendAuth = mSetupData.getAccount().getOrCreateHostAuthSend(mAppContext);
225        if (!mSetupData.isOutgoingCredLoaded()) {
226            sendAuth.setUserName(mSetupData.getEmail());
227            AccountSetupCredentialsFragment.populateHostAuthWithResults(mAppContext, sendAuth,
228                    mSetupData.getCredentialResults());
229            final String[] emailParts = mSetupData.getEmail().split("@");
230            final String domain = emailParts[1];
231            sendAuth.setConnection(sendAuth.mProtocol, domain, HostAuth.PORT_UNKNOWN,
232                    HostAuth.FLAG_NONE);
233            mSetupData.setOutgoingCredLoaded(true);
234        }
235        if ((sendAuth.mFlags & HostAuth.FLAG_AUTHENTICATE) != 0) {
236            final String username = sendAuth.mLogin;
237            if (username != null) {
238                mUsernameView.setText(username);
239                mRequireLoginView.setChecked(true);
240            }
241
242            final List<VendorPolicyLoader.OAuthProvider> oauthProviders =
243                    AccountSettingsUtils.getAllOAuthProviders(getActivity());
244            mAuthenticationView.setAuthInfo(oauthProviders.size() > 0, sendAuth);
245            if (mAuthenticationLabel != null) {
246                mAuthenticationLabel.setText(R.string.authentication_label);
247            }
248        }
249
250        final int flags = sendAuth.mFlags & HostAuth.FLAG_TRANSPORTSECURITY_MASK;
251        SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags);
252
253        final String hostname = sendAuth.mAddress;
254        if (hostname != null) {
255            mServerView.setText(hostname);
256        }
257
258        final int port = sendAuth.mPort;
259        if (port != -1) {
260            mPortView.setText(Integer.toString(port));
261        } else {
262            updatePortFromSecurityType();
263        }
264
265        // Make a deep copy of the HostAuth to compare with later
266        final Parcel parcel = Parcel.obtain();
267        parcel.writeParcelable(sendAuth, sendAuth.describeContents());
268        parcel.setDataPosition(0);
269        mLoadedSendAuth = parcel.readParcelable(HostAuth.class.getClassLoader());
270        parcel.recycle();
271
272        mLoaded = true;
273        validateFields();
274    }
275
276    /**
277     * Preflight the values in the fields and decide if it makes sense to enable the "next" button
278     */
279    private void validateFields() {
280        if (!mLoaded) return;
281        boolean enabled =
282            Utility.isServerNameValid(mServerView) && Utility.isPortFieldValid(mPortView);
283
284        if (enabled && mRequireLoginView.isChecked()) {
285            enabled = !TextUtils.isEmpty(mUsernameView.getText())
286                    && mAuthenticationView.getAuthValid();
287        }
288        enableNextButton(enabled);
289   }
290
291    /**
292     * implements OnCheckedChangeListener
293     */
294    @Override
295    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
296        final HostAuth sendAuth = mSetupData.getAccount().getOrCreateHostAuthSend(mAppContext);
297        mAuthenticationView.setAuthInfo(true, sendAuth);
298        final int visibility = isChecked ? View.VISIBLE : View.GONE;
299        UiUtilities.setVisibilitySafe(getView(), R.id.account_require_login_settings, visibility);
300        UiUtilities.setVisibilitySafe(getView(), R.id.account_require_login_settings_2, visibility);
301        validateFields();
302    }
303
304    private int getPortFromSecurityType() {
305        final int securityType =
306                (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
307        return (securityType & HostAuth.FLAG_SSL) != 0 ? SMTP_PORT_SSL : SMTP_PORT_NORMAL;
308    }
309
310    private void updatePortFromSecurityType() {
311        final int port = getPortFromSecurityType();
312        mPortView.setText(Integer.toString(port));
313    }
314
315    private static class SaveSettingsLoader extends MailAsyncTaskLoader<Boolean> {
316        private final SetupDataFragment mSetupData;
317        private final boolean mSettingsMode;
318
319        private SaveSettingsLoader(Context context, SetupDataFragment setupData,
320                boolean settingsMode) {
321            super(context);
322            mSetupData = setupData;
323            mSettingsMode = settingsMode;
324        }
325
326        @Override
327        public Boolean loadInBackground() {
328            if (mSettingsMode) {
329                saveSettingsAfterEdit(getContext(), mSetupData);
330            } else {
331                saveSettingsAfterSetup(getContext(), mSetupData);
332            }
333            return true;
334        }
335
336        @Override
337        protected void onDiscardResult(Boolean result) {}
338    }
339
340    @Override
341    public Loader<Boolean> getSaveSettingsLoader() {
342        return new SaveSettingsLoader(mAppContext, mSetupData, mSettingsMode);
343    }
344
345    /**
346     * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
347     * Blocking - do not call from UI Thread.
348     */
349    public static void saveSettingsAfterEdit(Context context, SetupDataFragment setupData) {
350        final Account account = setupData.getAccount();
351        final Credential cred = account.mHostAuthSend.mCredential;
352        if (cred != null) {
353            if (cred.isSaved()) {
354                cred.update(context, cred.toContentValues());
355            } else {
356                cred.save(context);
357                account.mHostAuthSend.mCredentialKey = cred.mId;
358            }
359        }
360        account.mHostAuthSend.update(context, account.mHostAuthSend.toContentValues());
361        // Update the backup (side copy) of the accounts
362        AccountBackupRestore.backup(context);
363    }
364
365    /**
366     * Entry point from Activity after entering new settings and verifying them.  For setup mode.
367     */
368    @SuppressWarnings("unused")
369    public static void saveSettingsAfterSetup(Context context, SetupDataFragment setupData) {
370        // No need to do anything here
371    }
372
373    /**
374     * Entry point from Activity, when "next" button is clicked
375     */
376    @Override
377    public int collectUserInputInternal() {
378        final Account account = mSetupData.getAccount();
379        final HostAuth sendAuth = account.getOrCreateHostAuthSend(mAppContext);
380
381        if (mRequireLoginView.isChecked()) {
382            final String userName = mUsernameView.getText().toString().trim();
383            final String userPassword = mAuthenticationView.getPassword();
384            sendAuth.setLogin(userName, userPassword);
385        } else {
386            sendAuth.setLogin(null, null);
387        }
388
389        final String serverAddress = mServerView.getText().toString().trim();
390        int serverPort;
391        try {
392            serverPort = Integer.parseInt(mPortView.getText().toString().trim());
393        } catch (NumberFormatException e) {
394            serverPort = getPortFromSecurityType();
395            LogUtils.d(LogUtils.TAG, "Non-integer server port; using '" + serverPort + "'");
396        }
397        final int securityType =
398                (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
399        sendAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType);
400        sendAuth.mDomain = null;
401
402        return SetupDataFragment.CHECK_OUTGOING;
403    }
404
405    @Override
406    public void onValidateStateChanged() {
407        validateFields();
408    }
409
410    @Override
411    public void onRequestSignIn() {
412        // Launch the credential activity.
413        // Use HostAuthRecv here because we want to know if this is account is IMAP (offer OAuth) or
414        // if it's POP (password only)
415        final String protocol =
416                mSetupData.getAccount().getOrCreateHostAuthRecv(mAppContext).mProtocol;
417        final Intent intent = AccountCredentials.getAccountCredentialsIntent(getActivity(),
418                mUsernameView.getText().toString(), protocol);
419        startActivityForResult(intent, SIGN_IN_REQUEST);
420    }
421
422    @Override
423    public void onActivityResult(final int requestCode, final int resultCode, final Intent data) {
424        if (requestCode == SIGN_IN_REQUEST && resultCode == Activity.RESULT_OK) {
425            final Account account = mSetupData.getAccount();
426            final HostAuth sendAuth = account.getOrCreateHostAuthSend(getActivity());
427            AccountSetupCredentialsFragment.populateHostAuthWithResults(mAppContext, sendAuth,
428                    data.getExtras());
429            mAuthenticationView.setAuthInfo(true, sendAuth);
430        }
431    }
432}
433