diff options
-rw-r--r-- | res/values/arrays.xml | 36 | ||||
-rw-r--r-- | res/values/strings.xml | 59 | ||||
-rw-r--r-- | res/values/styles.xml | 10 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/VpnDialog.java | 325 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/VpnProfile.java | 115 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/VpnSettings.java | 347 |
6 files changed, 885 insertions, 7 deletions
diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 3f2fc23..c0578fa 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -612,5 +612,39 @@ <item>Use HDCP checking for DRM content only</item> <item>Always use HDCP checking</item> </string-array> -</resources> + <!-- Match this with the constants in VpnProfile. --> <skip /> + <!-- Short names for each VPN type, not really translatable. [CHAR LIMIT=20] --> + <string-array name="vpn_types" translatable="false"> + <item>PPTP</item> + <item>L2TP/IPSec PSK</item> + <item>L2TP/IPSec RSA</item> + <item>IPSec Xauth PSK</item> + <item>IPSec Xauth RSA</item> + <item>IPSec Hybrid RSA</item> + </string-array> + + <!-- Match this with the constants in VpnProfile. --> <skip /> + <!-- Longer descriptions for each VPN type. [CHAR LIMIT=100] --> + <string-array name="vpn_types_long"> + <item>PPTP VPN</item> + <item>L2TP/IPSec VPN with pre-shared keys</item> + <item>L2TP/IPSec VPN with certificates</item> + <item>IPSec VPN with pre-shared keys and Xauth authentication</item> + <item>IPSec VPN with certificates and Xauth authentication</item> + <item>IPSec VPN with certificates and hybrid authentication</item> + </string-array> + + <!-- Match this with the constants in VpnProfile. --> <skip /> + <!-- Status for a VPN network. [CHAR LIMIT=100] --> + <string-array name="vpn_states"> + <!-- Status message when VPN is connecting. --> + <item>Connecting\u2026</item> + <!-- Status message when VPN is connected. --> + <item>Connected</item> + <!-- Status message when VPN is disconnected. --> + <item>Disconnected</item> + <!-- Status message when VPN failed to connect. --> + <item>Failed</item> + </string-array> +</resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 6c1ca6f..ed9f949 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2912,8 +2912,6 @@ found in the list of installed applications.</string> <string name="vpn_settings_activity_title">VPN settings</string> - <!-- Title of VPN connect dialog --> - <string name="vpn_connect_to">Connect to <xliff:g id="name" example="Work Network">%s</xliff:g></string> <!-- In VPN connect dialog, for inputing username and password --> <string name="vpn_username_colon">Username:</string> <string name="vpn_password_colon">Password:</string> @@ -2934,8 +2932,6 @@ found in the list of installed applications.</string> <string name="vpn_menu_revert">Revert</string> <string name="vpn_menu_connect">Connect to network</string> <string name="vpn_menu_disconnect">Disconnect from network</string> - <string name="vpn_menu_edit">Edit network</string> - <string name="vpn_menu_delete">Delete network</string> <!-- VPN error dialog messages --> <string name="vpn_error_miss_entering">You must enter <xliff:g id="code">%s</xliff:g>.</string> @@ -2976,7 +2972,6 @@ found in the list of installed applications.</string> <string name="vpn_connect_hint">Connect to network</string> <!-- Name of a VPN profile --> - <string name="vpn_name">VPN name</string> <string name="vpn_a_name">a VPN name</string> <!-- Toast message shown when a profile is added --> @@ -2998,7 +2993,6 @@ found in the list of installed applications.</string> <!-- Preference title --> <string name="vpn_l2tp_secret_string_title">Set L2TP secret</string> <!-- Complete term --> - <string name="vpn_l2tp_secret">L2TP secret</string> <string name="vpn_a_l2tp_secret">an L2TP secret</string> <string name="vpn_pptp_encryption_title">encryption</string> <string name="vpn_pptp_encryption">PPTP encryption</string> @@ -3443,4 +3437,57 @@ found in the list of installed applications.</string> <!-- Dialog button indicating that data connection should be re-enabled. [CHAR LIMIT=28] --> <string name="data_usage_disabled_dialog_enable">re-enable data</string> + <!-- Input label for the name of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_name">Name</string> + <!-- Input label for the type of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_type">Type</string> + <!-- Input label for the server address of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_server">Server address</string> + <!-- Checkbox label to enable PPP encryption for a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_mppe">PPP encryption (MPPE)</string> + <!-- Input label for the L2TP secret of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_l2tp_secret">L2TP secret</string> + <!-- Input label for the IPSec identifier of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_ipsec_identifier">IPSec identifier</string> + <!-- Input label for the IPSec pre-shared key of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_ipsec_secret">IPSec pre-shared key</string> + <!-- Selection label for the IPSec user certificate of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_ipsec_user_cert">IPSec user certificate</string> + <!-- Selection label for the IPSec CA certificate of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_ipsec_ca_cert">IPSec CA certificate</string> + <!-- Input label for the DNS search domains of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_domains">DNS search domains</string> + <!-- Input label for the forwarding routes of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_routes">Forwarding routes</string> + <!-- Input label for the username of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_username">Username</string> + <!-- Input label for the password of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_password">Password</string> + <!-- Checkbox label to save the username and the password for a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_save_login">Save this information</string> + + <!-- Hint for an optional input of a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_not_used">(not used)</string> + <!-- Option to not use a CA certificate to verify the VPN server. [CHAR LIMIT=40] --> + <string name="vpn_no_ca_cert">(do not verify server)</string> + + <!-- Button label to cancel chaning a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_cancel">Cancel</string> + <!-- Button label to save a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_save">Save</string> + <!-- Button label to connect to a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_connect">Connect</string> + <!-- Dialog title to edit a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_edit">Edit VPN network</string> + <!-- Dialog title to connect to a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_connect_to">Connect to <xliff:g id="network" example="School">%s</xliff:g></string> + + <!-- Preference title for VPN settings. [CHAR LIMIT=40] --> + <string name="vpn_title">VPN settings</string> + <!-- Preference title to create a new VPN network. [CHAR LIMIT=40] --> + <string name="vpn_create">Add VPN network</string> + <!-- Menu item to edit a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_menu_edit">Edit network</string> + <!-- Menu item to delete a VPN network. [CHAR LIMIT=40] --> + <string name="vpn_menu_delete">Delete network</string> </resources> diff --git a/res/values/styles.xml b/res/values/styles.xml index 1a6380f..2f09901 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -138,4 +138,14 @@ <item name="android:singleLine">true</item> </style> + <style name="vpn_label"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:textSize">14sp</item> + </style> + + <style name="vpn_value"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">wrap_content</item> + </style> </resources> diff --git a/src/com/android/settings/vpn2/VpnDialog.java b/src/com/android/settings/vpn2/VpnDialog.java new file mode 100644 index 0000000..b3e417b --- /dev/null +++ b/src/com/android/settings/vpn2/VpnDialog.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import com.android.settings.R; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.security.Credentials; +import android.security.KeyStore; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.Spinner; +import android.widget.TextView; + +class VpnDialog extends AlertDialog implements TextWatcher, OnItemSelectedListener { + private static final String DUMMY = "\r\r\r\r"; + + private static String getDummy(String secret) { + return secret.isEmpty() ? "" : DUMMY; + } + + private static String getSecret(TextView dummy) { + String secret = dummy.getText().toString(); + return DUMMY.equals(secret) ? "" : secret; + } + + private final KeyStore mKeyStore = KeyStore.getInstance(); + private final DialogInterface.OnClickListener mListener; + private final VpnProfile mProfile; + + private boolean mEditing; + + private View mView; + + private TextView mName; + private Spinner mType; + private TextView mServer; + private TextView mUsername; + private TextView mPassword; + private TextView mDomains; + private TextView mRoutes; + private CheckBox mMppe; + private TextView mL2tpSecret; + private TextView mIpsecIdentifier; + private TextView mIpsecSecret; + private Spinner mIpsecUserCert; + private Spinner mIpsecCaCert; + private CheckBox mSaveLogin; + + VpnDialog(Context context, DialogInterface.OnClickListener listener, + VpnProfile profile, boolean editing) { + super(context); + mListener = listener; + mProfile = profile; + mEditing = editing; + } + + @Override + protected void onCreate(Bundle savedState) { + mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null); + setView(mView); + setInverseBackgroundForced(true); + + Context context = getContext(); + + // First, find out all the fields. + mName = (TextView) mView.findViewById(R.id.name); + mType = (Spinner) mView.findViewById(R.id.type); + mServer = (TextView) mView.findViewById(R.id.server); + mUsername = (TextView) mView.findViewById(R.id.username); + mPassword = (TextView) mView.findViewById(R.id.password); + mDomains = (TextView) mView.findViewById(R.id.domains); + mRoutes = (TextView) mView.findViewById(R.id.routes); + mMppe = (CheckBox) mView.findViewById(R.id.mppe); + mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret); + mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier); + mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret); + mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert); + mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert); + mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login); + + // Second, copy values from the profile. + mName.setText(mProfile.name); + mType.setSelection(mProfile.type); + mServer.setText(mProfile.server); + mUsername.setText(mProfile.username); + mPassword.setText(getDummy(mProfile.password)); + mDomains.setText(mProfile.domains); + mRoutes.setText(mProfile.routes); + mMppe.setChecked(mProfile.mppe); + mL2tpSecret.setText(getDummy(mProfile.l2tpSecret)); + mIpsecIdentifier.setText(mProfile.ipsecIdentifier); + mIpsecSecret.setText(getDummy(mProfile.ipsecSecret)); + loadCertificates(mIpsecUserCert, Credentials.USER_CERTIFICATE, + 0, mProfile.ipsecUserCert); + loadCertificates(mIpsecUserCert, Credentials.CA_CERTIFICATE, + R.string.vpn_no_ca_cert, mProfile.ipsecCaCert); + mSaveLogin.setChecked(mProfile.saveLogin); + + // Third, add listeners to required fields. + mName.addTextChangedListener(this); + mType.setOnItemSelectedListener(this); + mServer.addTextChangedListener(this); + mUsername.addTextChangedListener(this); + mPassword.addTextChangedListener(this); + mIpsecSecret.addTextChangedListener(this); + mIpsecUserCert.setOnItemSelectedListener(this); + + // Forth, determine to do editing or connecting. + boolean valid = validate(true); + mEditing = mEditing || !valid; + + if (mEditing) { + setTitle(R.string.vpn_edit); + + // Show common fields. + mView.findViewById(R.id.editor).setVisibility(View.VISIBLE); + + // Show type-specific fields. + changeType(mProfile.type); + + // Create a button to save the profile. + setButton(DialogInterface.BUTTON_POSITIVE, + context.getString(R.string.vpn_save), mListener); + } else { + setTitle(context.getString(R.string.vpn_connect_to, mProfile.name)); + + // Not editing, just show username and password. + mView.findViewById(R.id.login).setVisibility(View.VISIBLE); + + // Create a button to connect the network. + setButton(DialogInterface.BUTTON_POSITIVE, + context.getString(R.string.vpn_connect), mListener); + } + + // Always provide a cancel button. + setButton(DialogInterface.BUTTON_NEGATIVE, + context.getString(R.string.vpn_cancel), mListener); + + // Let AlertDialog create everything. + super.onCreate(null); + + // Disable the action button if necessary. + getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(mEditing ? valid : validate(false)); + } + + @Override + public void afterTextChanged(Editable field) { + getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); + } + + @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 onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (parent == mType) { + changeType(position); + } + getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(false)); + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + } + + private void changeType(int type) { + // First, hide everything. + mMppe.setVisibility(View.GONE); + mView.findViewById(R.id.l2tp).setVisibility(View.GONE); + mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE); + mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE); + mView.findViewById(R.id.ipsec_ca).setVisibility(View.GONE); + + // Then, unhide type-specific fields. + switch (type) { + case VpnProfile.TYPE_PPTP: + mMppe.setVisibility(View.VISIBLE); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + mView.findViewById(R.id.ipsec_ca).setVisibility(View.VISIBLE); + // fall through + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE); + break; + } + } + + private boolean validate(boolean editing) { + if (!editing) { + return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; + } + if (mName.getText().length() == 0 || mServer.getText().length() == 0) { + return false; + } + switch (mType.getSelectedItemPosition()) { + case VpnProfile.TYPE_PPTP: + return true; + + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + return mIpsecSecret.getText().length() != 0; + + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + return mIpsecUserCert.getSelectedItemPosition() != 0; + } + return false; + } + + private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) { + Context context = getContext(); + String first = (firstId == 0) ? "" : context.getString(firstId); + String[] certs = mKeyStore.saw(prefix); + + if (certs == null || certs.length == 0) { + certs = new String[] {first}; + } else { + String[] array = new String[certs.length + 1]; + array[0] = first; + System.arraycopy(certs, 0, array, 1, certs.length); + certs = array; + } + + ArrayAdapter<String> adapter = new ArrayAdapter<String>( + context, android.R.layout.simple_spinner_item, certs); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + + for (int i = 1; i < certs.length; ++i) { + if (certs[i].equals(selected)) { + spinner.setSelection(i); + break; + } + } + } + + boolean isEditing() { + return mEditing; + } + + VpnProfile getProfile() { + // First, save common fields. + VpnProfile profile = new VpnProfile(mProfile.key); + profile.name = mName.getText().toString(); + profile.type = mType.getSelectedItemPosition(); + profile.server = mServer.getText().toString().trim(); + profile.username = mUsername.getText().toString(); + profile.password = getSecret(mPassword); + profile.domains = mDomains.getText().toString().trim(); + profile.routes = mRoutes.getText().toString().trim(); + + // Then, save type-specific fields. + switch (profile.type) { + case VpnProfile.TYPE_PPTP: + profile.mppe = mMppe.isChecked(); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_PSK: + profile.l2tpSecret = getSecret(mL2tpSecret); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_PSK: + profile.ipsecSecret = getSecret(mIpsecSecret); + break; + + case VpnProfile.TYPE_L2TP_IPSEC_RSA: + profile.l2tpSecret = getSecret(mL2tpSecret); + // fall through + case VpnProfile.TYPE_IPSEC_XAUTH_RSA: + if (mIpsecCaCert.getSelectedItemPosition() != 0) { + profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem(); + } + // fall through + case VpnProfile.TYPE_IPSEC_HYBRID_RSA: + if (mIpsecUserCert.getSelectedItemPosition() != 0) { + profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem(); + } + break; + } + + profile.saveLogin = mSaveLogin.isChecked(); + return profile; + } +} diff --git a/src/com/android/settings/vpn2/VpnProfile.java b/src/com/android/settings/vpn2/VpnProfile.java new file mode 100644 index 0000000..9e4c528 --- /dev/null +++ b/src/com/android/settings/vpn2/VpnProfile.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import java.nio.charset.Charsets; + +/** + * Parcel-like entity class for VPN profiles. To keep things simple, all + * fields are package private. Methods are provided for serialization, so + * storage can be implemented easily. Two rules are set for this class. + * First, all fields must be kept non-null. Second, always make a copy + * using clone() before modifying. + */ +class VpnProfile implements Cloneable { + // Match these constants with R.array.vpn_types. + static final int TYPE_PPTP = 0; + static final int TYPE_L2TP_IPSEC_PSK = 1; + static final int TYPE_L2TP_IPSEC_RSA = 2; + static final int TYPE_IPSEC_XAUTH_PSK = 3; + static final int TYPE_IPSEC_XAUTH_RSA = 4; + static final int TYPE_IPSEC_HYBRID_RSA = 5; + static final int TYPE_MAX = 5; + + // Entity fields. + final String key; // -1 + String name = ""; // 0 + int type = TYPE_PPTP; // 1 + String server = ""; // 2 + String username = ""; // 3 + String password = ""; // 4 + String domains = ""; // 5 + String routes = ""; // 6 + boolean mppe = false; // 7 + String l2tpSecret = ""; // 8 + String ipsecIdentifier = "";// 9 + String ipsecSecret = ""; // 10 + String ipsecUserCert = ""; // 11 + String ipsecCaCert = ""; // 12 + + // Helper fields. + boolean saveLogin = false; + + VpnProfile(String key) { + this.key = key; + } + + static VpnProfile decode(String key, byte[] value) { + try { + if (key == null) { + return null; + } + + String[] values = new String(value, Charsets.UTF_8).split("\0", -1); + // Currently it always has 13 fields. + if (values.length < 13) { + return null; + } + + VpnProfile profile = new VpnProfile(key); + profile.name = values[0]; + profile.type = Integer.valueOf(values[1]); + if (profile.type < 0 || profile.type > TYPE_MAX) { + return null; + } + profile.server = values[2]; + profile.username = values[3]; + profile.password = values[4]; + profile.domains = values[5]; + profile.routes = values[6]; + profile.mppe = Boolean.valueOf(values[7]); + profile.l2tpSecret = values[8]; + profile.ipsecIdentifier = values[9]; + profile.ipsecSecret = values[10]; + profile.ipsecUserCert = values[11]; + profile.ipsecCaCert = values[12]; + + profile.saveLogin = !profile.username.isEmpty() || !profile.password.isEmpty(); + return profile; + } catch (Exception e) { + // ignore + } + return null; + } + + byte[] encode() { + StringBuilder builder = new StringBuilder(name); + builder.append('\0').append(type); + builder.append('\0').append(server); + builder.append('\0').append(saveLogin ? username : ""); + builder.append('\0').append(saveLogin ? password : ""); + builder.append('\0').append(domains); + builder.append('\0').append(routes); + builder.append('\0').append(mppe); + builder.append('\0').append(l2tpSecret); + builder.append('\0').append(ipsecIdentifier); + builder.append('\0').append(ipsecSecret); + builder.append('\0').append(ipsecUserCert); + builder.append('\0').append(ipsecCaCert); + return builder.toString().getBytes(Charsets.UTF_8); + } +} diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java new file mode 100644 index 0000000..6662dd9 --- /dev/null +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.vpn2; + +import com.android.settings.R; + +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.security.Credentials; +import android.security.KeyStore; +import android.util.Log; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView.AdapterContextMenuInfo; + +import com.android.settings.SettingsPreferenceFragment; + +import java.util.HashMap; + +public class VpnSettings extends SettingsPreferenceFragment implements + Handler.Callback, Preference.OnPreferenceClickListener, + DialogInterface.OnClickListener, DialogInterface.OnDismissListener { + + private static final String TAG = "VpnSettings"; + + // Match these constants with R.array.vpn_states. + private static final int STATE_NONE = -1; + private static final int STATE_CONNECTING = 0; + private static final int STATE_CONNECTED = 1; + private static final int STATE_DISCONNECTED = 2; + private static final int STATE_FAILED = 3; + + private final KeyStore mKeyStore = KeyStore.getInstance(); + private boolean mUnlocking = false; + + private HashMap<String, VpnPreference> mPreferences; + private VpnDialog mDialog; + private String mSelectedKey; + private Handler mHandler; + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + addPreferencesFromResource(R.xml.vpn_settings2); + PreferenceGroup group = getPreferenceScreen(); + group.setOrderingAsAdded(false); + group.findPreference("add_network").setOnPreferenceClickListener(this); + + if (savedState != null) { + VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), + savedState.getByteArray("VpnProfile")); + if (profile != null) { + mDialog = new VpnDialog(getActivity(), this, profile, + savedState.getBoolean("VpnEditing")); + } + } + } + + @Override + public void onSaveInstanceState(Bundle savedState) { + // We do not save view hierarchy, as they are just profiles. + if (mDialog != null) { + VpnProfile profile = mDialog.getProfile(); + savedState.putString("VpnKey", profile.key); + savedState.putByteArray("VpnProfile", profile.encode()); + savedState.putBoolean("VpnEditing", mDialog.isEditing()); + } + // else? + } + + @Override + public void onResume() { + super.onResume(); + + // Check KeyStore here, so others do not need to deal with it. + if (mKeyStore.state() != KeyStore.State.UNLOCKED) { + if (!mUnlocking) { + // Let us unlock KeyStore. See you later! + Credentials.getInstance().unlock(getActivity()); + } else { + // We already tried, but it is still not working! + getActivity().getFragmentManager().popBackStack(); + } + mUnlocking = !mUnlocking; + return; + } + + // Now KeyStore is always unlocked. Reset the flag. + mUnlocking = false; + + // Currently we are the only user of profiles in KeyStore. + // Assuming KeyStore and KeyGuard do the right thing, we can + // safely cache profiles in the memory. + if (mPreferences == null) { + mPreferences = new HashMap<String, VpnPreference>(); + + String[] keys = mKeyStore.saw(Credentials.VPN); + if (keys != null && keys.length > 0) { + Context context = getActivity(); + + for (String key : keys) { + VpnProfile profile = VpnProfile.decode(key, + mKeyStore.get(Credentials.VPN + key)); + if (profile == null) { + Log.w(TAG, "bad profile: key = " + key); + mKeyStore.delete(Credentials.VPN + key); + } else { + VpnPreference preference = new VpnPreference(context, profile); + mPreferences.put(key, preference); + } + } + } + } + PreferenceGroup group = getPreferenceScreen(); + for (VpnPreference preference : mPreferences.values()) { + group.addPreference(preference); + } + + // Show the dialog if there is one. + if (mDialog != null) { + mDialog.setOnDismissListener(this); + mDialog.show(); + } + + // Start monitoring. + if (mHandler == null) { + mHandler = new Handler(this); + } + mHandler.sendEmptyMessage(0); + + // Register for context menu. Hmmm, getListView() is hidden? + registerForContextMenu(getListView()); + } + + @Override + public void onPause() { + super.onPause(); + + // Hide the dialog if there is one. + if (mDialog != null) { + mDialog.setOnDismissListener(null); + mDialog.dismiss(); + } + + // Unregister for context menu. + unregisterForContextMenu(getListView()); + } + + @Override + public void onDismiss(DialogInterface dialog) { + // Here is the exit of a dialog. + mDialog = null; + } + + @Override + public void onClick(DialogInterface dialog, int button) { + if (button == DialogInterface.BUTTON_POSITIVE) { + // Always save the profile. + VpnProfile profile = mDialog.getProfile(); + mKeyStore.put(Credentials.VPN + profile.key, profile.encode()); + + // Update the preference. + VpnPreference preference = mPreferences.get(profile.key); + if (preference != null) { + disconnect(profile.key); + preference.update(profile); + } else { + preference = new VpnPreference(getActivity(), profile); + mPreferences.put(profile.key, preference); + getPreferenceScreen().addPreference(preference); + } + + // If we are not editing, connect! + if (!mDialog.isEditing()) { + connect(profile.key); + } + } + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { + if (mDialog != null) { + Log.v(TAG, "onCreateContextMenu() is called when mDialog != null"); + return; + } + + if (info instanceof AdapterContextMenuInfo) { + Preference preference = (Preference) getListView().getItemAtPosition( + ((AdapterContextMenuInfo) info).position); + if (preference instanceof VpnPreference) { + VpnProfile profile = ((VpnPreference) preference).getProfile(); + mSelectedKey = profile.key; + menu.setHeaderTitle(profile.name); + menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit); + menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + if (mDialog != null) { + Log.v(TAG, "onContextItemSelected() is called when mDialog != null"); + return false; + } + + VpnPreference preference = mPreferences.get(mSelectedKey); + if (preference == null) { + Log.v(TAG, "onContextItemSelected() is called but no preference is found"); + return false; + } + + switch (item.getItemId()) { + case R.string.vpn_menu_edit: + mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true); + mDialog.setOnDismissListener(this); + mDialog.show(); + return true; + case R.string.vpn_menu_delete: + disconnect(mSelectedKey); + getPreferenceScreen().removePreference(preference); + mPreferences.remove(mSelectedKey); + mKeyStore.delete(Credentials.VPN + mSelectedKey); + return true; + } + return false; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (mDialog != null) { + Log.v(TAG, "onPreferenceClick() is called when mDialog != null"); + return true; + } + + if (preference instanceof VpnPreference) { + mDialog = new VpnDialog(getActivity(), this, + ((VpnPreference) preference).getProfile(), false); + } else { + // Generate a new key. Here we just use the current time. + long millis = System.currentTimeMillis(); + while (mPreferences.containsKey(Long.toHexString(millis))) { + ++millis; + } + mDialog = new VpnDialog(getActivity(), this, + new VpnProfile(Long.toHexString(millis)), true); + } + mDialog.setOnDismissListener(this); + mDialog.show(); + return true; + } + + @Override + public boolean handleMessage(Message message) { + mHandler.removeMessages(0); + + if (isResumed()) { + + + + + mHandler.sendEmptyMessageDelayed(0, 1000); + } + return true; + } + + private void connect(String key) { + } + + private void disconnect(String key) { + } + + + private class VpnPreference extends Preference { + private VpnProfile mProfile; + private int mState = STATE_NONE; + + VpnPreference(Context context, VpnProfile profile) { + super(context); + setPersistent(false); + setOnPreferenceClickListener(VpnSettings.this); + + mProfile = profile; + update(); + } + + VpnProfile getProfile() { + return mProfile; + } + + void update(VpnProfile profile) { + mProfile = profile; + update(); + } + + void update() { + if (mState != STATE_NONE) { + String[] states = getContext().getResources() + .getStringArray(R.array.vpn_states); + setSummary(states[mState]); + } else { + String[] types = getContext().getResources() + .getStringArray(R.array.vpn_types_long); + setSummary(types[mProfile.type]); + } + setTitle(mProfile.name); + notifyChanged(); + } + + @Override + public int compareTo(Preference preference) { + int result = 1; + if (preference instanceof VpnPreference) { + VpnPreference another = (VpnPreference) preference; + + if ((result = another.mState - mState) == 0 && + (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && + (result = mProfile.type - another.mProfile.type) == 0) { + result = mProfile.key.compareTo(another.mProfile.key); + } + } + return result; + } + } +} |