Vpn settings per vpn

This CL adds a setting for each VPN
- When no_config_vpn user restriction is applied, user can't change anything in the page
- Launch the subsetting activity in the corresponding user to unlock keystore and force work challenge
- Show dialog when user replace always-on-VPN package
- When forget VPN, unset always-on-vpn

TODO: show per-VPN status in VPN list

Change-Id: Ica360ea44117db6a4ecfaed1eec6c188189c246c
This commit is contained in:
Victor Chang
2016-03-11 19:44:56 +00:00
parent e580f94079
commit 16da2aa450
8 changed files with 397 additions and 20 deletions

View File

@@ -5174,6 +5174,8 @@
<string name="vpn_save">Save</string> <string name="vpn_save">Save</string>
<!-- Button label to connect to a VPN profile. [CHAR LIMIT=40] --> <!-- Button label to connect to a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_connect">Connect</string> <string name="vpn_connect">Connect</string>
<!-- Button label to continue changing a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_continue">Continue</string>
<!-- Dialog title to edit a VPN profile. [CHAR LIMIT=40] --> <!-- Dialog title to edit a VPN profile. [CHAR LIMIT=40] -->
<string name="vpn_edit">Edit VPN profile</string> <string name="vpn_edit">Edit VPN profile</string>
<!-- Button label to forget a VPN profile. [CHAR LIMIT=40] --> <!-- Button label to forget a VPN profile. [CHAR LIMIT=40] -->
@@ -5186,6 +5188,12 @@
<string name="vpn_disconnect">Disconnect</string> <string name="vpn_disconnect">Disconnect</string>
<!-- Field label to show the version number for a VPN app. [CHAR LIMIT=40] --> <!-- Field label to show the version number for a VPN app. [CHAR LIMIT=40] -->
<string name="vpn_version">Version <xliff:g id="version" example="3.3.0">%s</xliff:g></string> <string name="vpn_version">Version <xliff:g id="version" example="3.3.0">%s</xliff:g></string>
<!-- Button label to forget a VPN profile [CHAR LIMIT=40] -->
<string name="vpn_forget_long">Forget VPN</string>
<!-- Dialog message title to set another VPN app to be always-on [CHAR LIMIT=40] -->
<string name="vpn_replace_always_on_vpn_title">Replace existing VPN?</string>
<!-- Dialog message body to set another VPN app to be always-on [CHAR LIMIT=NONE] -->
<string name="vpn_replace_always_on_vpn_message">You already have a VPN connected to this profile. If you connected to one, your existing VPN will be replaced.</string>
<!-- Preference title for VPN settings. [CHAR LIMIT=40] --> <!-- Preference title for VPN settings. [CHAR LIMIT=40] -->
<string name="vpn_title">VPN</string> <string name="vpn_title">VPN</string>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2016 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.
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
android:title="@string/vpn_title">
<Preference
android:key="version"
android:textColor="?android:attr/textColorSecondary"
android:selectable="false"/>
<com.android.settingslib.RestrictedSwitchPreference
android:key="always_on_vpn"
android:title="@string/vpn_menu_lockdown"
android:defaultValue="false"
settings:userRestriction="no_config_vpn"/>
<com.android.settings.DimmableIconPreference
android:key="forget_vpn"
android:title="@string/vpn_forget_long"
android:icon="@drawable/ic_menu_delete"
settings:userRestriction="no_config_vpn"
settings:useAdminDisabledSummary="true" />
</PreferenceScreen>

View File

@@ -521,22 +521,33 @@ public final class Utils extends com.android.settingslib.Utils {
public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args, public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args,
int titleResId, CharSequence title, boolean isShortcut, int titleResId, CharSequence title, boolean isShortcut,
UserHandle userHandle) { UserHandle userHandle) {
// workaround to avoid crash in b/17523189
if (userHandle.getIdentifier() == UserHandle.myUserId()) {
startWithFragment(context, fragmentName, args, null, 0, titleResId, title, isShortcut);
} else {
Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, Intent intent = onBuildStartFragmentIntent(context, fragmentName, args,
null /* titleResPackageName */, titleResId, title, isShortcut); null /* titleResPackageName */, titleResId, title, isShortcut);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivityAsUser(intent, userHandle); context.startActivityAsUser(intent, userHandle);
} }
}
public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args, public static void startWithFragmentAsUser(Context context, String fragmentName, Bundle args,
String titleResPackageName, int titleResId, CharSequence title, boolean isShortcut, String titleResPackageName, int titleResId, CharSequence title, boolean isShortcut,
UserHandle userHandle) { UserHandle userHandle) {
Intent intent = onBuildStartFragmentIntent(context, fragmentName, args, titleResPackageName, // workaround to avoid crash in b/17523189
titleResId, title, isShortcut); if (userHandle.getIdentifier() == UserHandle.myUserId()) {
startWithFragment(context, fragmentName, args, null, 0, titleResPackageName, titleResId,
title, isShortcut);
} else {
Intent intent = onBuildStartFragmentIntent(context, fragmentName, args,
titleResPackageName, titleResId, title, isShortcut);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivityAsUser(intent, userHandle); context.startActivityAsUser(intent, userHandle);
} }
}
/** /**
* Build an Intent to launch a new activity showing the selected fragment. * Build an Intent to launch a new activity showing the selected fragment.

View File

@@ -19,6 +19,7 @@ package com.android.settings.vpn2;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.DialogFragment; import android.app.DialogFragment;
import android.app.Fragment;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
@@ -45,13 +46,25 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste
private static final String ARG_PACKAGE = "package"; private static final String ARG_PACKAGE = "package";
private PackageInfo mPackageInfo; private PackageInfo mPackageInfo;
private Listener mListener;
private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface( private final IConnectivityManager mService = IConnectivityManager.Stub.asInterface(
ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
public static void show(VpnSettings parent, PackageInfo packageInfo, String label, public interface Listener {
public void onForget();
public void onCancel();
}
public static void show(Fragment parent, PackageInfo packageInfo, String label,
boolean managing, boolean connected) { boolean managing, boolean connected) {
if (!parent.isAdded()) return; show(parent, null, packageInfo, label, managing, connected);
}
public static void show(Fragment parent, Listener listener, PackageInfo packageInfo,
String label, boolean managing, boolean connected) {
if (!parent.isAdded())
return;
Bundle args = new Bundle(); Bundle args = new Bundle();
args.putParcelable(ARG_PACKAGE, packageInfo); args.putParcelable(ARG_PACKAGE, packageInfo);
@@ -60,6 +73,7 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste
args.putBoolean(ARG_CONNECTED, connected); args.putBoolean(ARG_CONNECTED, connected);
final AppDialogFragment frag = new AppDialogFragment(); final AppDialogFragment frag = new AppDialogFragment();
frag.mListener = listener;
frag.setArguments(args); frag.setArguments(args);
frag.setTargetFragment(parent, 0); frag.setTargetFragment(parent, 0);
frag.show(parent.getFragmentManager(), TAG_APP_DIALOG); frag.show(parent.getFragmentManager(), TAG_APP_DIALOG);
@@ -98,6 +112,9 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
dismiss(); dismiss();
if (mListener != null) {
mListener.onCancel();
}
super.onCancel(dialog); super.onCancel(dialog);
} }
@@ -111,6 +128,10 @@ public class AppDialogFragment extends DialogFragment implements AppDialog.Liste
Log.e(TAG, "Failed to forget authorization of " + mPackageInfo.packageName + Log.e(TAG, "Failed to forget authorization of " + mPackageInfo.packageName +
" for user " + userId, e); " for user " + userId, e);
} }
if (mListener != null) {
mListener.onForget();
}
} }
private void onDisconnect(final DialogInterface dialog) { private void onDisconnect(final DialogInterface dialog) {

View File

@@ -0,0 +1,267 @@
/*
* Copyright (C) 2016 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 android.app.AlertDialog;
import android.app.AppOpsManager;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.ConnectivityManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.support.v7.preference.Preference;
import android.util.Log;
import com.android.internal.logging.MetricsProto.MetricsEvent;
import com.android.internal.net.VpnConfig;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settingslib.RestrictedSwitchPreference;
import com.android.settingslib.RestrictedPreference;
import com.android.settings.Utils;
import java.util.List;
import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
public class AppManagementFragment extends SettingsPreferenceFragment
implements Preference.OnPreferenceClickListener {
private static final String TAG = "AppManagementFragment";
private static final String ARG_PACKAGE_NAME = "package";
private static final String KEY_VERSION = "version";
private static final String KEY_ALWAYS_ON_VPN = "always_on_vpn";
private static final String KEY_FORGET_VPN = "forget_vpn";
private AppOpsManager mAppOpsManager;
private PackageManager mPackageManager;
private ConnectivityManager mConnectivityManager;
// VPN app info
private final int mUserId = UserHandle.myUserId();
private int mPackageUid;
private String mPackageName;
private PackageInfo mPackageInfo;
private String mVpnLabel;
// UI preference
private Preference mPreferenceVersion;
private RestrictedSwitchPreference mPreferenceAlwaysOn;
private RestrictedPreference mPreferenceForget;
// Listener
private final AppDialogFragment.Listener mForgetVpnDialogFragmentListener =
new AppDialogFragment.Listener() {
@Override
public void onForget() {
// Unset always-on-vpn when forgetting the VPN
if (isVpnAlwaysOn()) {
setAlwaysOnVpn(false);
}
// Also dismiss and go back to VPN list
finish();
}
@Override
public void onCancel() {
// do nothing
}
};
public static void show(Context context, AppPreference pref) {
Bundle args = new Bundle();
args.putString(ARG_PACKAGE_NAME, pref.getPackageName());
Utils.startWithFragmentAsUser(context, AppManagementFragment.class.getName(), args, -1,
pref.getLabel(), false, new UserHandle(pref.getUserId()));
}
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
addPreferencesFromResource(R.xml.vpn_app_management);
mPackageManager = getContext().getPackageManager();
mAppOpsManager = getContext().getSystemService(AppOpsManager.class);
mConnectivityManager = getContext().getSystemService(ConnectivityManager.class);
mPreferenceVersion = findPreference(KEY_VERSION);
mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN);
mPreferenceForget = (RestrictedPreference) findPreference(KEY_FORGET_VPN);
mPreferenceAlwaysOn.setOnPreferenceClickListener(this);
mPreferenceForget.setOnPreferenceClickListener(this);
}
@Override
public void onResume() {
super.onResume();
boolean isInfoLoaded = loadInfo();
if (isInfoLoaded) {
mPreferenceVersion.setTitle(
getPrefContext().getString(R.string.vpn_version, mPackageInfo.versionName));
updateUI();
} else {
finish();
}
}
@Override
public boolean onPreferenceClick(Preference preference) {
String key = preference.getKey();
switch (key) {
case KEY_FORGET_VPN:
return onForgetVpnClick();
case KEY_ALWAYS_ON_VPN:
return onAlwaysOnVpnClick();
default:
Log.w(TAG, "unknown key is clicked: " + key);
return false;
}
}
@Override
protected int getMetricsCategory() {
return MetricsEvent.VPN;
}
private boolean onForgetVpnClick() {
AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel,
true /* editing */, true);
return true;
}
private boolean onAlwaysOnVpnClick() {
final boolean isChecked = mPreferenceAlwaysOn.isChecked();
if (isChecked && isLegacyVpnLockDownOrAnotherPackageAlwaysOn()) {
// Show dialog if user replace always-on-vpn package and show not checked first
mPreferenceAlwaysOn.setChecked(false);
ReplaceExistingVpnFragment.show(this);
} else {
setAlwaysOnVpn(isChecked);
}
return true;
}
private void setAlwaysOnVpn(boolean isEnabled) {
// Only clear legacy lockdown vpn in system user.
if (mUserId == UserHandle.USER_SYSTEM) {
VpnUtils.clearLockdownVpn(getContext());
}
mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId, isEnabled ? mPackageName : null);
updateUI();
}
private void updateUI() {
if (isAdded()) {
mPreferenceAlwaysOn.setChecked(isVpnAlwaysOn());
}
}
private String getAlwaysOnVpnPackage() {
return mConnectivityManager.getAlwaysOnVpnPackageForUser(mUserId);
}
private boolean isVpnAlwaysOn() {
return mPackageName.equals(getAlwaysOnVpnPackage());
}
/**
* @return false if the intent doesn't contain an existing package or can't retrieve activated
* vpn info.
*/
private boolean loadInfo() {
final Bundle args = getArguments();
if (args == null) {
Log.e(TAG, "empty bundle");
return false;
}
mPackageName = args.getString(ARG_PACKAGE_NAME);
if (mPackageName == null) {
Log.e(TAG, "empty package name");
return false;
}
try {
mPackageUid = mPackageManager.getPackageUid(mPackageName, /* PackageInfoFlags */ 0);
mPackageInfo = mPackageManager.getPackageInfo(mPackageName, /* PackageInfoFlags */ 0);
mVpnLabel = VpnConfig.getVpnLabel(getPrefContext(), mPackageName).toString();
} catch (NameNotFoundException nnfe) {
Log.e(TAG, "package not found", nnfe);
return false;
}
if (!isVpnActivated()) {
Log.e(TAG, "package didn't register VPN profile");
return false;
}
return true;
}
private boolean isVpnActivated() {
final List<AppOpsManager.PackageOps> apps = mAppOpsManager.getOpsForPackage(mPackageUid,
mPackageName, new int[]{OP_ACTIVATE_VPN});
return apps != null && apps.size() > 0 && apps.get(0) != null;
}
private boolean isLegacyVpnLockDownOrAnotherPackageAlwaysOn() {
if (mUserId == UserHandle.USER_SYSTEM) {
String lockdownKey = VpnUtils.getLockdownVpn();
if (lockdownKey != null) {
return true;
}
}
return getAlwaysOnVpnPackage() != null && !isVpnAlwaysOn();
}
public static class ReplaceExistingVpnFragment extends DialogFragment
implements DialogInterface.OnClickListener {
public static void show(AppManagementFragment parent) {
final ReplaceExistingVpnFragment frag = new ReplaceExistingVpnFragment();
frag.setTargetFragment(parent, 0);
frag.show(parent.getFragmentManager(), null);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.vpn_replace_always_on_vpn_title)
.setMessage(getActivity().getString(R.string.vpn_replace_always_on_vpn_message))
.setNegativeButton(getActivity().getString(R.string.vpn_cancel), null)
.setPositiveButton(getActivity().getString(R.string.vpn_continue), this)
.create();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (getTargetFragment() instanceof AppManagementFragment) {
((AppManagementFragment) getTargetFragment()).setAlwaysOnVpn(true);
}
}
}
}

View File

@@ -71,14 +71,9 @@ public class LockdownConfigFragment extends DialogFragment {
dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN); dialog.show(parent.getFragmentManager(), TAG_LOCKDOWN);
} }
private static String getStringOrNull(KeyStore keyStore, String key) {
final byte[] value = keyStore.get(key);
return value == null ? null : new String(value);
}
private void initProfiles(KeyStore keyStore, Resources res) { private void initProfiles(KeyStore keyStore, Resources res) {
final ConnectivityManager cm = ConnectivityManager.from(getActivity()); final ConnectivityManager cm = ConnectivityManager.from(getActivity());
final String lockdownKey = getStringOrNull(keyStore, Credentials.LOCKDOWN_VPN); final String lockdownKey = VpnUtils.getLockdownVpn();
final String alwaysOnPackage = cm.getAlwaysOnVpnPackageForUser(UserHandle.myUserId()); final String alwaysOnPackage = cm.getAlwaysOnVpnPackageForUser(UserHandle.myUserId());
// Legacy VPN has a separate always-on mechanism which takes over the whole device, so // Legacy VPN has a separate always-on mechanism which takes over the whole device, so

View File

@@ -355,8 +355,7 @@ public class VpnSettings extends RestrictedSettingsFragment implements
} else if (tag instanceof AppPreference) { } else if (tag instanceof AppPreference) {
AppPreference pref = (AppPreference) tag; AppPreference pref = (AppPreference) tag;
boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), pref.getLabel(), AppManagementFragment.show(getPrefContext(), pref);
true /* editing */, connected);
} }
} }
}; };

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2016 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 android.content.Context;
import android.net.ConnectivityManager;
import android.security.Credentials;
import android.security.KeyStore;
/**
* Utility functions for vpn
*/
public class VpnUtils {
public static String getLockdownVpn() {
final byte[] value = KeyStore.getInstance().get(Credentials.LOCKDOWN_VPN);
return value == null ? null : new String(value);
}
public static void clearLockdownVpn(Context context) {
KeyStore.getInstance().delete(Credentials.LOCKDOWN_VPN);
// Always notify ConnectivityManager after keystore update
context.getSystemService(ConnectivityManager.class).updateLockdownVpn();
}
}