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.os.Bundle;
     22 import android.text.Editable;
     23 import android.text.TextUtils;
     24 import android.text.TextWatcher;
     25 import android.text.method.DigitsKeyListener;
     26 import android.util.Log;
     27 import android.view.LayoutInflater;
     28 import android.view.View;
     29 import android.view.ViewGroup;
     30 import android.view.inputmethod.EditorInfo;
     31 import android.widget.AdapterView;
     32 import android.widget.ArrayAdapter;
     33 import android.widget.EditText;
     34 import android.widget.Spinner;
     35 import android.widget.TextView;
     36 
     37 import com.android.email.Email;
     38 import com.android.email.R;
     39 import com.android.email.activity.UiUtilities;
     40 import com.android.email.provider.AccountBackupRestore;
     41 import com.android.emailcommon.Logging;
     42 import com.android.emailcommon.provider.Account;
     43 import com.android.emailcommon.provider.HostAuth;
     44 import com.android.emailcommon.utility.Utility;
     45 
     46 /**
     47  * Provides UI for IMAP/POP account settings.
     48  *
     49  * This fragment is used by AccountSetupIncoming (for creating accounts) and by AccountSettingsXL
     50  * (for editing existing accounts).
     51  */
     52 public class AccountSetupIncomingFragment extends AccountServerBaseFragment {
     53 
     54     private final static String STATE_KEY_CREDENTIAL = "AccountSetupIncomingFragment.credential";
     55     private final static String STATE_KEY_LOADED = "AccountSetupIncomingFragment.loaded";
     56 
     57     private static final int POP3_PORT_NORMAL = 110;
     58     private static final int POP3_PORT_SSL = 995;
     59 
     60     private static final int IMAP_PORT_NORMAL = 143;
     61     private static final int IMAP_PORT_SSL = 993;
     62 
     63     private EditText mUsernameView;
     64     private EditText mPasswordView;
     65     private TextView mServerLabelView;
     66     private EditText mServerView;
     67     private EditText mPortView;
     68     private Spinner mSecurityTypeView;
     69     private TextView mDeletePolicyLabelView;
     70     private Spinner mDeletePolicyView;
     71     private View mImapPathPrefixSectionView;
     72     private EditText mImapPathPrefixView;
     73     // Delete policy as loaded from the device
     74     private int mLoadedDeletePolicy;
     75 
     76     // Support for lifecycle
     77     private boolean mStarted;
     78     private boolean mConfigured;
     79     private boolean mLoaded;
     80     private String mCacheLoginCredential;
     81 
     82     /**
     83      * Called to do initial creation of a fragment.  This is called after
     84      * {@link #onAttach(Activity)} and before {@link #onActivityCreated(Bundle)}.
     85      */
     86     @Override
     87     public void onCreate(Bundle savedInstanceState) {
     88         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
     89             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreate");
     90         }
     91         super.onCreate(savedInstanceState);
     92 
     93         if (savedInstanceState != null) {
     94             mCacheLoginCredential = savedInstanceState.getString(STATE_KEY_CREDENTIAL);
     95             mLoaded = savedInstanceState.getBoolean(STATE_KEY_LOADED, false);
     96         }
     97     }
     98 
     99     @Override
    100     public View onCreateView(LayoutInflater inflater, ViewGroup container,
    101             Bundle savedInstanceState) {
    102         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    103             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onCreateView");
    104         }
    105         int layoutId = mSettingsMode
    106                 ? R.layout.account_settings_incoming_fragment
    107                 : R.layout.account_setup_incoming_fragment;
    108 
    109         View view = inflater.inflate(layoutId, container, false);
    110         Context context = getActivity();
    111 
    112         mUsernameView = (EditText) UiUtilities.getView(view, R.id.account_username);
    113         mPasswordView = (EditText) UiUtilities.getView(view, R.id.account_password);
    114         mServerLabelView = (TextView) UiUtilities.getView(view, R.id.account_server_label);
    115         mServerView = (EditText) UiUtilities.getView(view, R.id.account_server);
    116         mPortView = (EditText) UiUtilities.getView(view, R.id.account_port);
    117         mSecurityTypeView = (Spinner) UiUtilities.getView(view, R.id.account_security_type);
    118         mDeletePolicyLabelView = (TextView) UiUtilities.getView(view,
    119                 R.id.account_delete_policy_label);
    120         mDeletePolicyView = (Spinner) UiUtilities.getView(view, R.id.account_delete_policy);
    121         mImapPathPrefixSectionView = UiUtilities.getView(view, R.id.imap_path_prefix_section);
    122         mImapPathPrefixView = (EditText) UiUtilities.getView(view, R.id.imap_path_prefix);
    123 
    124         // Set up spinners
    125         SpinnerOption securityTypes[] = {
    126             new SpinnerOption(HostAuth.FLAG_NONE, context.getString(
    127                     R.string.account_setup_incoming_security_none_label)),
    128             new SpinnerOption(HostAuth.FLAG_SSL, context.getString(
    129                     R.string.account_setup_incoming_security_ssl_label)),
    130             new SpinnerOption(HostAuth.FLAG_SSL | HostAuth.FLAG_TRUST_ALL, context.getString(
    131                     R.string.account_setup_incoming_security_ssl_trust_certificates_label)),
    132             new SpinnerOption(HostAuth.FLAG_TLS, context.getString(
    133                     R.string.account_setup_incoming_security_tls_label)),
    134             new SpinnerOption(HostAuth.FLAG_TLS | HostAuth.FLAG_TRUST_ALL, context.getString(
    135                     R.string.account_setup_incoming_security_tls_trust_certificates_label)),
    136         };
    137 
    138         SpinnerOption deletePolicies[] = {
    139             new SpinnerOption(Account.DELETE_POLICY_NEVER,
    140                     context.getString(R.string.account_setup_incoming_delete_policy_never_label)),
    141             new SpinnerOption(Account.DELETE_POLICY_ON_DELETE,
    142                     context.getString(R.string.account_setup_incoming_delete_policy_delete_label)),
    143         };
    144 
    145         ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(context,
    146                 android.R.layout.simple_spinner_item, securityTypes);
    147         securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    148         mSecurityTypeView.setAdapter(securityTypesAdapter);
    149 
    150         ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(context,
    151                 android.R.layout.simple_spinner_item, deletePolicies);
    152         deletePoliciesAdapter.setDropDownViewResource(
    153                 android.R.layout.simple_spinner_dropdown_item);
    154         mDeletePolicyView.setAdapter(deletePoliciesAdapter);
    155 
    156         // Updates the port when the user changes the security type. This allows
    157         // us to show a reasonable default which the user can change.
    158         mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
    159             @Override
    160             public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
    161                 updatePortFromSecurityType();
    162             }
    163 
    164             @Override
    165             public void onNothingSelected(AdapterView<?> arg0) { }
    166         });
    167 
    168         // After any text edits, call validateFields() which enables or disables the Next button
    169         TextWatcher validationTextWatcher = new TextWatcher() {
    170             @Override
    171             public void afterTextChanged(Editable s) {
    172                 validateFields();
    173             }
    174 
    175             @Override
    176             public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
    177             @Override
    178             public void onTextChanged(CharSequence s, int start, int before, int count) { }
    179         };
    180         // We're editing an existing account; don't allow modification of the user name
    181         if (mSettingsMode) {
    182             makeTextViewUneditable(mUsernameView,
    183                     getString(R.string.account_setup_username_uneditable_error));
    184         }
    185         mUsernameView.addTextChangedListener(validationTextWatcher);
    186         mPasswordView.addTextChangedListener(validationTextWatcher);
    187         mServerView.addTextChangedListener(validationTextWatcher);
    188         mPortView.addTextChangedListener(validationTextWatcher);
    189 
    190         // Only allow digits in the port field.
    191         mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
    192 
    193         // Additional setup only used while in "settings" mode
    194         onCreateViewSettingsMode(view);
    195 
    196         return view;
    197     }
    198 
    199     @Override
    200     public void onActivityCreated(Bundle savedInstanceState) {
    201         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    202             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onActivityCreated");
    203         }
    204         super.onActivityCreated(savedInstanceState);
    205     }
    206 
    207     /**
    208      * Called when the Fragment is visible to the user.
    209      */
    210     @Override
    211     public void onStart() {
    212         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    213             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStart");
    214         }
    215         super.onStart();
    216         mStarted = true;
    217         configureEditor();
    218         loadSettings();
    219     }
    220 
    221     /**
    222      * Called when the fragment is visible to the user and actively running.
    223      */
    224     @Override
    225     public void onResume() {
    226         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    227             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onResume");
    228         }
    229         super.onResume();
    230         validateFields();
    231     }
    232 
    233     @Override
    234     public void onPause() {
    235         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    236             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onPause");
    237         }
    238         super.onPause();
    239     }
    240 
    241     /**
    242      * Called when the Fragment is no longer started.
    243      */
    244     @Override
    245     public void onStop() {
    246         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    247             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onStop");
    248         }
    249         super.onStop();
    250         mStarted = false;
    251     }
    252 
    253     /**
    254      * Called when the fragment is no longer in use.
    255      */
    256     @Override
    257     public void onDestroy() {
    258         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    259             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onDestroy");
    260         }
    261         super.onDestroy();
    262     }
    263 
    264     @Override
    265     public void onSaveInstanceState(Bundle outState) {
    266         if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) {
    267             Log.d(Logging.LOG_TAG, "AccountSetupIncomingFragment onSaveInstanceState");
    268         }
    269         super.onSaveInstanceState(outState);
    270 
    271         outState.putString(STATE_KEY_CREDENTIAL, mCacheLoginCredential);
    272         outState.putBoolean(STATE_KEY_LOADED, mLoaded);
    273     }
    274 
    275     /**
    276      * Activity provides callbacks here.  This also triggers loading and setting up the UX
    277      */
    278     @Override
    279     public void setCallback(Callback callback) {
    280         super.setCallback(callback);
    281         if (mStarted) {
    282             configureEditor();
    283             loadSettings();
    284         }
    285     }
    286 
    287     /**
    288      * Configure the editor for the account type
    289      */
    290     private void configureEditor() {
    291         if (mConfigured) return;
    292         Account account = SetupData.getAccount();
    293         if (account == null || account.mHostAuthRecv == null) {
    294             return;
    295         }
    296         TextView lastView = mImapPathPrefixView;
    297         mBaseScheme = account.mHostAuthRecv.mProtocol;
    298         if (HostAuth.SCHEME_POP3.equals(mBaseScheme)) {
    299             mServerLabelView.setText(R.string.account_setup_incoming_pop_server_label);
    300             mServerView.setContentDescription(
    301                     getResources().getString(R.string.account_setup_incoming_pop_server_label));
    302             mImapPathPrefixSectionView.setVisibility(View.GONE);
    303             lastView = mPortView;
    304         } else if (HostAuth.SCHEME_IMAP.equals(mBaseScheme)) {
    305             mServerLabelView.setText(R.string.account_setup_incoming_imap_server_label);
    306             mServerView.setContentDescription(
    307                     getResources().getString(R.string.account_setup_incoming_imap_server_label));
    308             mDeletePolicyLabelView.setVisibility(View.GONE);
    309             mDeletePolicyView.setVisibility(View.GONE);
    310             mPortView.setImeOptions(EditorInfo.IME_ACTION_NEXT);
    311         } else {
    312             throw new Error("Unknown account type: " + account);
    313         }
    314         lastView.setOnEditorActionListener(mDismissImeOnDoneListener);
    315         mConfigured = true;
    316     }
    317 
    318     /**
    319      * Load the current settings into the UI
    320      */
    321     private void loadSettings() {
    322         if (mLoaded) return;
    323 
    324         Account account = SetupData.getAccount();
    325         HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
    326 
    327         String username = recvAuth.mLogin;
    328         if (username != null) {
    329             mUsernameView.setText(username);
    330         }
    331         String password = recvAuth.mPassword;
    332         if (password != null) {
    333             mPasswordView.setText(password);
    334             // Since username is uneditable, focus on the next editable field
    335             if (mSettingsMode) {
    336                 mPasswordView.requestFocus();
    337             }
    338         }
    339 
    340         if (HostAuth.SCHEME_IMAP.equals(recvAuth.mProtocol)) {
    341             String prefix = recvAuth.mDomain;
    342             if (prefix != null && prefix.length() > 0) {
    343                 mImapPathPrefixView.setText(prefix.substring(1));
    344             }
    345         } else if (!HostAuth.SCHEME_POP3.equals(recvAuth.mProtocol)) {
    346             // Account must either be IMAP or POP3
    347             throw new Error("Unknown account type: " + recvAuth.mProtocol);
    348         }
    349 
    350         // The delete policy is set for all legacy accounts. For POP3 accounts, the user sets
    351         // the policy explicitly. For IMAP accounts, the policy is set when the Account object
    352         // is created. @see AccountSetupBasics#populateSetupData
    353         mLoadedDeletePolicy = account.getDeletePolicy();
    354         SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mLoadedDeletePolicy);
    355 
    356         int flags = recvAuth.mFlags;
    357         flags &= ~HostAuth.FLAG_AUTHENTICATE;
    358         SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, flags);
    359 
    360         String hostname = recvAuth.mAddress;
    361         if (hostname != null) {
    362             mServerView.setText(hostname);
    363         }
    364 
    365         int port = recvAuth.mPort;
    366         if (port != HostAuth.PORT_UNKNOWN) {
    367             mPortView.setText(Integer.toString(port));
    368         } else {
    369             updatePortFromSecurityType();
    370         }
    371 
    372         mLoadedRecvAuth = recvAuth;
    373         mLoaded = true;
    374         validateFields();
    375     }
    376 
    377     /**
    378      * Check the values in the fields and decide if it makes sense to enable the "next" button
    379      */
    380     private void validateFields() {
    381         if (!mConfigured || !mLoaded) return;
    382         boolean enabled = Utility.isTextViewNotEmpty(mUsernameView)
    383                 && Utility.isTextViewNotEmpty(mPasswordView)
    384                 && Utility.isServerNameValid(mServerView)
    385                 && Utility.isPortFieldValid(mPortView);
    386         enableNextButton(enabled);
    387 
    388         String userName = mUsernameView.getText().toString().trim();
    389         mCacheLoginCredential = userName;
    390 
    391         // Warn (but don't prevent) if password has leading/trailing spaces
    392         AccountSettingsUtils.checkPasswordSpaces(mContext, mPasswordView);
    393     }
    394 
    395     private int getPortFromSecurityType() {
    396         int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
    397         boolean useSsl = ((securityType & HostAuth.FLAG_SSL) != 0);
    398         int port = useSsl ? IMAP_PORT_SSL : IMAP_PORT_NORMAL;     // default to IMAP
    399         if (HostAuth.SCHEME_POP3.equals(mBaseScheme)) {
    400             port = useSsl ? POP3_PORT_SSL : POP3_PORT_NORMAL;
    401         }
    402         return port;
    403     }
    404 
    405     private void updatePortFromSecurityType() {
    406         int port = getPortFromSecurityType();
    407         mPortView.setText(Integer.toString(port));
    408     }
    409 
    410     /**
    411      * Entry point from Activity after editing settings and verifying them.  Must be FLOW_MODE_EDIT.
    412      * Note, we update account here (as well as the account.mHostAuthRecv) because we edit
    413      * account's delete policy here.
    414      * Blocking - do not call from UI Thread.
    415      */
    416     @Override
    417     public void saveSettingsAfterEdit() {
    418         Account account = SetupData.getAccount();
    419         account.update(mContext, account.toContentValues());
    420         account.mHostAuthRecv.update(mContext, account.mHostAuthRecv.toContentValues());
    421         // Update the backup (side copy) of the accounts
    422         AccountBackupRestore.backup(mContext);
    423     }
    424 
    425     /**
    426      * Entry point from Activity after entering new settings and verifying them.  For setup mode.
    427      */
    428     @Override
    429     public void saveSettingsAfterSetup() {
    430         Account account = SetupData.getAccount();
    431         HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
    432         HostAuth sendAuth = account.getOrCreateHostAuthSend(mContext);
    433 
    434         // Set the username and password for the outgoing settings to the username and
    435         // password the user just set for incoming.  Use the verified host address to try and
    436         // pick a smarter outgoing address.
    437         String hostName = AccountSettingsUtils.inferServerName(recvAuth.mAddress, null, "smtp");
    438         sendAuth.setLogin(recvAuth.mLogin, recvAuth.mPassword);
    439         sendAuth.setConnection(sendAuth.mProtocol, hostName, sendAuth.mPort, sendAuth.mFlags);
    440     }
    441 
    442     /**
    443      * Entry point from Activity, when "next" button is clicked
    444      */
    445     @Override
    446     public void onNext() {
    447         Account account = SetupData.getAccount();
    448 
    449         // Make sure delete policy is an valid option before using it; otherwise, the results are
    450         // indeterminate, I suspect...
    451         if (mDeletePolicyView.getVisibility() == View.VISIBLE) {
    452             account.setDeletePolicy(
    453                     (Integer) ((SpinnerOption) mDeletePolicyView.getSelectedItem()).value);
    454         }
    455 
    456         HostAuth recvAuth = account.getOrCreateHostAuthRecv(mContext);
    457         String userName = mUsernameView.getText().toString().trim();
    458         String userPassword = mPasswordView.getText().toString();
    459         recvAuth.setLogin(userName, userPassword);
    460 
    461         String serverAddress = mServerView.getText().toString().trim();
    462         int serverPort;
    463         try {
    464             serverPort = Integer.parseInt(mPortView.getText().toString().trim());
    465         } catch (NumberFormatException e) {
    466             serverPort = getPortFromSecurityType();
    467             Log.d(Logging.LOG_TAG, "Non-integer server port; using '" + serverPort + "'");
    468         }
    469         int securityType = (Integer) ((SpinnerOption) mSecurityTypeView.getSelectedItem()).value;
    470         recvAuth.setConnection(mBaseScheme, serverAddress, serverPort, securityType);
    471         if (HostAuth.SCHEME_IMAP.equals(recvAuth.mProtocol)) {
    472             String prefix = mImapPathPrefixView.getText().toString().trim();
    473             recvAuth.mDomain = TextUtils.isEmpty(prefix) ? null : ("/" + prefix);
    474         } else {
    475             recvAuth.mDomain = null;
    476         }
    477 
    478         // Check for a duplicate account (requires async DB work) and if OK,
    479         // proceed with check
    480         startDuplicateTaskCheck(
    481                 account.mId, serverAddress, mCacheLoginCredential, SetupData.CHECK_INCOMING);
    482     }
    483 
    484     @Override
    485     public boolean haveSettingsChanged() {
    486         boolean deletePolicyChanged = false;
    487 
    488         // Only verify the delete policy if the control is visible (i.e. is a pop3 account)
    489         if (mDeletePolicyView.getVisibility() == View.VISIBLE) {
    490             int newDeletePolicy =
    491                 (Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value;
    492             deletePolicyChanged = mLoadedDeletePolicy != newDeletePolicy;
    493         }
    494 
    495         return deletePolicyChanged || super.haveSettingsChanged();
    496     }
    497 }
    498