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 
17 package com.android.email.activity.setup;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.Loader;
23 import android.os.Bundle;
24 import android.os.Parcel;
25 import android.text.Editable;
26 import android.text.TextUtils;
27 import android.text.TextWatcher;
28 import android.text.method.DigitsKeyListener;
29 import android.view.LayoutInflater;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.AdapterView;
33 import android.widget.ArrayAdapter;
34 import android.widget.CheckBox;
35 import android.widget.CompoundButton;
36 import android.widget.CompoundButton.OnCheckedChangeListener;
37 import android.widget.EditText;
38 import android.widget.Spinner;
39 import android.widget.TextView;
40 
41 import com.android.email.R;
42 import com.android.email.activity.UiUtilities;
43 import com.android.email.activity.setup.AuthenticationView.AuthenticationCallback;
44 import com.android.email.provider.AccountBackupRestore;
45 import com.android.emailcommon.VendorPolicyLoader;
46 import com.android.emailcommon.provider.Account;
47 import com.android.emailcommon.provider.Credential;
48 import com.android.emailcommon.provider.HostAuth;
49 import com.android.emailcommon.utility.Utility;
50 import com.android.mail.ui.MailAsyncTaskLoader;
51 import com.android.mail.utils.LogUtils;
52 
53 import 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  */
61 public 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 
newInstance(boolean settingsMode)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
AccountSetupOutgoingFragment()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
onCreate(Bundle savedInstanceState)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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)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
onResume()206     public void onResume() {
207         super.onResume();
208         validateFields();
209     }
210 
211     @Override
onSaveInstanceState(Bundle outState)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      */
loadSettings()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      */
validateFields()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
onCheckedChanged(CompoundButton buttonView, boolean isChecked)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 
getPortFromSecurityType()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 
updatePortFromSecurityType()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 
SaveSettingsLoader(Context context, SetupDataFragment setupData, boolean settingsMode)319         private SaveSettingsLoader(Context context, SetupDataFragment setupData,
320                 boolean settingsMode) {
321             super(context);
322             mSetupData = setupData;
323             mSettingsMode = settingsMode;
324         }
325 
326         @Override
loadInBackground()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
onDiscardResult(Boolean result)337         protected void onDiscardResult(Boolean result) {}
338     }
339 
340     @Override
getSaveSettingsLoader()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      */
saveSettingsAfterEdit(Context context, SetupDataFragment setupData)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")
saveSettingsAfterSetup(Context context, SetupDataFragment setupData)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
collectUserInputInternal()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
onValidateStateChanged()406     public void onValidateStateChanged() {
407         validateFields();
408     }
409 
410     @Override
onRequestSignIn()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
onActivityResult(final int requestCode, final int resultCode, final Intent data)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