Always-on VPN.

Adds support for always-on VPN profiles. Users pick an always-on VPN
from list of existing VPN profiles, which must use an IP address for
both VPN server and DNS.  Moved "add" operation into action bar.

Bug: 5756357
Change-Id: I4c7ed7f2a3b027be1baf65c08213336a61f3acfe
This commit is contained in:
Jeff Sharkey
2012-08-25 00:06:08 -07:00
parent 5e61ba5576
commit 9fd7ac1ec7
6 changed files with 239 additions and 26 deletions

View File

@@ -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>

27
res/menu/vpn.xml Normal file
View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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"),
@@ -80,6 +97,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.
@@ -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;
}
}