Home | History | Annotate | Download | only in setup
      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 
     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