org.strongswan.android.ui.VpnProfileDetailActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.strongswan.android.ui.VpnProfileDetailActivity.java

Source

/*
 * Copyright (C) 2012-2017 Tobias Brunner
 * Copyright (C) 2012 Giuliano Grassi
 * Copyright (C) 2012 Ralf Sager
 * HSR Hochschule fuer Technik Rapperswil
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

package org.strongswan.android.ui;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.security.KeyChain;
import android.security.KeyChainAliasCallback;
import android.security.KeyChainException;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDialogFragment;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.EditText;
import android.widget.MultiAutoCompleteTextView;
import android.widget.RelativeLayout;
import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;

import org.strongswan.android.R;
import org.strongswan.android.data.VpnProfile;
import org.strongswan.android.data.VpnProfile.SelectedAppsHandling;
import org.strongswan.android.data.VpnProfileDataSource;
import org.strongswan.android.data.VpnType;
import org.strongswan.android.data.VpnType.VpnTypeFeature;
import org.strongswan.android.logic.TrustedCertificateManager;
import org.strongswan.android.security.TrustedCertificateEntry;
import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter;
import org.strongswan.android.ui.widget.TextInputLayoutHelper;
import org.strongswan.android.utils.Constants;
import org.strongswan.android.utils.IPRangeSet;
import org.strongswan.android.utils.Utils;

import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.UUID;

public class VpnProfileDetailActivity extends AppCompatActivity {
    private static final int SELECT_TRUSTED_CERTIFICATE = 0;
    private static final int SELECT_APPLICATIONS = 1;

    private VpnProfileDataSource mDataSource;
    private Long mId;
    private TrustedCertificateEntry mCertEntry;
    private String mUserCertLoading;
    private CertificateIdentitiesAdapter mSelectUserIdAdapter;
    private String mSelectedUserId;
    private TrustedCertificateEntry mUserCertEntry;
    private VpnType mVpnType = VpnType.IKEV2_EAP;
    private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
    private SortedSet<String> mSelectedApps = new TreeSet<>();
    private VpnProfile mProfile;
    private MultiAutoCompleteTextView mName;
    private TextInputLayoutHelper mNameWrap;
    private EditText mGateway;
    private TextInputLayoutHelper mGatewayWrap;
    private Spinner mSelectVpnType;
    private ViewGroup mUsernamePassword;
    private EditText mUsername;
    private TextInputLayoutHelper mUsernameWrap;
    private EditText mPassword;
    private ViewGroup mUserCertificate;
    private RelativeLayout mSelectUserCert;
    private Spinner mSelectUserId;
    private CheckBox mCheckAuto;
    private RelativeLayout mSelectCert;
    private RelativeLayout mTncNotice;
    private CheckBox mShowAdvanced;
    private ViewGroup mAdvancedSettings;
    private MultiAutoCompleteTextView mRemoteId;
    private TextInputLayoutHelper mRemoteIdWrap;
    private EditText mMTU;
    private TextInputLayoutHelper mMTUWrap;
    private EditText mPort;
    private TextInputLayoutHelper mPortWrap;
    private Switch mCertReq;
    private EditText mNATKeepalive;
    private TextInputLayoutHelper mNATKeepaliveWrap;
    private EditText mIncludedSubnets;
    private TextInputLayoutHelper mIncludedSubnetsWrap;
    private EditText mExcludedSubnets;
    private TextInputLayoutHelper mExcludedSubnetsWrap;
    private CheckBox mBlockIPv4;
    private CheckBox mBlockIPv6;
    private Spinner mSelectSelectedAppsHandling;
    private RelativeLayout mSelectApps;
    private TextInputLayoutHelper mIkeProposalWrap;
    private EditText mIkeProposal;
    private TextInputLayoutHelper mEspProposalWrap;
    private EditText mEspProposal;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        /* the title is set when we load the profile, if any */
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        mDataSource = new VpnProfileDataSource(this);
        mDataSource.open();

        setContentView(R.layout.profile_detail_view);

        mName = (MultiAutoCompleteTextView) findViewById(R.id.name);
        mNameWrap = (TextInputLayoutHelper) findViewById(R.id.name_wrap);
        mGateway = (EditText) findViewById(R.id.gateway);
        mGatewayWrap = (TextInputLayoutHelper) findViewById(R.id.gateway_wrap);
        mSelectVpnType = (Spinner) findViewById(R.id.vpn_type);
        mTncNotice = (RelativeLayout) findViewById(R.id.tnc_notice);

        mUsernamePassword = (ViewGroup) findViewById(R.id.username_password_group);
        mUsername = (EditText) findViewById(R.id.username);
        mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
        mPassword = (EditText) findViewById(R.id.password);

        mUserCertificate = (ViewGroup) findViewById(R.id.user_certificate_group);
        mSelectUserCert = (RelativeLayout) findViewById(R.id.select_user_certificate);
        mSelectUserId = (Spinner) findViewById(R.id.select_user_id);

        mCheckAuto = (CheckBox) findViewById(R.id.ca_auto);
        mSelectCert = (RelativeLayout) findViewById(R.id.select_certificate);

        mShowAdvanced = (CheckBox) findViewById(R.id.show_advanced);
        mAdvancedSettings = (ViewGroup) findViewById(R.id.advanced_settings);

        mRemoteId = (MultiAutoCompleteTextView) findViewById(R.id.remote_id);
        mRemoteIdWrap = (TextInputLayoutHelper) findViewById(R.id.remote_id_wrap);
        mMTU = (EditText) findViewById(R.id.mtu);
        mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap);
        mPort = (EditText) findViewById(R.id.port);
        mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap);
        mNATKeepalive = (EditText) findViewById(R.id.nat_keepalive);
        mNATKeepaliveWrap = (TextInputLayoutHelper) findViewById(R.id.nat_keepalive_wrap);
        mCertReq = (Switch) findViewById(R.id.cert_req);
        mIncludedSubnets = (EditText) findViewById(R.id.included_subnets);
        mIncludedSubnetsWrap = (TextInputLayoutHelper) findViewById(R.id.included_subnets_wrap);
        mExcludedSubnets = (EditText) findViewById(R.id.excluded_subnets);
        mExcludedSubnetsWrap = (TextInputLayoutHelper) findViewById(R.id.excluded_subnets_wrap);
        mBlockIPv4 = (CheckBox) findViewById(R.id.split_tunneling_v4);
        mBlockIPv6 = (CheckBox) findViewById(R.id.split_tunneling_v6);

        mSelectSelectedAppsHandling = (Spinner) findViewById(R.id.apps_handling);
        mSelectApps = (RelativeLayout) findViewById(R.id.select_applications);

        mIkeProposal = (EditText) findViewById(R.id.ike_proposal);
        mIkeProposalWrap = (TextInputLayoutHelper) findViewById(R.id.ike_proposal_wrap);
        mEspProposal = (EditText) findViewById(R.id.esp_proposal);
        mEspProposalWrap = (TextInputLayoutHelper) findViewById(R.id.esp_proposal_wrap);
        /* make the link clickable */
        ((TextView) findViewById(R.id.proposal_intro)).setMovementMethod(LinkMovementMethod.getInstance());

        final SpaceTokenizer spaceTokenizer = new SpaceTokenizer();
        mName.setTokenizer(spaceTokenizer);
        mRemoteId.setTokenizer(spaceTokenizer);
        final ArrayAdapter<String> completeAdapter = new ArrayAdapter<>(this,
                android.R.layout.simple_dropdown_item_1line);
        mName.setAdapter(completeAdapter);
        mRemoteId.setAdapter(completeAdapter);

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
            findViewById(R.id.apps).setVisibility(View.GONE);
            mSelectSelectedAppsHandling.setVisibility(View.GONE);
            mSelectApps.setVisibility(View.GONE);
        }

        mGateway.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
            }

            @Override
            public void afterTextChanged(Editable s) {
                completeAdapter.clear();
                completeAdapter.add(mGateway.getText().toString());
                if (TextUtils.isEmpty(mGateway.getText())) {
                    mNameWrap.setHelperText(getString(R.string.profile_name_hint));
                    mRemoteIdWrap.setHelperText(getString(R.string.profile_remote_id_hint));
                } else {
                    mNameWrap.setHelperText(
                            String.format(getString(R.string.profile_name_hint_gateway), mGateway.getText()));
                    mRemoteIdWrap.setHelperText(
                            String.format(getString(R.string.profile_remote_id_hint_gateway), mGateway.getText()));
                }
            }
        });

        mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                mVpnType = VpnType.values()[position];
                updateCredentialView();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) { /* should not happen */
                mVpnType = VpnType.IKEV2_EAP;
                updateCredentialView();
            }
        });

        ((TextView) mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title);
        ((TextView) mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle);
        mTncNotice.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                new TncNoticeDialog().show(VpnProfileDetailActivity.this.getSupportFragmentManager(), "TncNotice");
            }
        });

        mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
        mSelectUserIdAdapter = new CertificateIdentitiesAdapter(this);
        mSelectUserId.setAdapter(mSelectUserIdAdapter);
        mSelectUserId.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                if (mUserCertEntry != null) { /* we don't store the subject DN as it is in the reverse order and the default anyway */
                    mSelectedUserId = position == 0 ? null : mSelectUserIdAdapter.getItem(position);
                }
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) {
                mSelectedUserId = null;
            }
        });

        mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                updateCertificateSelector();
            }
        });

        mSelectCert.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(VpnProfileDetailActivity.this, TrustedCertificatesActivity.class);
                intent.setAction(TrustedCertificatesActivity.SELECT_CERTIFICATE);
                startActivityForResult(intent, SELECT_TRUSTED_CERTIFICATE);
            }
        });

        mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                updateAdvancedSettings();
            }
        });

        mSelectSelectedAppsHandling.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                mSelectedAppsHandling = SelectedAppsHandling.values()[position];
                updateAppsSelector();
            }

            @Override
            public void onNothingSelected(AdapterView<?> parent) { /* should not happen */
                mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE;
                updateAppsSelector();
            }
        });

        mSelectApps.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(VpnProfileDetailActivity.this, SelectedApplicationsActivity.class);
                intent.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
                startActivityForResult(intent, SELECT_APPLICATIONS);
            }
        });

        mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID);
        if (mId == null) {
            Bundle extras = getIntent().getExtras();
            mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID);
        }

        loadProfileData(savedInstanceState);

        updateCredentialView();
        updateCertificateSelector();
        updateAdvancedSettings();
        updateAppsSelector();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mDataSource.close();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        if (mId != null) {
            outState.putLong(VpnProfileDataSource.KEY_ID, mId);
        }
        if (mUserCertEntry != null) {
            outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
        }
        if (mSelectedUserId != null) {
            outState.putString(VpnProfileDataSource.KEY_LOCAL_ID, mSelectedUserId);
        }
        if (mCertEntry != null) {
            outState.putString(VpnProfileDataSource.KEY_CERTIFICATE, mCertEntry.getAlias());
        }
        outState.putStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps));
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.profile_edit, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case android.R.id.home:
        case R.id.menu_cancel:
            finish();
            return true;
        case R.id.menu_accept:
            saveProfile();
            return true;
        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
        case SELECT_TRUSTED_CERTIFICATE:
            if (resultCode == RESULT_OK) {
                String alias = data.getStringExtra(VpnProfileDataSource.KEY_CERTIFICATE);
                X509Certificate certificate = TrustedCertificateManager.getInstance()
                        .getCACertificateFromAlias(alias);
                mCertEntry = certificate == null ? null : new TrustedCertificateEntry(alias, certificate);
                updateCertificateSelector();
            }
            break;
        case SELECT_APPLICATIONS:
            if (resultCode == RESULT_OK) {
                ArrayList<String> selection = data
                        .getStringArrayListExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
                mSelectedApps = new TreeSet<>(selection);
                updateAppsSelector();
            }
            break;
        default:
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    /**
     * Update the UI to enter credentials depending on the type of VPN currently selected
     */
    private void updateCredentialView() {
        mUsernamePassword.setVisibility(mVpnType.has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
        mUserCertificate.setVisibility(mVpnType.has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
        mTncNotice.setVisibility(mVpnType.has(VpnTypeFeature.BYOD) ? View.VISIBLE : View.GONE);

        if (mVpnType.has(VpnTypeFeature.CERTIFICATE)) {
            mSelectUserId.setEnabled(false);
            if (mUserCertLoading != null) {
                ((TextView) mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
                ((TextView) mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
            } else if (mUserCertEntry != null) { /* clear any errors and set the new data */
                ((TextView) mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
                ((TextView) mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
                ((TextView) mSelectUserCert.findViewById(android.R.id.text2))
                        .setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
                mSelectUserIdAdapter.setCertificate(mUserCertEntry);
                mSelectUserId.setSelection(mSelectUserIdAdapter.getPosition(mSelectedUserId));
                mSelectUserId.setEnabled(true);
            } else {
                ((TextView) mSelectUserCert.findViewById(android.R.id.text1))
                        .setText(R.string.profile_user_select_certificate_label);
                ((TextView) mSelectUserCert.findViewById(android.R.id.text2))
                        .setText(R.string.profile_user_select_certificate);
                mSelectUserIdAdapter.setCertificate(null);
            }
        }
    }

    /**
     * Show an alert in case the previously selected certificate is not found anymore
     * or the user did not select a certificate in the spinner.
     */
    private void showCertificateAlert() {
        AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this);
        adb.setTitle(R.string.alert_text_nocertfound_title);
        adb.setMessage(R.string.alert_text_nocertfound);
        adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int id) {
                dialog.cancel();
            }
        });
        adb.show();
    }

    /**
     * Update the CA certificate selection UI depending on whether the
     * certificate should be automatically selected or not.
     */
    private void updateCertificateSelector() {
        if (!mCheckAuto.isChecked()) {
            mSelectCert.setEnabled(true);
            mSelectCert.setVisibility(View.VISIBLE);

            if (mCertEntry != null) {
                ((TextView) mSelectCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
                ((TextView) mSelectCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
            } else {
                ((TextView) mSelectCert.findViewById(android.R.id.text1))
                        .setText(R.string.profile_ca_select_certificate_label);
                ((TextView) mSelectCert.findViewById(android.R.id.text2))
                        .setText(R.string.profile_ca_select_certificate);
            }
        } else {
            mSelectCert.setEnabled(false);
            mSelectCert.setVisibility(View.GONE);
        }
    }

    /**
     * Update the application selection UI
     */
    private void updateAppsSelector() {
        if (mSelectedAppsHandling == SelectedAppsHandling.SELECTED_APPS_DISABLE) {
            mSelectApps.setEnabled(false);
            mSelectApps.setVisibility(View.GONE);

        } else {
            mSelectApps.setEnabled(true);
            mSelectApps.setVisibility(View.VISIBLE);

            ((TextView) mSelectApps.findViewById(android.R.id.text1)).setText(R.string.profile_select_apps);
            String selected;
            switch (mSelectedApps.size()) {
            case 0:
                selected = getString(R.string.profile_select_no_apps);
                break;
            case 1:
                selected = getString(R.string.profile_select_one_app);
                break;
            default:
                selected = getString(R.string.profile_select_x_apps, mSelectedApps.size());
                break;
            }
            ((TextView) mSelectApps.findViewById(android.R.id.text2)).setText(selected);
        }
    }

    /**
     * Update the advanced settings UI depending on whether any advanced
     * settings have already been made.
     */
    private void updateAdvancedSettings() {
        boolean show = mShowAdvanced.isChecked();
        if (!show && mProfile != null) {
            Integer st = mProfile.getSplitTunneling(), flags = mProfile.getFlags();
            show = mProfile.getRemoteId() != null || mProfile.getMTU() != null || mProfile.getPort() != null
                    || mProfile.getNATKeepAlive() != null || (flags != null && flags != 0)
                    || (st != null && st != 0) || mProfile.getIncludedSubnets() != null
                    || mProfile.getExcludedSubnets() != null
                    || mProfile.getSelectedAppsHandling() != SelectedAppsHandling.SELECTED_APPS_DISABLE
                    || mProfile.getIkeProposal() != null || mProfile.getEspProposal() != null;
        }
        mShowAdvanced.setVisibility(!show ? View.VISIBLE : View.GONE);
        mAdvancedSettings.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    /**
     * Save or update the profile depending on whether we actually have a
     * profile object or not (this was created in updateProfileData)
     */
    private void saveProfile() {
        if (verifyInput()) {
            if (mProfile != null) {
                updateProfileData();
                if (mProfile.getUUID() == null) {
                    mProfile.setUUID(UUID.randomUUID());
                }
                mDataSource.updateVpnProfile(mProfile);
            } else {
                mProfile = new VpnProfile();
                updateProfileData();
                mDataSource.insertProfile(mProfile);
            }
            Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
            intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
            LocalBroadcastManager.getInstance(this).sendBroadcast(intent);

            setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
            finish();
        }
    }

    /**
     * Verify the user input and display error messages.
     * @return true if the input is valid
     */
    private boolean verifyInput() {
        boolean valid = true;
        if (mGateway.getText().toString().trim().isEmpty()) {
            mGatewayWrap.setError(getString(R.string.alert_text_no_input_gateway));
            valid = false;
        }
        if (mVpnType.has(VpnTypeFeature.USER_PASS)) {
            if (mUsername.getText().toString().trim().isEmpty()) {
                mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
                valid = false;
            }
        }
        if (mVpnType.has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null) { /* let's show an error icon */
            ((TextView) mSelectUserCert.findViewById(android.R.id.text1)).setError("");
            valid = false;
        }
        if (!mCheckAuto.isChecked() && mCertEntry == null) {
            showCertificateAlert();
            valid = false;
        }
        if (!validateInteger(mMTU, Constants.MTU_MIN, Constants.MTU_MAX)) {
            mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), Constants.MTU_MIN,
                    Constants.MTU_MAX));
            valid = false;
        }
        if (!validateSubnets(mIncludedSubnets)) {
            mIncludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
            valid = false;
        }
        if (!validateSubnets(mExcludedSubnets)) {
            mExcludedSubnetsWrap.setError(getString(R.string.alert_text_no_subnets));
            valid = false;
        }
        if (!validateInteger(mPort, 1, 65535)) {
            mPortWrap.setError(String.format(getString(R.string.alert_text_out_of_range), 1, 65535));
            valid = false;
        }
        if (!validateInteger(mNATKeepalive, Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX)) {
            mNATKeepaliveWrap.setError(String.format(getString(R.string.alert_text_out_of_range),
                    Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX));
            valid = false;
        }
        if (!validateProposal(mIkeProposal, true)) {
            mIkeProposalWrap.setError(getString(R.string.alert_text_no_proposal));
            valid = false;
        }
        if (!validateProposal(mEspProposal, false)) {
            mEspProposalWrap.setError(getString(R.string.alert_text_no_proposal));
            valid = false;
        }
        return valid;
    }

    /**
     * Update the profile object with the data entered by the user
     */
    private void updateProfileData() {
        /* the name is optional, we default to the gateway if none is given */
        String name = mName.getText().toString().trim();
        String gateway = mGateway.getText().toString().trim();
        mProfile.setName(name.isEmpty() ? gateway : name);
        mProfile.setGateway(gateway);
        mProfile.setVpnType(mVpnType);
        if (mVpnType.has(VpnTypeFeature.USER_PASS)) {
            mProfile.setUsername(mUsername.getText().toString().trim());
            String password = mPassword.getText().toString().trim();
            password = password.isEmpty() ? null : password;
            mProfile.setPassword(password);
        }
        if (mVpnType.has(VpnTypeFeature.CERTIFICATE)) {
            mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
            mProfile.setLocalId(mSelectedUserId);
        }
        String certAlias = mCheckAuto.isChecked() ? null : mCertEntry.getAlias();
        mProfile.setCertificateAlias(certAlias);
        String remote_id = mRemoteId.getText().toString().trim();
        mProfile.setRemoteId(remote_id.isEmpty() ? null : remote_id);
        mProfile.setMTU(getInteger(mMTU));
        mProfile.setPort(getInteger(mPort));
        mProfile.setNATKeepAlive(getInteger(mNATKeepalive));
        int flags = 0;
        flags |= !mCertReq.isChecked() ? VpnProfile.FLAGS_SUPPRESS_CERT_REQS : 0;
        mProfile.setFlags(flags);
        String included = mIncludedSubnets.getText().toString().trim();
        mProfile.setIncludedSubnets(included.isEmpty() ? null : included);
        String excluded = mExcludedSubnets.getText().toString().trim();
        mProfile.setExcludedSubnets(excluded.isEmpty() ? null : excluded);
        int st = 0;
        st |= mBlockIPv4.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
        st |= mBlockIPv6.isChecked() ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
        mProfile.setSplitTunneling(st == 0 ? null : st);
        mProfile.setSelectedAppsHandling(mSelectedAppsHandling);
        mProfile.setSelectedApps(mSelectedApps);
        String ike = mIkeProposal.getText().toString().trim();
        mProfile.setIkeProposal(ike.isEmpty() ? null : ike);
        String esp = mEspProposal.getText().toString().trim();
        mProfile.setEspProposal(esp.isEmpty() ? null : esp);
    }

    /**
     * Load an existing profile if we got an ID
     *
     * @param savedInstanceState previously saved state
     */
    private void loadProfileData(Bundle savedInstanceState) {
        String useralias = null, local_id = null, alias = null;
        Integer flags = null;

        getSupportActionBar().setTitle(R.string.add_profile);
        if (mId != null && mId != 0) {
            mProfile = mDataSource.getVpnProfile(mId);
            if (mProfile != null) {
                mName.setText(mProfile.getName());
                mGateway.setText(mProfile.getGateway());
                mVpnType = mProfile.getVpnType();
                mUsername.setText(mProfile.getUsername());
                mPassword.setText(mProfile.getPassword());
                mRemoteId.setText(mProfile.getRemoteId());
                mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null);
                mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null);
                mNATKeepalive
                        .setText(mProfile.getNATKeepAlive() != null ? mProfile.getNATKeepAlive().toString() : null);
                mIncludedSubnets.setText(mProfile.getIncludedSubnets());
                mExcludedSubnets.setText(mProfile.getExcludedSubnets());
                mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null
                        && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0);
                mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null
                        && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0);
                mSelectedAppsHandling = mProfile.getSelectedAppsHandling();
                mSelectedApps = mProfile.getSelectedAppsSet();
                mIkeProposal.setText(mProfile.getIkeProposal());
                mEspProposal.setText(mProfile.getEspProposal());
                flags = mProfile.getFlags();
                useralias = mProfile.getUserCertificateAlias();
                local_id = mProfile.getLocalId();
                alias = mProfile.getCertificateAlias();
                getSupportActionBar().setTitle(mProfile.getName());
            } else {
                Log.e(VpnProfileDetailActivity.class.getSimpleName(), "VPN profile with id " + mId + " not found");
                finish();
            }
        }

        mSelectVpnType.setSelection(mVpnType.ordinal());
        mCertReq.setChecked(flags == null || (flags & VpnProfile.FLAGS_SUPPRESS_CERT_REQS) == 0);

        /* check if the user selected a user certificate previously */
        useralias = savedInstanceState == null ? useralias
                : savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
        local_id = savedInstanceState == null ? local_id
                : savedInstanceState.getString(VpnProfileDataSource.KEY_LOCAL_ID);
        if (useralias != null) {
            UserCertificateLoader loader = new UserCertificateLoader(this, useralias);
            mUserCertLoading = useralias;
            mSelectedUserId = local_id;
            loader.execute();
        }

        /* check if the user selected a CA certificate previously */
        alias = savedInstanceState == null ? alias
                : savedInstanceState.getString(VpnProfileDataSource.KEY_CERTIFICATE);
        mCheckAuto.setChecked(alias == null);
        if (alias != null) {
            X509Certificate certificate = TrustedCertificateManager.getInstance().getCACertificateFromAlias(alias);
            if (certificate != null) {
                mCertEntry = new TrustedCertificateEntry(alias, certificate);
            } else { /* previously selected certificate is not here anymore */
                showCertificateAlert();
                mCertEntry = null;
            }
        }

        mSelectSelectedAppsHandling.setSelection(mSelectedAppsHandling.ordinal());
        if (savedInstanceState != null) {
            ArrayList<String> selectedApps = savedInstanceState
                    .getStringArrayList(VpnProfileDataSource.KEY_SELECTED_APPS_LIST);
            mSelectedApps = new TreeSet<>(selectedApps);
        }
    }

    /**
     * Get the integer value in the given text box or null if empty
     *
     * @param view text box (numeric entry assumed)
     */
    private Integer getInteger(EditText view) {
        String value = view.getText().toString().trim();
        try {
            return value.isEmpty() ? null : Integer.valueOf(value);
        } catch (NumberFormatException e) {
            return null;
        }
    }

    /**
     * Check that the value in the given text box is a valid integer in the given range
     *
     * @param view text box (numeric entry assumed)
     * @param min minimum value (inclusive)
     * @param max maximum value (inclusive)
     */
    private boolean validateInteger(EditText view, Integer min, Integer max) {
        String value = view.getText().toString().trim();
        try {
            if (value.isEmpty()) {
                return true;
            }
            Integer val = Integer.valueOf(value);
            return min <= val && val <= max;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    /**
     * Check that the value in the given text box is a valid list of subnets/ranges
     *
     * @param view text box
     */
    private boolean validateSubnets(EditText view) {
        String value = view.getText().toString().trim();
        return value.isEmpty() || IPRangeSet.fromString(value) != null;
    }

    /**
     * Check that the value in the given text box is a valid proposal
     *
     * @param view text box
     */
    private boolean validateProposal(EditText view, boolean ike) {
        String value = view.getText().toString().trim();
        return value.isEmpty() || Utils.isProposalValid(ike, value);
    }

    private class SelectUserCertOnClickListener implements OnClickListener, KeyChainAliasCallback {
        @Override
        public void onClick(View v) {
            String useralias = mUserCertEntry != null ? mUserCertEntry.getAlias() : null;
            KeyChain.choosePrivateKeyAlias(VpnProfileDetailActivity.this, this, new String[] { "RSA" }, null, null,
                    -1, useralias);
        }

        @Override
        public void alias(final String alias) {
            if (alias != null) { /* otherwise the dialog was canceled, the request denied */
                try {
                    final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this,
                            alias);
                    /* alias() is not called from our main thread */
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            if (chain != null && chain.length > 0) {
                                mUserCertEntry = new TrustedCertificateEntry(alias, chain[0]);
                            }
                            updateCredentialView();
                        }
                    });
                } catch (KeyChainException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * Load the selected user certificate asynchronously.  This cannot be done
     * from the main thread as getCertificateChain() calls back to our main
     * thread to bind to the KeyChain service resulting in a deadlock.
     */
    private class UserCertificateLoader extends AsyncTask<Void, Void, X509Certificate> {
        private final Context mContext;
        private final String mAlias;

        public UserCertificateLoader(Context context, String alias) {
            mContext = context;
            mAlias = alias;
        }

        @Override
        protected X509Certificate doInBackground(Void... params) {
            X509Certificate[] chain = null;
            try {
                chain = KeyChain.getCertificateChain(mContext, mAlias);
            } catch (KeyChainException | InterruptedException e) {
                e.printStackTrace();
            }
            if (chain != null && chain.length > 0) {
                return chain[0];
            }
            return null;
        }

        @Override
        protected void onPostExecute(X509Certificate result) {
            if (result != null) {
                mUserCertEntry = new TrustedCertificateEntry(mAlias, result);
            } else { /* previously selected certificate is not here anymore */
                ((TextView) mSelectUserCert.findViewById(android.R.id.text1)).setError("");
                mUserCertEntry = null;
            }
            mUserCertLoading = null;
            updateCredentialView();
        }
    }

    /**
     * Dialog with notification message if EAP-TNC is used.
     */
    public static class TncNoticeDialog extends AppCompatDialogFragment {
        @Override
        public Dialog onCreateDialog(Bundle savedInstanceState) {
            return new AlertDialog.Builder(getActivity()).setTitle(R.string.tnc_notice_title)
                    .setMessage(Html.fromHtml(getString(R.string.tnc_notice_details)))
                    .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            dialog.dismiss();
                        }
                    }).create();
        }
    }

    /**
     * Tokenizer implementation that separates by white-space
     */
    public static class SpaceTokenizer implements MultiAutoCompleteTextView.Tokenizer {
        @Override
        public int findTokenStart(CharSequence text, int cursor) {
            int i = cursor;

            while (i > 0 && !Character.isWhitespace(text.charAt(i - 1))) {
                i--;
            }
            return i;
        }

        @Override
        public int findTokenEnd(CharSequence text, int cursor) {
            int i = cursor;
            int len = text.length();

            while (i < len) {
                if (Character.isWhitespace(text.charAt(i))) {
                    return i;
                } else {
                    i++;
                }
            }
            return len;
        }

        @Override
        public CharSequence terminateToken(CharSequence text) {
            int i = text.length();

            if (i > 0 && Character.isWhitespace(text.charAt(i - 1))) {
                return text;
            } else {
                if (text instanceof Spanned) {
                    SpannableString sp = new SpannableString(text + " ");
                    TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0);
                    return sp;
                } else {
                    return text + " ";
                }
            }
        }
    }
}