diff options
-rw-r--r-- | res/layout/vpn_lockdown_editor.xml | 38 | ||||
-rw-r--r-- | res/menu/vpn.xml | 27 | ||||
-rw-r--r-- | res/values/strings.xml | 9 | ||||
-rw-r--r-- | res/xml/vpn_settings2.xml | 4 | ||||
-rw-r--r-- | src/com/android/settings/Settings.java | 4 | ||||
-rw-r--r-- | src/com/android/settings/vpn2/VpnSettings.java | 183 |
6 files changed, 239 insertions, 26 deletions
diff --git a/res/layout/vpn_lockdown_editor.xml b/res/layout/vpn_lockdown_editor.xml new file mode 100644 index 0000000..933c5ec --- /dev/null +++ b/res/layout/vpn_lockdown_editor.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2012 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="16dip" + android:paddingRight="16dip" + android:paddingTop="8dip" + android:paddingBottom="8dip" + android:textAppearance="?android:attr/textAppearanceMedium" + android:text="@string/vpn_lockdown_summary" /> + + <ListView + android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="0dip" + android:layout_weight="1" /> + +</LinearLayout> diff --git a/res/menu/vpn.xml b/res/menu/vpn.xml new file mode 100644 index 0000000..dd8f64c --- /dev/null +++ b/res/menu/vpn.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2012 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. +--> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/vpn_create" + android:title="@string/vpn_create" + android:icon="@drawable/ic_menu_add" + android:showAsAction="always" /> + <item + android:id="@+id/vpn_lockdown" + android:title="@string/vpn_menu_lockdown" + android:showAsAction="never" /> +</menu> diff --git a/res/values/strings.xml b/res/values/strings.xml index ea7273c..eab6718 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4034,6 +4034,15 @@ <string name="vpn_menu_edit">Edit profile</string> <!-- Menu item to delete a VPN profile. [CHAR LIMIT=40] --> <string name="vpn_menu_delete">Delete profile</string> + <!-- Menu item to select always-on VPN profile. [CHAR LIMIT=40] --> + <string name="vpn_menu_lockdown">Always-on VPN</string> + + <!-- Summary describing the always-on VPN feature. [CHAR LIMIT=NONE] --> + <string name="vpn_lockdown_summary">Select a VPN profile to always remain connected to. Network traffic will only be allowed when connected to this VPN.</string> + <!-- List item indicating that no always-on VPN is selected. [CHAR LIMIT=64] --> + <string name="vpn_lockdown_none">None</string> + <!-- Error indicating that the selected VPN doesn't meet requirements. [CHAR LIMIT=NONE] --> + <string name="vpn_lockdown_config_error">Always-on VPN requires an IP address for both server and DNS.</string> <!-- Toast message when there is no network connection to start VPN. [CHAR LIMIT=100] --> <string name="vpn_no_network">There is no network connection. Please try again later.</string> diff --git a/res/xml/vpn_settings2.xml b/res/xml/vpn_settings2.xml index 38632cc..08075a6 100644 --- a/res/xml/vpn_settings2.xml +++ b/res/xml/vpn_settings2.xml @@ -16,8 +16,4 @@ <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="@string/vpn_title"> - <Preference android:key="add_network" - android:title="@string/vpn_create" - android:order="1" - android:persistent="false"/> </PreferenceScreen> diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index f8c23d7..8e3987f 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -24,6 +24,7 @@ import com.android.settings.applications.ManageApplications; import com.android.settings.bluetooth.BluetoothEnabler; import com.android.settings.deviceinfo.Memory; import com.android.settings.fuelgauge.PowerUsageSummary; +import com.android.settings.vpn2.VpnSettings; import com.android.settings.wifi.WifiEnabler; import android.accounts.Account; @@ -361,7 +362,8 @@ public class Settings extends PreferenceActivity WirelessSettings.class.getName().equals(fragmentName) || SoundSettings.class.getName().equals(fragmentName) || PrivacySettings.class.getName().equals(fragmentName) || - ManageAccountsSettings.class.getName().equals(fragmentName)) { + ManageAccountsSettings.class.getName().equals(fragmentName) || + VpnSettings.class.getName().equals(fragmentName)) { intent.putExtra(EXTRA_CLEAR_UI_OPTIONS, true); } diff --git a/src/com/android/settings/vpn2/VpnSettings.java b/src/com/android/settings/vpn2/VpnSettings.java index ecc80b0..07aa04f 100644 --- a/src/com/android/settings/vpn2/VpnSettings.java +++ b/src/com/android/settings/vpn2/VpnSettings.java @@ -16,8 +16,13 @@ package com.android.settings.vpn2; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; import android.content.Context; import android.content.DialogInterface; +import android.content.res.Resources; +import android.net.ConnectivityManager; import android.net.IConnectivityManager; import android.os.Bundle; import android.os.Handler; @@ -27,13 +32,18 @@ import android.preference.Preference; import android.preference.PreferenceGroup; import android.security.Credentials; import android.security.KeyStore; +import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.ArrayAdapter; +import android.widget.ListView; import android.widget.Toast; import com.android.internal.net.LegacyVpnInfo; @@ -41,15 +51,21 @@ import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; +import com.google.android.collect.Lists; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; public class VpnSettings extends SettingsPreferenceFragment implements Handler.Callback, Preference.OnPreferenceClickListener, DialogInterface.OnClickListener, DialogInterface.OnDismissListener { - private static final String TAG = "VpnSettings"; + private static final String TAG_LOCKDOWN = "lockdown"; + + // TODO: migrate to using DialogFragment when editing + private final IConnectivityManager mService = IConnectivityManager.Stub .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); private final KeyStore mKeyStore = KeyStore.getInstance(); @@ -67,8 +83,9 @@ public class VpnSettings extends SettingsPreferenceFragment implements @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); + + setHasOptionsMenu(true); addPreferencesFromResource(R.xml.vpn_settings2); - getPreferenceScreen().setOrderingAsAdded(false); if (savedState != null) { VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), @@ -81,6 +98,35 @@ public class VpnSettings extends SettingsPreferenceFragment implements } @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.vpn, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.vpn_create: { + // 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; + } + case R.id.vpn_lockdown: { + LockdownConfigFragment.show(this); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override public void onSaveInstanceState(Bundle savedState) { // We do not save view hierarchy, as they are just profiles. if (mDialog != null) { @@ -119,24 +165,14 @@ public class VpnSettings extends SettingsPreferenceFragment implements mPreferences = new HashMap<String, VpnPreference>(); PreferenceGroup group = getPreferenceScreen(); - 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); - group.addPreference(preference); - } - } + final Context context = getActivity(); + final List<VpnProfile> profiles = loadVpnProfiles(mKeyStore); + for (VpnProfile profile : profiles) { + final VpnPreference pref = new VpnPreference(context, profile); + pref.setOnPreferenceClickListener(this); + mPreferences.put(profile.key, pref); + group.addPreference(pref); } - group.findPreference("add_network").setOnPreferenceClickListener(this); } // Show the dialog if there is one. @@ -191,6 +227,7 @@ public class VpnSettings extends SettingsPreferenceFragment implements preference.update(profile); } else { preference = new VpnPreference(getActivity(), profile); + preference.setOnPreferenceClickListener(this); mPreferences.put(profile.key, preference); getPreferenceScreen().addPreference(preference); } @@ -340,7 +377,7 @@ public class VpnSettings extends SettingsPreferenceFragment implements return R.string.help_url_vpn; } - private class VpnPreference extends Preference { + private static class VpnPreference extends Preference { private VpnProfile mProfile; private int mState = -1; @@ -348,7 +385,6 @@ public class VpnSettings extends SettingsPreferenceFragment implements super(context); setPersistent(false); setOrder(0); - setOnPreferenceClickListener(VpnSettings.this); mProfile = profile; update(); @@ -396,4 +432,109 @@ public class VpnSettings extends SettingsPreferenceFragment implements return result; } } + + /** + * Dialog to configure always-on VPN. + */ + public static class LockdownConfigFragment extends DialogFragment { + private List<VpnProfile> mProfiles; + private List<CharSequence> mTitles; + private int mCurrentIndex; + + private static class TitleAdapter extends ArrayAdapter<CharSequence> { + public TitleAdapter(Context context, List<CharSequence> objects) { + super(context, com.android.internal.R.layout.select_dialog_singlechoice_holo, + android.R.id.text1, objects); + } + } + + public static void show(VpnSettings parent) { + if (!parent.isAdded()) return; + + final LockdownConfigFragment dialog = new LockdownConfigFragment(); + dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN); + } + + private static String getStringOrNull(KeyStore keyStore, String key) { + final byte[] value = keyStore.get(Credentials.LOCKDOWN_VPN); + return value == null ? null : new String(value); + } + + private void initProfiles(KeyStore keyStore, Resources res) { + final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN); + + mProfiles = loadVpnProfiles(keyStore); + mTitles = Lists.newArrayList(); + mTitles.add(res.getText(R.string.vpn_lockdown_none)); + mCurrentIndex = 0; + + for (VpnProfile profile : mProfiles) { + if (TextUtils.equals(profile.key, lockdownKey)) { + mCurrentIndex = mTitles.size(); + } + mTitles.add(profile.name); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Context context = getActivity(); + final KeyStore keyStore = KeyStore.getInstance(); + + initProfiles(keyStore, context.getResources()); + + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final LayoutInflater dialogInflater = LayoutInflater.from(builder.getContext()); + + builder.setTitle(R.string.vpn_menu_lockdown); + + final View view = dialogInflater.inflate(R.layout.vpn_lockdown_editor, null, false); + final ListView listView = (ListView) view.findViewById(android.R.id.list); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setAdapter(new TitleAdapter(context, mTitles)); + listView.setItemChecked(mCurrentIndex, true); + builder.setView(view); + + builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final int newIndex = listView.getCheckedItemPosition(); + if (mCurrentIndex == newIndex) return; + + if (newIndex == 0) { + keyStore.delete(Credentials.LOCKDOWN_VPN); + + } else { + final VpnProfile profile = mProfiles.get(newIndex - 1); + if (!profile.isValidLockdownProfile()) { + Toast.makeText(context, R.string.vpn_lockdown_config_error, + Toast.LENGTH_LONG).show(); + return; + } + keyStore.put(Credentials.LOCKDOWN_VPN, profile.key.getBytes()); + } + + // kick profiles since we changed them + ConnectivityManager.from(getActivity()).updateLockdownVpn(); + } + }); + + return builder.create(); + } + } + + private static List<VpnProfile> loadVpnProfiles(KeyStore keyStore) { + final ArrayList<VpnProfile> result = Lists.newArrayList(); + final String[] keys = keyStore.saw(Credentials.VPN); + if (keys != null) { + for (String key : keys) { + final VpnProfile profile = VpnProfile.decode( + key, keyStore.get(Credentials.VPN + key)); + if (profile != null) { + result.add(profile); + } + } + } + return result; + } } |