fix: 引入Settings的Module

This commit is contained in:
2024-12-10 14:57:24 +08:00
parent ad8fc8731d
commit df105485bd
6934 changed files with 896168 additions and 2 deletions

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2022 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;
/**
* Feature Provider used in vpn usage
*/
public interface AdvancedVpnFeatureProvider {
/**
* Returns package name of advanced vpn.
*/
String getAdvancedVpnPackageName();
/**
* Returns {@code true} advanced vpn is supported.
*/
boolean isAdvancedVpnSupported(Context context);
/**
* Returns the title of advanced vpn preference group.
*/
String getAdvancedVpnPreferenceGroupTitle(Context context);
/**
* Returns the title of vpn preference group.
*/
String getVpnPreferenceGroupTitle(Context context);
/**
* Returns {@code true} advanced vpn is removable.
*/
boolean isAdvancedVpnRemovable();
/**
* Returns {@code true} if the disconnect dialog is enabled when advanced vpn is connected.
*/
boolean isDisconnectDialogEnabled();
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2022 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;
/**
* Feature provider implementation for advanced vpn.
*/
public class AdvancedVpnFeatureProviderImpl implements AdvancedVpnFeatureProvider {
@Override
public String getAdvancedVpnPackageName() {
return null;
}
@Override
public boolean isAdvancedVpnSupported(Context context) {
return false;
}
@Override
public String getAdvancedVpnPreferenceGroupTitle(Context context) {
return null;
}
@Override
public String getVpnPreferenceGroupTitle(Context context) {
return null;
}
@Override
public boolean isAdvancedVpnRemovable() {
return true;
}
@Override
public boolean isDisconnectDialogEnabled() {
return true;
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2015 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.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import com.android.settings.R;
/**
* UI for managing the connection controlled by an app.
*
* Among the actions available are (depending on context):
* <ul>
* <li><strong>Forget</strong>: revoke the managing app's VPN permission</li>
* <li><strong>Dismiss</strong>: continue to use the VPN</li>
* </ul>
*
* {@see ConfigDialog}
*/
class AppDialog extends AlertDialog implements DialogInterface.OnClickListener {
private final Listener mListener;
private final PackageInfo mPackageInfo;
private final String mLabel;
AppDialog(Context context, Listener listener, PackageInfo pkgInfo, String label) {
super(context);
mListener = listener;
mPackageInfo = pkgInfo;
mLabel = label;
}
public final PackageInfo getPackageInfo() {
return mPackageInfo;
}
@Override
protected void onCreate(Bundle savedState) {
setTitle(mLabel);
setMessage(getContext().getString(R.string.vpn_version, mPackageInfo.versionName));
createButtons();
super.onCreate(savedState);
}
protected void createButtons() {
Context context = getContext();
// Forget the network
setButton(DialogInterface.BUTTON_NEGATIVE,
context.getString(R.string.vpn_forget), this);
// Dismiss
setButton(DialogInterface.BUTTON_POSITIVE,
context.getString(R.string.vpn_done), this);
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_NEGATIVE) {
mListener.onForget(dialog);
}
dismiss();
}
public interface Listener {
public void onForget(DialogInterface dialog);
}
}

View File

@@ -0,0 +1,180 @@
/*
* Copyright (C) 2015 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.Dialog;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.net.VpnManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.android.internal.net.VpnConfig;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
* Fragment wrapper around an {@link AppDialog}.
*/
public class AppDialogFragment extends InstrumentedDialogFragment implements AppDialog.Listener {
private static final String TAG_APP_DIALOG = "vpnappdialog";
private static final String TAG = "AppDialogFragment";
private static final String ARG_MANAGING = "managing";
private static final String ARG_LABEL = "label";
private static final String ARG_CONNECTED = "connected";
private static final String ARG_PACKAGE = "package";
private PackageInfo mPackageInfo;
private Listener mListener;
private UserManager mUserManager;
private DevicePolicyManager mDevicePolicyManager;
private VpnManager mVpnManager;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_VPN_APP_CONFIG;
}
public interface Listener {
void onForget();
void onCancel();
}
public static void show(Fragment parent, PackageInfo packageInfo, String label,
boolean managing, boolean connected) {
if (!managing && !connected) {
// We can't display anything useful for this case.
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();
args.putParcelable(ARG_PACKAGE, packageInfo);
args.putString(ARG_LABEL, label);
args.putBoolean(ARG_MANAGING, managing);
args.putBoolean(ARG_CONNECTED, connected);
final AppDialogFragment frag = new AppDialogFragment();
frag.mListener = listener;
frag.setArguments(args);
frag.setTargetFragment(parent, 0);
frag.show(parent.getFragmentManager(), TAG_APP_DIALOG);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPackageInfo = getArguments().getParcelable(ARG_PACKAGE);
mUserManager = UserManager.get(getContext());
mDevicePolicyManager = getContext()
.createContextAsUser(UserHandle.of(getUserId()), /* flags= */ 0)
.getSystemService(DevicePolicyManager.class);
mVpnManager = getContext().getSystemService(VpnManager.class);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
final String label = args.getString(ARG_LABEL);
boolean managing = args.getBoolean(ARG_MANAGING);
boolean connected = args.getBoolean(ARG_CONNECTED);
if (managing) {
return new AppDialog(getActivity(), this, mPackageInfo, label);
} else {
// Build an AlertDialog with an option to disconnect.
AlertDialog.Builder dlog = new AlertDialog.Builder(getActivity())
.setTitle(label)
.setMessage(getActivity().getString(R.string.vpn_disconnect_confirm))
.setNegativeButton(getActivity().getString(R.string.vpn_cancel), null);
if (connected && !isUiRestricted()) {
dlog.setPositiveButton(getActivity().getString(R.string.vpn_disconnect),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
onDisconnect(dialog);
}
});
}
return dlog.create();
}
}
@Override
public void onCancel(DialogInterface dialog) {
dismiss();
if (mListener != null) {
mListener.onCancel();
}
super.onCancel(dialog);
}
@Override
public void onForget(final DialogInterface dialog) {
if (isUiRestricted()) {
return;
}
final int userId = getUserId();
mVpnManager.setVpnPackageAuthorization(
mPackageInfo.packageName, userId, VpnManager.TYPE_VPN_NONE);
onDisconnect(dialog);
if (mListener != null) {
mListener.onForget();
}
}
private void onDisconnect(final DialogInterface dialog) {
if (isUiRestricted()) {
return;
}
final int userId = getUserId();
if (mPackageInfo.packageName.equals(VpnUtils.getConnectedPackage(mVpnManager, userId))) {
mVpnManager.setAlwaysOnVpnPackageForUser(userId, null, /* lockdownEnabled */ false,
/* lockdownAllowlist */ null);
mVpnManager.prepareVpn(mPackageInfo.packageName, VpnConfig.LEGACY_VPN, userId);
}
}
private boolean isUiRestricted() {
final UserHandle userHandle = UserHandle.of(getUserId());
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN, userHandle)) {
return true;
}
return mPackageInfo.packageName.equals(mDevicePolicyManager.getAlwaysOnVpnPackage());
}
private int getUserId() {
return UserHandle.getUserId(mPackageInfo.applicationInfo.uid);
}
}

View File

@@ -0,0 +1,385 @@
/*
* 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 static android.app.AppOpsManager.OP_ACTIVATE_PLATFORM_VPN;
import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
import android.app.AppOpsManager;
import android.app.Dialog;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.net.VpnManager;
import android.os.Bundle;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.preference.Preference;
import com.android.internal.net.VpnConfig;
import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.core.SubSettingLauncher;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settingslib.RestrictedPreference;
import com.android.settingslib.RestrictedSwitchPreference;
import java.util.List;
public class AppManagementFragment extends SettingsPreferenceFragment
implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener,
ConfirmLockdownFragment.ConfirmLockdownListener {
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_LOCKDOWN_VPN = "lockdown_vpn";
private static final String KEY_FORGET_VPN = "forget_vpn";
private PackageManager mPackageManager;
private DevicePolicyManager mDevicePolicyManager;
private VpnManager mVpnManager;
private AdvancedVpnFeatureProvider mFeatureProvider;
// VPN app info
private final int mUserId = UserHandle.myUserId();
private String mPackageName;
private PackageInfo mPackageInfo;
private String mVpnLabel;
// UI preference
private Preference mPreferenceVersion;
private RestrictedSwitchPreference mPreferenceAlwaysOn;
private RestrictedSwitchPreference mPreferenceLockdown;
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, false);
}
// Also dismiss and go back to VPN list
finish();
}
@Override
public void onCancel() {
// do nothing
}
};
public static void show(Context context, AppPreference pref, int sourceMetricsCategory) {
final Bundle args = new Bundle();
args.putString(ARG_PACKAGE_NAME, pref.getPackageName());
new SubSettingLauncher(context)
.setDestination(AppManagementFragment.class.getName())
.setArguments(args)
.setTitleText(pref.getLabel())
.setSourceMetricsCategory(sourceMetricsCategory)
.setUserHandle(new UserHandle(pref.getUserId()))
.launch();
}
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
addPreferencesFromResource(R.xml.vpn_app_management);
mPackageManager = getContext().getPackageManager();
mDevicePolicyManager = getContext().getSystemService(DevicePolicyManager.class);
mVpnManager = getContext().getSystemService(VpnManager.class);
mFeatureProvider = FeatureFactory.getFeatureFactory().getAdvancedVpnFeatureProvider();
mPreferenceVersion = findPreference(KEY_VERSION);
mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN);
mPreferenceLockdown = (RestrictedSwitchPreference) findPreference(KEY_LOCKDOWN_VPN);
mPreferenceForget = (RestrictedPreference) findPreference(KEY_FORGET_VPN);
mPreferenceAlwaysOn.setOnPreferenceChangeListener(this);
mPreferenceLockdown.setOnPreferenceChangeListener(this);
mPreferenceForget.setOnPreferenceClickListener(this);
}
@Override
public void onResume() {
super.onResume();
boolean isInfoLoaded = loadInfo();
if (isInfoLoaded) {
mPreferenceVersion.setSummary(mPackageInfo.versionName);
updateUI();
} else {
finish();
}
}
@Override
public boolean onPreferenceClick(Preference preference) {
String key = preference.getKey();
switch (key) {
case KEY_FORGET_VPN:
return onForgetVpnClick();
default:
Log.w(TAG, "unknown key is clicked: " + key);
return false;
}
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
switch (preference.getKey()) {
case KEY_ALWAYS_ON_VPN:
return onAlwaysOnVpnClick((Boolean) newValue, mPreferenceLockdown.isChecked());
case KEY_LOCKDOWN_VPN:
return onAlwaysOnVpnClick(mPreferenceAlwaysOn.isChecked(), (Boolean) newValue);
default:
Log.w(TAG, "unknown key is clicked: " + preference.getKey());
return false;
}
}
@Override
public int getMetricsCategory() {
return SettingsEnums.VPN_APP_MANAGEMENT;
}
private boolean onForgetVpnClick() {
updateRestrictedViews();
if (!mPreferenceForget.isEnabled()) {
return false;
}
AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel,
true /* editing */, true);
return true;
}
private boolean onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown) {
final boolean replacing = isAnotherVpnActive();
final boolean wasLockdown = VpnUtils.isAnyLockdownActive(getActivity());
if (ConfirmLockdownFragment.shouldShow(replacing, wasLockdown, lockdown)) {
// Place a dialog to confirm that traffic should be locked down.
final Bundle options = null;
ConfirmLockdownFragment.show(
this, replacing, alwaysOnSetting, wasLockdown, lockdown, options);
return false;
}
// No need to show the dialog. Change the setting straight away.
return setAlwaysOnVpnByUI(alwaysOnSetting, lockdown);
}
@Override
public void onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown) {
setAlwaysOnVpnByUI(isEnabled, isLockdown);
}
private boolean setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown) {
updateRestrictedViews();
if (!mPreferenceAlwaysOn.isEnabled()) {
return false;
}
// Only clear legacy lockdown vpn in system user.
if (mUserId == UserHandle.USER_SYSTEM) {
VpnUtils.clearLockdownVpn(getContext());
}
final boolean success = setAlwaysOnVpn(isEnabled, isLockdown);
if (isEnabled && (!success || !isVpnAlwaysOn())) {
CannotConnectFragment.show(this, mVpnLabel);
} else {
updateUI();
}
return success;
}
private boolean setAlwaysOnVpn(boolean isEnabled, boolean isLockdown) {
return mVpnManager.setAlwaysOnVpnPackageForUser(mUserId,
isEnabled ? mPackageName : null, isLockdown, /* lockdownAllowlist */ null);
}
private void updateUI() {
if (isAdded()) {
final boolean alwaysOn = isVpnAlwaysOn();
final boolean lockdown = alwaysOn
&& VpnUtils.isAnyLockdownActive(getActivity());
mPreferenceAlwaysOn.setChecked(alwaysOn);
mPreferenceLockdown.setChecked(lockdown);
updateRestrictedViews();
}
}
@VisibleForTesting
void updateRestrictedViews() {
if (mFeatureProvider.isAdvancedVpnSupported(getContext())
&& !mFeatureProvider.isAdvancedVpnRemovable()
&& TextUtils.equals(mPackageName, mFeatureProvider.getAdvancedVpnPackageName())) {
mPreferenceForget.setVisible(false);
} else {
mPreferenceForget.setVisible(true);
}
if (isAdded()) {
mPreferenceAlwaysOn.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN,
mUserId);
mPreferenceLockdown.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN,
mUserId);
mPreferenceForget.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN,
mUserId);
if (mPackageName.equals(mDevicePolicyManager.getAlwaysOnVpnPackage())) {
EnforcedAdmin admin = RestrictedLockUtils.getProfileOrDeviceOwner(
getContext(), UserHandle.of(mUserId));
mPreferenceAlwaysOn.setDisabledByAdmin(admin);
mPreferenceForget.setDisabledByAdmin(admin);
if (mDevicePolicyManager.isAlwaysOnVpnLockdownEnabled()) {
mPreferenceLockdown.setDisabledByAdmin(admin);
}
}
if (mVpnManager.isAlwaysOnVpnPackageSupportedForUser(mUserId, mPackageName)) {
// setSummary doesn't override the admin message when user restriction is applied
mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary);
// setEnabled is not required here, as checkRestrictionAndSetDisabled
// should have refreshed the enable state.
} else {
mPreferenceAlwaysOn.setEnabled(false);
mPreferenceLockdown.setEnabled(false);
mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary_not_supported);
}
}
}
@VisibleForTesting
void init(String packageName, AdvancedVpnFeatureProvider featureProvider,
RestrictedPreference preference) {
mPackageName = packageName;
mFeatureProvider = featureProvider;
mPreferenceForget = preference;
}
private String getAlwaysOnVpnPackage() {
return mVpnManager.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 {
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 (mPackageInfo.applicationInfo == null) {
Log.e(TAG, "package does not include an application");
return false;
}
if (!appHasVpnPermission(getContext(), mPackageInfo.applicationInfo)) {
Log.e(TAG, "package didn't register VPN profile");
return false;
}
return true;
}
@VisibleForTesting
static boolean appHasVpnPermission(Context context, @NonNull ApplicationInfo application) {
final AppOpsManager service =
(AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
final List<AppOpsManager.PackageOps> ops = service.getOpsForPackage(application.uid,
application.packageName, new int[]{OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN});
return !ArrayUtils.isEmpty(ops);
}
/**
* @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on.
*/
private boolean isAnotherVpnActive() {
final VpnConfig config = mVpnManager.getVpnConfig(mUserId);
return config != null && !TextUtils.equals(config.user, mPackageName);
}
public static class CannotConnectFragment extends InstrumentedDialogFragment {
private static final String TAG = "CannotConnect";
private static final String ARG_VPN_LABEL = "label";
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_VPN_CANNOT_CONNECT;
}
public static void show(AppManagementFragment parent, String vpnLabel) {
if (parent.getFragmentManager().findFragmentByTag(TAG) == null) {
final Bundle args = new Bundle();
args.putString(ARG_VPN_LABEL, vpnLabel);
final DialogFragment frag = new CannotConnectFragment();
frag.setArguments(args);
frag.show(parent.getFragmentManager(), TAG);
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final String vpnLabel = getArguments().getString(ARG_VPN_LABEL);
return new AlertDialog.Builder(getActivity())
.setTitle(getActivity().getString(R.string.vpn_cant_connect_title, vpnLabel))
.setMessage(getActivity().getString(R.string.vpn_cant_connect_message))
.setPositiveButton(R.string.okay, null)
.create();
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2015 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.admin.DevicePolicyManager;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
import androidx.preference.Preference;
import com.android.internal.net.LegacyVpnInfo;
import com.android.internal.net.VpnConfig;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
import com.android.settings.R;
/**
* {@link androidx.preference.Preference} containing information about a VPN
* application. Tracks the package name and connection state.
*/
public class AppPreference extends ManageablePreference {
public static final int STATE_CONNECTED = LegacyVpnInfo.STATE_CONNECTED;
public static final int STATE_DISCONNECTED = STATE_NONE;
private final String mPackageName;
private final String mName;
public AppPreference(Context context, int userId, String packageName) {
super(context, null /* attrs */, R.style.VpnAppManagementPreferenceStyle, 0);
super.setUserId(userId);
mPackageName = packageName;
disableIfConfiguredByAdmin();
// Fetch icon and VPN label
String label = packageName;
Drawable icon = null;
try {
// Make all calls to the package manager as the appropriate user.
Context userContext = getUserContext();
PackageManager pm = userContext.getPackageManager();
// The nested catch block is for the case that the app doesn't exist, so we can fall
// back to the default activity icon.
try {
PackageInfo pkgInfo = pm.getPackageInfo(mPackageName, 0 /* flags */);
if (pkgInfo != null) {
icon = pkgInfo.applicationInfo.loadIcon(pm);
label = VpnConfig.getVpnLabel(userContext, mPackageName).toString();
}
} catch (PackageManager.NameNotFoundException pkgNotFound) {
// Use default app label and icon as fallback
}
if (icon == null) {
icon = pm.getDefaultActivityIcon();
}
} catch (PackageManager.NameNotFoundException userNotFound) {
// No user, no useful information to obtain. Quietly fail.
}
mName = label;
setTitle(mName);
setIcon(icon);
}
/**
* Disable this preference if VPN is set as always on by a profile or device owner.
* NB: it should be called after super.setUserId() otherwise admin information can be lost.
*/
private void disableIfConfiguredByAdmin() {
if (isDisabledByAdmin()) {
// Already disabled due to user restriction.
return;
}
final DevicePolicyManager dpm = getContext()
.createContextAsUser(UserHandle.of(getUserId()), /* flags= */ 0)
.getSystemService(DevicePolicyManager.class);
if (mPackageName.equals(dpm.getAlwaysOnVpnPackage())) {
final EnforcedAdmin admin = RestrictedLockUtils.getProfileOrDeviceOwner(
getContext(), UserHandle.of(mUserId));
setDisabledByAdmin(admin);
}
}
public PackageInfo getPackageInfo() {
try {
PackageManager pm = getUserContext().getPackageManager();
return pm.getPackageInfo(mPackageName, 0 /* flags */);
} catch (PackageManager.NameNotFoundException nnfe) {
return null;
}
}
public String getLabel() {
return mName;
}
public String getPackageName() {
return mPackageName;
}
private Context getUserContext() throws PackageManager.NameNotFoundException {
UserHandle user = UserHandle.of(mUserId);
return getContext().createPackageContextAsUser(
getContext().getPackageName(), 0 /* flags */, user);
}
public int compareTo(Preference preference) {
if (preference instanceof AppPreference) {
AppPreference another = (AppPreference) preference;
int result;
if ((result = another.mState - mState) == 0 &&
(result = mName.compareToIgnoreCase(another.mName)) == 0 &&
(result = mPackageName.compareTo(another.mPackageName)) == 0) {
result = mUserId - another.mUserId;
}
return result;
} else if (preference instanceof LegacyVpnPreference) {
// Use comparator from ConfigPreference
LegacyVpnPreference another = (LegacyVpnPreference) preference;
return -another.compareTo(this);
} else {
return super.compareTo(preference);
}
}
}

View File

@@ -0,0 +1,44 @@
package com.android.settings.vpn2;
import androidx.annotation.NonNull;
import com.android.internal.util.Preconditions;
import java.util.Objects;
/**
* Holds packageName:userId pairs without any heavyweight fields.
* {@see ApplicationInfo}
*/
class AppVpnInfo implements Comparable<AppVpnInfo> {
public final int userId;
public final String packageName;
public AppVpnInfo(int userId, @NonNull String packageName) {
this.userId = userId;
this.packageName = Preconditions.checkNotNull(packageName);
}
@Override
public int compareTo(AppVpnInfo other) {
int result = packageName.compareTo(other.packageName);
if (result == 0) {
result = other.userId - userId;
}
return result;
}
@Override
public boolean equals(Object other) {
if (other instanceof AppVpnInfo) {
AppVpnInfo that = (AppVpnInfo) other;
return userId == that.userId && Objects.equals(packageName, that.packageName);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(packageName, userId);
}
}

View File

@@ -0,0 +1,563 @@
/*
* 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 android.content.Context;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.net.ProxyInfo;
import android.os.Bundle;
import android.os.SystemProperties;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.android.internal.net.VpnProfile;
import com.android.net.module.util.ProxyUtils;
import com.android.settings.R;
import com.android.settings.utils.AndroidKeystoreAliasLoader;
import java.util.Collection;
import java.util.List;
/**
* Dialog showing information about a VPN configuration. The dialog
* can be launched to either edit or prompt for credentials to connect
* to a user-added VPN.
*
* {@see AppDialog}
*/
class ConfigDialog extends AlertDialog implements TextWatcher,
View.OnClickListener, AdapterView.OnItemSelectedListener,
CompoundButton.OnCheckedChangeListener {
private static final String TAG = "ConfigDialog";
// Vpn profile constants to match with R.array.vpn_types.
private static final List<Integer> VPN_TYPES = List.of(
VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS,
VpnProfile.TYPE_IKEV2_IPSEC_PSK,
VpnProfile.TYPE_IKEV2_IPSEC_RSA
);
private final DialogInterface.OnClickListener mListener;
private final VpnProfile mProfile;
private boolean mEditing;
private boolean mExists;
private View mView;
private TextView mName;
private Spinner mType;
private TextView mServer;
private TextView mUsername;
private TextView mPassword;
private Spinner mProxySettings;
private TextView mProxyHost;
private TextView mProxyPort;
private TextView mIpsecIdentifier;
private TextView mIpsecSecret;
private Spinner mIpsecUserCert;
private Spinner mIpsecCaCert;
private Spinner mIpsecServerCert;
private CheckBox mSaveLogin;
private CheckBox mShowOptions;
private CheckBox mAlwaysOnVpn;
private TextView mAlwaysOnInvalidReason;
ConfigDialog(Context context, DialogInterface.OnClickListener listener,
VpnProfile profile, boolean editing, boolean exists) {
super(context);
mListener = listener;
mProfile = profile;
mEditing = editing;
mExists = exists;
}
@Override
protected void onCreate(Bundle savedState) {
mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
setView(mView);
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);
mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings);
mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host);
mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port);
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);
mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
mShowOptions = (CheckBox) mView.findViewById(R.id.show_options);
mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn);
mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason);
// Second, copy values from the profile.
mName.setText(mProfile.name);
setTypesByFeature(mType);
mType.setSelection(convertVpnProfileConstantToTypeIndex(mProfile.type));
mServer.setText(mProfile.server);
if (mProfile.saveLogin) {
mUsername.setText(mProfile.username);
mPassword.setText(mProfile.password);
}
if (mProfile.proxy != null) {
mProxyHost.setText(mProfile.proxy.getHost());
int port = mProfile.proxy.getPort();
mProxyPort.setText(port == 0 ? "" : Integer.toString(port));
}
mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
mIpsecSecret.setText(mProfile.ipsecSecret);
final AndroidKeystoreAliasLoader androidKeystoreAliasLoader =
new AndroidKeystoreAliasLoader(null);
loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0,
mProfile.ipsecUserCert);
loadCertificates(mIpsecCaCert, androidKeystoreAliasLoader.getCaCertAliases(),
R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
loadCertificates(mIpsecServerCert, androidKeystoreAliasLoader.getKeyCertAliases(),
R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
mSaveLogin.setChecked(mProfile.saveLogin);
mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
// Hide lockdown VPN on devices that require IMS authentication
if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
mAlwaysOnVpn.setVisibility(View.GONE);
}
// Third, add listeners to required fields.
mName.addTextChangedListener(this);
mType.setOnItemSelectedListener(this);
mServer.addTextChangedListener(this);
mUsername.addTextChangedListener(this);
mPassword.addTextChangedListener(this);
mProxySettings.setOnItemSelectedListener(this);
mProxyHost.addTextChangedListener(this);
mProxyPort.addTextChangedListener(this);
mIpsecIdentifier.addTextChangedListener(this);
mIpsecSecret.addTextChangedListener(this);
mIpsecUserCert.setOnItemSelectedListener(this);
mShowOptions.setOnClickListener(this);
mAlwaysOnVpn.setOnCheckedChangeListener(this);
// Fourth, determine whether to do editing or connecting.
mEditing = mEditing || !validate(true /*editing*/);
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);
// Hide 'save login' when we are editing.
mSaveLogin.setVisibility(View.GONE);
configureAdvancedOptionsVisibility();
if (mExists) {
// Create a button to forget the profile if it has already been saved..
setButton(DialogInterface.BUTTON_NEUTRAL,
context.getString(R.string.vpn_forget), mListener);
}
// 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));
setUsernamePasswordVisibility(mProfile.type);
// 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(savedState);
// Update UI controls according to the current configuration.
updateUiControls();
// Workaround to resize the dialog for the input method.
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
}
@Override
public void onRestoreInstanceState(Bundle savedState) {
super.onRestoreInstanceState(savedState);
// Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced
// options here if they were already revealed or set.
configureAdvancedOptionsVisibility();
}
@Override
public void afterTextChanged(Editable field) {
updateUiControls();
}
@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 onClick(View view) {
if (view == mShowOptions) {
configureAdvancedOptionsVisibility();
}
}
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (parent == mType) {
changeType(VPN_TYPES.get(position));
} else if (parent == mProxySettings) {
updateProxyFieldsVisibility(position);
}
updateUiControls();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
if (compoundButton == mAlwaysOnVpn) {
updateUiControls();
}
}
public boolean isVpnAlwaysOn() {
return mAlwaysOnVpn.isChecked();
}
/**
* Updates the UI according to the current configuration entered by the user.
*
* These include:
* "Always-on VPN" checkbox
* Reason for "Always-on VPN" being disabled, when necessary
* Proxy info if manually configured
* "Save account information" checkbox
* "Save" and "Connect" buttons
*/
private void updateUiControls() {
VpnProfile profile = getProfile();
// Always-on VPN
if (profile.isValidLockdownProfile()) {
mAlwaysOnVpn.setEnabled(true);
mAlwaysOnInvalidReason.setVisibility(View.GONE);
} else {
mAlwaysOnVpn.setChecked(false);
mAlwaysOnVpn.setEnabled(false);
mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_other);
mAlwaysOnInvalidReason.setVisibility(View.VISIBLE);
}
// Show proxy fields if any proxy field is filled.
if (mProfile.proxy != null && (!mProfile.proxy.getHost().isEmpty() ||
mProfile.proxy.getPort() != 0)) {
mProxySettings.setSelection(VpnProfile.PROXY_MANUAL);
updateProxyFieldsVisibility(VpnProfile.PROXY_MANUAL);
}
// Save account information
if (mAlwaysOnVpn.isChecked()) {
mSaveLogin.setChecked(true);
mSaveLogin.setEnabled(false);
} else {
mSaveLogin.setChecked(mProfile.saveLogin);
mSaveLogin.setEnabled(true);
}
// Save or Connect button
getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
}
private void updateProxyFieldsVisibility(int position) {
final int visible = position == VpnProfile.PROXY_MANUAL ? View.VISIBLE : View.GONE;
mView.findViewById(R.id.vpn_proxy_fields).setVisibility(visible);
}
private boolean isAdvancedOptionsEnabled() {
return mProxyHost.getText().length() > 0 || mProxyPort.getText().length() > 0;
}
private void configureAdvancedOptionsVisibility() {
if (mShowOptions.isChecked() || isAdvancedOptionsEnabled()) {
mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
mShowOptions.setVisibility(View.GONE);
// TODO(b/149070123): Add ability for platform VPNs to support DNS & routes
} else {
mView.findViewById(R.id.options).setVisibility(View.GONE);
mShowOptions.setVisibility(View.VISIBLE);
}
}
private void changeType(int type) {
// First, hide everything.
mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.GONE);
setUsernamePasswordVisibility(type);
// Always enable identity for IKEv2/IPsec profiles.
mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE);
// Then, unhide type-specific fields.
switch (type) {
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE);
break;
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
// fall through
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
break;
}
configureAdvancedOptionsVisibility();
}
private boolean validate(boolean editing) {
if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) {
return false;
}
final int position = mType.getSelectedItemPosition();
final int type = VPN_TYPES.get(position);
if (!editing && requiresUsernamePassword(type)) {
return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
}
if (mName.getText().length() == 0 || mServer.getText().length() == 0) {
return false;
}
// All IKEv2 methods require an identifier
if (mIpsecIdentifier.getText().length() == 0) {
return false;
}
if (!validateProxy()) {
return false;
}
switch (type) {
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
return true;
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
return mIpsecSecret.getText().length() != 0;
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
return mIpsecUserCert.getSelectedItemPosition() != 0;
}
return false;
}
private void setTypesByFeature(Spinner typeSpinner) {
String[] types = getContext().getResources().getStringArray(R.array.vpn_types);
if (types.length != VPN_TYPES.size()) {
Log.wtf(TAG, "VPN_TYPES array length does not match string array");
}
// Although FEATURE_IPSEC_TUNNELS should always be present in android S and beyond,
// keep this check here just to be safe.
if (!getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_IPSEC_TUNNELS)) {
Log.wtf(TAG, "FEATURE_IPSEC_TUNNELS missing from system");
}
// If the vpn is new or is not already a legacy type,
// don't allow the user to set the type to a legacy option.
// Set the mProfile.type to TYPE_IKEV2_IPSEC_USER_PASS if the VPN not exist
if (!mExists) {
mProfile.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
}
final ArrayAdapter<String> adapter = new ArrayAdapter<String>(
getContext(), android.R.layout.simple_spinner_item, types);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
typeSpinner.setAdapter(adapter);
}
private void loadCertificates(Spinner spinner, Collection<String> choices, int firstId,
String selected) {
Context context = getContext();
String first = (firstId == 0) ? "" : context.getString(firstId);
String[] myChoices;
if (choices == null || choices.size() == 0) {
myChoices = new String[] {first};
} else {
myChoices = new String[choices.size() + 1];
myChoices[0] = first;
int i = 1;
for (String c : choices) {
myChoices[i++] = c;
}
}
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
context, android.R.layout.simple_spinner_item, myChoices);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
for (int i = 1; i < myChoices.length; ++i) {
if (myChoices[i].equals(selected)) {
spinner.setSelection(i);
break;
}
}
}
private void setUsernamePasswordVisibility(int type) {
mView.findViewById(R.id.userpass).setVisibility(
requiresUsernamePassword(type) ? View.VISIBLE : View.GONE);
}
private boolean requiresUsernamePassword(int type) {
switch (type) {
case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
return false;
default:
return true;
}
}
boolean isEditing() {
return mEditing;
}
boolean hasProxy() {
return mProxySettings.getSelectedItemPosition() == VpnProfile.PROXY_MANUAL;
}
VpnProfile getProfile() {
// First, save common fields.
VpnProfile profile = new VpnProfile(mProfile.key);
profile.name = mName.getText().toString();
final int position = mType.getSelectedItemPosition();
profile.type = VPN_TYPES.get(position);
profile.server = mServer.getText().toString().trim();
profile.username = mUsername.getText().toString();
profile.password = mPassword.getText().toString();
// Save fields based on VPN type.
profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
if (hasProxy()) {
String proxyHost = mProxyHost.getText().toString().trim();
String proxyPort = mProxyPort.getText().toString().trim();
// 0 is a last resort default, but the interface validates that the proxy port is
// present and non-zero.
int port = 0;
if (!proxyPort.isEmpty()) {
try {
port = Integer.parseInt(proxyPort);
} catch (NumberFormatException e) {
Log.e(TAG, "Could not parse proxy port integer ", e);
}
}
profile.proxy = ProxyInfo.buildDirectProxy(proxyHost, port);
} else {
profile.proxy = null;
}
// Then, save type-specific fields.
switch (profile.type) {
case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
profile.ipsecSecret = mIpsecSecret.getText().toString();
break;
case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
if (mIpsecUserCert.getSelectedItemPosition() != 0) {
profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
profile.ipsecSecret = profile.ipsecUserCert;
}
// fall through
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
if (mIpsecCaCert.getSelectedItemPosition() != 0) {
profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
}
if (mIpsecServerCert.getSelectedItemPosition() != 0) {
profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
}
break;
}
final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin);
return profile;
}
private boolean validateProxy() {
if (!hasProxy()) {
return true;
}
final String host = mProxyHost.getText().toString().trim();
final String port = mProxyPort.getText().toString().trim();
return ProxyUtils.validate(host, port, "") == ProxyUtils.PROXY_VALID;
}
private int convertVpnProfileConstantToTypeIndex(int vpnType) {
final int typeIndex = VPN_TYPES.indexOf(vpnType);
if (typeIndex == -1) {
// Existing legacy profile type
Log.wtf(TAG, "Invalid existing profile type");
return 0;
}
return typeIndex;
}
}

View File

@@ -0,0 +1,242 @@
/*
* Copyright (C) 2015 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.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.net.VpnManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.security.Credentials;
import android.security.LegacyVpnProfileStore;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import com.android.internal.net.LegacyVpnInfo;
import com.android.internal.net.VpnProfile;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
/**
* Fragment wrapper around a {@link ConfigDialog}.
*/
public class ConfigDialogFragment extends InstrumentedDialogFragment implements
DialogInterface.OnClickListener, DialogInterface.OnShowListener, View.OnClickListener,
ConfirmLockdownFragment.ConfirmLockdownListener {
private static final String TAG_CONFIG_DIALOG = "vpnconfigdialog";
private static final String TAG = "ConfigDialogFragment";
private static final String ARG_PROFILE = "profile";
private static final String ARG_EDITING = "editing";
private static final String ARG_EXISTS = "exists";
private Context mContext;
private VpnManager mService;
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_LEGACY_VPN_CONFIG;
}
public static void show(VpnSettings parent, VpnProfile profile, boolean edit, boolean exists) {
if (!parent.isAdded()) return;
Bundle args = new Bundle();
args.putParcelable(ARG_PROFILE, profile);
args.putBoolean(ARG_EDITING, edit);
args.putBoolean(ARG_EXISTS, exists);
final ConfigDialogFragment frag = new ConfigDialogFragment();
frag.setArguments(args);
frag.setTargetFragment(parent, 0);
frag.show(parent.getFragmentManager(), TAG_CONFIG_DIALOG);
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
mContext = context;
mService = context.getSystemService(VpnManager.class);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
VpnProfile profile = (VpnProfile) args.getParcelable(ARG_PROFILE);
boolean editing = args.getBoolean(ARG_EDITING);
boolean exists = args.getBoolean(ARG_EXISTS);
final Dialog dialog = new ConfigDialog(getActivity(), this, profile, editing, exists);
dialog.setOnShowListener(this);
return dialog;
}
/**
* Override for the default onClick handler which also calls dismiss().
*
* @see DialogInterface.OnClickListener#onClick(DialogInterface, int)
*/
@Override
public void onShow(DialogInterface dialogInterface) {
((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
}
@Override
public void onClick(View positiveButton) {
onClick(getDialog(), AlertDialog.BUTTON_POSITIVE);
}
@Override
public void onConfirmLockdown(Bundle options, boolean isAlwaysOn, boolean isLockdown) {
VpnProfile profile = (VpnProfile) options.getParcelable(ARG_PROFILE);
connect(profile, isAlwaysOn);
dismiss();
}
@Override
public void onClick(DialogInterface dialogInterface, int button) {
ConfigDialog dialog = (ConfigDialog) getDialog();
if (dialog == null) {
Log.e(TAG, "ConfigDialog object is null");
return;
}
VpnProfile profile = dialog.getProfile();
if (button == DialogInterface.BUTTON_POSITIVE) {
// Possibly throw up a dialog to explain lockdown VPN.
final boolean shouldLockdown = dialog.isVpnAlwaysOn();
final boolean shouldConnect = shouldLockdown || !dialog.isEditing();
final boolean wasLockdown = VpnUtils.isAnyLockdownActive(mContext);
try {
final boolean replace = VpnUtils.isVpnActive(mContext);
if (shouldConnect && !isConnected(profile) &&
ConfirmLockdownFragment.shouldShow(replace, wasLockdown, shouldLockdown)) {
final Bundle opts = new Bundle();
opts.putParcelable(ARG_PROFILE, profile);
ConfirmLockdownFragment.show(this, replace, /* alwaysOn */ shouldLockdown,
/* from */ wasLockdown, /* to */ shouldLockdown, opts);
} else if (shouldConnect) {
connect(profile, shouldLockdown);
} else {
save(profile, false);
}
} catch (RemoteException e) {
Log.w(TAG, "Failed to check active VPN state. Skipping.", e);
}
} else if (button == DialogInterface.BUTTON_NEUTRAL) {
// Disable profile if connected
if (!disconnect(profile)) {
Log.e(TAG, "Failed to disconnect VPN. Leaving profile in keystore.");
return;
}
// Delete from profile store.
LegacyVpnProfileStore.remove(Credentials.VPN + profile.key);
updateLockdownVpn(false, profile);
}
dismiss();
}
@Override
public void onCancel(DialogInterface dialog) {
dismiss();
super.onCancel(dialog);
}
private void updateLockdownVpn(boolean isVpnAlwaysOn, VpnProfile profile) {
// Save lockdown vpn
if (isVpnAlwaysOn) {
// Show toast if vpn profile is not valid
if (!profile.isValidLockdownProfile()) {
Toast.makeText(mContext, R.string.vpn_lockdown_config_error,
Toast.LENGTH_LONG).show();
return;
}
mService.setAlwaysOnVpnPackageForUser(UserHandle.myUserId(), null,
/* lockdownEnabled */ false, /* lockdownAllowlist */ null);
VpnUtils.setLockdownVpn(mContext, profile.key);
} else {
// update only if lockdown vpn has been changed
if (VpnUtils.isVpnLockdown(profile.key)) {
VpnUtils.clearLockdownVpn(mContext);
}
}
}
private void save(VpnProfile profile, boolean lockdown) {
LegacyVpnProfileStore.put(Credentials.VPN + profile.key, profile.encode());
// Flush out old version of profile
disconnect(profile);
// Notify lockdown VPN that the profile has changed.
updateLockdownVpn(lockdown, profile);
}
private void connect(VpnProfile profile, boolean lockdown) {
save(profile, lockdown);
// Now try to start the VPN - this is not necessary if the profile is set as lockdown,
// because just saving the profile in this mode will start a connection.
if (!VpnUtils.isVpnLockdown(profile.key)) {
VpnUtils.clearLockdownVpn(mContext);
try {
mService.startLegacyVpn(profile);
} catch (IllegalStateException e) {
Toast.makeText(mContext, R.string.vpn_no_network, Toast.LENGTH_LONG).show();
} catch (UnsupportedOperationException e) {
Log.e(TAG, "Attempted to start an unsupported VPN type.");
final AlertDialog unusedDialog = new AlertDialog.Builder(mContext)
.setMessage(R.string.vpn_start_unsupported)
.setPositiveButton(android.R.string.ok, null)
.show();
}
}
}
/**
* Ensure that the VPN profile pointed at by {@param profile} is disconnected.
*
* @return {@code true} iff this VPN profile is no longer connected. Note that another profile
* may still be active - this function will then do nothing but still return success.
*/
private boolean disconnect(VpnProfile profile) {
try {
if (!isConnected(profile)) {
return true;
}
return VpnUtils.disconnectLegacyVpn(getContext());
} catch (RemoteException e) {
Log.e(TAG, "Failed to disconnect", e);
return false;
}
}
private boolean isConnected(VpnProfile profile) throws RemoteException {
LegacyVpnInfo connected = mService.getLegacyVpnInfo(UserHandle.myUserId());
return connected != null && profile.key.equals(connected.key);
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.Dialog;
import android.app.settings.SettingsEnums;
import android.content.DialogInterface;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.android.settings.R;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
public class ConfirmLockdownFragment extends InstrumentedDialogFragment
implements DialogInterface.OnClickListener {
public interface ConfirmLockdownListener {
public void onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown);
}
private static final String TAG = "ConfirmLockdown";
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_VPN_REPLACE_EXISTING;
}
private static final String ARG_REPLACING = "replacing";
private static final String ARG_ALWAYS_ON = "always_on";
private static final String ARG_LOCKDOWN_SRC = "lockdown_old";
private static final String ARG_LOCKDOWN_DST = "lockdown_new";
private static final String ARG_OPTIONS = "options";
public static boolean shouldShow(boolean replacing, boolean fromLockdown, boolean toLockdown) {
// We only need to show this if we are:
// - replacing an existing connection
// - switching on always-on mode with lockdown enabled where it was not enabled before.
return replacing || (toLockdown && !fromLockdown);
}
public static void show(Fragment parent, boolean replacing, boolean alwaysOn,
boolean fromLockdown, boolean toLockdown, Bundle options) {
if (parent.getFragmentManager().findFragmentByTag(TAG) != null) {
// Already exists. Don't show it twice.
return;
}
final Bundle args = new Bundle();
args.putBoolean(ARG_REPLACING, replacing);
args.putBoolean(ARG_ALWAYS_ON, alwaysOn);
args.putBoolean(ARG_LOCKDOWN_SRC, fromLockdown);
args.putBoolean(ARG_LOCKDOWN_DST, toLockdown);
args.putParcelable(ARG_OPTIONS, options);
final ConfirmLockdownFragment frag = new ConfirmLockdownFragment();
frag.setArguments(args);
frag.setTargetFragment(parent, 0);
frag.show(parent.getFragmentManager(), TAG);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final boolean replacing = getArguments().getBoolean(ARG_REPLACING);
final boolean alwaysOn = getArguments().getBoolean(ARG_ALWAYS_ON);
final boolean wasLockdown = getArguments().getBoolean(ARG_LOCKDOWN_SRC);
final boolean nowLockdown = getArguments().getBoolean(ARG_LOCKDOWN_DST);
final int titleId = (nowLockdown ? R.string.vpn_require_connection_title :
(replacing ? R.string.vpn_replace_vpn_title : R.string.vpn_set_vpn_title));
final int actionId =
(replacing ? R.string.vpn_replace :
(nowLockdown ? R.string.vpn_turn_on : R.string.okay));
final int messageId;
if (nowLockdown) {
messageId = replacing
? R.string.vpn_replace_always_on_vpn_enable_message
: R.string.vpn_first_always_on_vpn_message;
} else {
messageId = wasLockdown
? R.string.vpn_replace_always_on_vpn_disable_message
: R.string.vpn_replace_vpn_message;
}
return new AlertDialog.Builder(getActivity())
.setTitle(titleId)
.setMessage(messageId)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(actionId, this)
.create();
}
@Override
public void onClick(DialogInterface dialog, int which) {
if (getTargetFragment() instanceof ConfirmLockdownListener) {
((ConfirmLockdownListener) getTargetFragment()).onConfirmLockdown(
getArguments().getParcelable(ARG_OPTIONS),
getArguments().getBoolean(ARG_ALWAYS_ON),
getArguments().getBoolean(ARG_LOCKDOWN_DST));
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2015 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 static com.android.internal.net.LegacyVpnInfo.STATE_CONNECTED;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import androidx.preference.Preference;
import com.android.internal.net.VpnProfile;
import com.android.settings.R;
/**
* {@link androidx.preference.Preference} tracks the underlying legacy vpn profile and
* its connection state.
*/
public class LegacyVpnPreference extends ManageablePreference {
private VpnProfile mProfile;
LegacyVpnPreference(Context context) {
super(context, null /* attrs */);
setIcon(R.drawable.ic_vpn_key);
setIconSize(ICON_SIZE_SMALL);
}
public VpnProfile getProfile() {
return mProfile;
}
public void setProfile(VpnProfile profile) {
final String oldLabel = (mProfile != null ? mProfile.name : null);
final String newLabel = (profile != null ? profile.name : null);
if (!TextUtils.equals(oldLabel, newLabel)) {
setTitle(newLabel);
notifyHierarchyChanged();
}
mProfile = profile;
}
@Override
public int compareTo(Preference preference) {
if (preference instanceof LegacyVpnPreference) {
LegacyVpnPreference another = (LegacyVpnPreference) preference;
int result;
if ((result = another.mState - mState) == 0 &&
(result = mProfile.name.compareToIgnoreCase(another.mProfile.name)) == 0 &&
(result = mProfile.type - another.mProfile.type) == 0) {
result = mProfile.key.compareTo(another.mProfile.key);
}
return result;
} else if (preference instanceof AppPreference) {
// Try to sort connected VPNs first
AppPreference another = (AppPreference) preference;
if (mState != STATE_CONNECTED && another.getState() == AppPreference.STATE_CONNECTED) {
return 1;
}
// Show configured VPNs before app VPNs
return -1;
} else {
return super.compareTo(preference);
}
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.settings_button && isDisabledByAdmin()) {
performClick();
return;
}
super.onClick(v);
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2015 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 static android.text.Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
import android.content.Context;
import android.content.res.Resources;
import android.os.UserHandle;
import android.os.UserManager;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet;
import com.android.settings.R;
import com.android.settings.widget.GearPreference;
import com.android.settingslib.Utils;
/**
* This class sets appropriate enabled state and user admin message when userId is set
*/
public abstract class ManageablePreference extends GearPreference {
public static int STATE_NONE = -1;
boolean mIsAlwaysOn = false;
boolean mIsInsecureVpn = false;
int mState = STATE_NONE;
int mUserId;
public ManageablePreference(Context context, AttributeSet attrs) {
this(context, attrs, 0, 0);
}
public ManageablePreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setPersistent(false);
setOrder(0);
setUserId(UserHandle.myUserId());
}
public int getUserId() {
return mUserId;
}
public void setUserId(int userId) {
mUserId = userId;
checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, userId);
}
public boolean isAlwaysOn() {
return mIsAlwaysOn;
}
public boolean isInsecureVpn() {
return mIsInsecureVpn;
}
public int getState() {
return mState;
}
public void setState(int state) {
if (mState != state) {
mState = state;
updateSummary();
notifyHierarchyChanged();
}
}
public void setAlwaysOn(boolean isEnabled) {
if (mIsAlwaysOn != isEnabled) {
mIsAlwaysOn = isEnabled;
updateSummary();
}
}
/**
* Set whether the VPN associated with this preference has an insecure type.
* By default the value will be False.
*/
public void setInsecureVpn(boolean isInsecureVpn) {
if (mIsInsecureVpn != isInsecureVpn) {
mIsInsecureVpn = isInsecureVpn;
updateSummary();
}
}
/**
* Update the preference summary string (see {@see Preference#setSummary}) with a string
* reflecting connection status, always-on setting and whether the vpn is insecure.
*
* State is not shown for {@code STATE_NONE}.
*/
protected void updateSummary() {
final Resources res = getContext().getResources();
final String[] states = res.getStringArray(R.array.vpn_states);
String summary = (mState == STATE_NONE ? "" : states[mState]);
if (mIsInsecureVpn) {
final String insecureString = res.getString(R.string.vpn_insecure_summary);
summary = TextUtils.isEmpty(summary) ? insecureString : summary + " / "
+ insecureString;
SpannableString summarySpan = new SpannableString(summary);
final int colorError = Utils.getColorErrorDefaultColor(getContext());
summarySpan.setSpan(new ForegroundColorSpan(colorError), 0, summary.length(),
SPAN_EXCLUSIVE_INCLUSIVE);
setSummary(summarySpan);
} else if (mIsAlwaysOn) {
final String alwaysOnString = res.getString(R.string.vpn_always_on_summary_active);
summary = TextUtils.isEmpty(summary) ? alwaysOnString : summary + " / "
+ alwaysOnString;
setSummary(summary);
} else {
setSummary(summary);
}
}
}

View File

@@ -0,0 +1,13 @@
# People who can approve changes for submission.
jchalard@google.com
lorenzo@google.com
maze@google.com
reminv@google.com
xiaom@google.com
hughchen@google.com
robertluo@google.com
timhypeng@google.com
vincentwei@google.com
# Emergency approvers in case the above are not available
satk@google.com

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2021 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.Activity;
import android.content.Context;
import android.content.Intent;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.RestrictedPreference;
/**
* A preference with an Info icon on the side
*/
public class VpnInfoPreference extends RestrictedPreference implements View.OnClickListener {
private boolean mIsInsecureVpn = false;
private String mHelpUrl;
public VpnInfoPreference(Context context, AttributeSet attrs) {
super(context, attrs);
mHelpUrl = context.getString(R.string.help_url_insecure_vpn);
}
@Override
protected int getSecondTargetResId() {
// Note: in the future, we will probably want to provide a configuration option
// for this info to not be the warning color.
return R.layout.preference_widget_warning;
}
@Override
protected boolean shouldHideSecondTarget() {
return false;
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
final View icon = holder.findViewById(R.id.warning_button);
if (mIsInsecureVpn && !TextUtils.isEmpty(mHelpUrl)) {
icon.setVisibility(View.VISIBLE);
icon.setOnClickListener(this);
icon.setEnabled(true);
} else {
icon.setVisibility(View.GONE);
icon.setOnClickListener(this);
icon.setEnabled(false);
}
// Hide the divider from view
final View divider =
holder.findViewById(com.android.settingslib.widget.preference.twotarget.R.id.two_target_divider);
divider.setVisibility(View.GONE);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.warning_button) {
final Intent intent = HelpUtils.getHelpIntent(
getContext(), mHelpUrl, this.getClass().getName());
if (intent != null) {
((Activity) getContext()).startActivityForResult(intent, 0);
}
}
}
/**
* Sets whether this preference corresponds to an insecure VPN. This will also affect whether
* the warning icon appears to the user.
*/
public void setInsecureVpn(boolean isInsecureVpn) {
if (mIsInsecureVpn != isInsecureVpn) {
mIsInsecureVpn = isInsecureVpn;
notifyChanged();
}
}
}

View File

@@ -0,0 +1,704 @@
/*
* 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 static android.app.AppOpsManager.OP_ACTIVATE_PLATFORM_VPN;
import static android.app.AppOpsManager.OP_ACTIVATE_VPN;
import android.annotation.UiThread;
import android.annotation.WorkerThread;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.VpnManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.UserHandle;
import android.os.UserManager;
import android.security.Credentials;
import android.security.LegacyVpnProfileStore;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceScreen;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.net.LegacyVpnInfo;
import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
import com.android.settings.R;
import com.android.settings.dashboard.RestrictedDashboardFragment;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.GearPreference;
import com.android.settings.widget.GearPreference.OnGearClickListener;
import com.android.settingslib.RestrictedLockUtilsInternal;
import com.google.android.collect.Lists;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Settings screen listing VPNs. Configured VPNs and networks managed by apps
* are shown in the same list.
*/
public class VpnSettings extends RestrictedDashboardFragment implements
Handler.Callback, Preference.OnPreferenceClickListener {
private static final String LOG_TAG = "VpnSettings";
private static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
private static final int RESCAN_MESSAGE = 0;
private static final int RESCAN_INTERVAL_MS = 1000;
private static final String ADVANCED_VPN_GROUP_KEY = "advanced_vpn_group";
private static final String VPN_GROUP_KEY = "vpn_group";
private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder()
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
.build();
private ConnectivityManager mConnectivityManager;
private UserManager mUserManager;
private VpnManager mVpnManager;
private Map<String, LegacyVpnPreference> mLegacyVpnPreferences = new ArrayMap<>();
private Map<AppVpnInfo, AppPreference> mAppPreferences = new ArrayMap<>();
@GuardedBy("this")
private Handler mUpdater;
private HandlerThread mUpdaterThread;
private LegacyVpnInfo mConnectedLegacyVpn;
private boolean mUnavailable;
private AdvancedVpnFeatureProvider mFeatureProvider;
private PreferenceScreen mPreferenceScreen;
private boolean mIsAdvancedVpnSupported;
public VpnSettings() {
super(UserManager.DISALLOW_CONFIG_VPN);
}
@Override
public int getMetricsCategory() {
return SettingsEnums.VPN;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);
mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
mVpnManager = (VpnManager) getSystemService(Context.VPN_MANAGEMENT_SERVICE);
mFeatureProvider = FeatureFactory.getFeatureFactory().getAdvancedVpnFeatureProvider();
mIsAdvancedVpnSupported = mFeatureProvider.isAdvancedVpnSupported(getContext());
mUnavailable = isUiRestricted();
setHasOptionsMenu(!mUnavailable);
mPreferenceScreen = getPreferenceScreen();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (!getContext().getResources().getBoolean(R.bool.config_show_vpn_options)) {
return;
}
// Although FEATURE_IPSEC_TUNNELS should always be present in android S and beyond,
// keep this check here just to be safe.
if (!getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_IPSEC_TUNNELS)) {
Log.wtf(LOG_TAG, "FEATURE_IPSEC_TUNNELS missing from system, cannot create new VPNs");
return;
} else {
// By default, we should inflate this menu.
inflater.inflate(R.menu.vpn, menu);
}
}
@Override
public void onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
// Disable all actions if VPN configuration has been disallowed
for (int i = 0; i < menu.size(); i++) {
if (isUiRestrictedByOnlyAdmin()) {
RestrictedLockUtilsInternal.setMenuItemAsDisabledByAdmin(getPrefContext(),
menu.getItem(i), getRestrictionEnforcedAdmin());
} else {
menu.getItem(i).setEnabled(!mUnavailable);
}
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Generate a new key. Here we just use the current time.
if (item.getItemId() == R.id.vpn_create) {
long millis = System.currentTimeMillis();
while (mLegacyVpnPreferences.containsKey(Long.toHexString(millis))) {
++millis;
}
VpnProfile profile = new VpnProfile(Long.toHexString(millis));
ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onResume() {
super.onResume();
mUnavailable = mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN);
if (mUnavailable) {
// Show a message to explain that VPN settings have been disabled
if (!isUiRestrictedByOnlyAdmin()) {
getEmptyTextView()
.setText(com.android.settingslib.R.string.vpn_settings_not_available);
}
getPreferenceScreen().removeAll();
return;
} else {
setEmptyView(getEmptyTextView());
getEmptyTextView().setText(R.string.vpn_no_vpns_added);
}
// Start monitoring
mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback);
// Trigger a refresh
mUpdaterThread = new HandlerThread("Refresh VPN list in background");
mUpdaterThread.start();
mUpdater = new Handler(mUpdaterThread.getLooper(), this);
mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
}
@Override
protected int getPreferenceScreenResId() {
return R.xml.vpn_settings2;
}
@Override
protected String getLogTag() {
return LOG_TAG;
}
@Override
public void onPause() {
if (mUnavailable) {
super.onPause();
return;
}
// Stop monitoring
mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
synchronized (this) {
mUpdater.removeCallbacksAndMessages(null);
mUpdater = null;
mUpdaterThread.quit();
mUpdaterThread = null;
}
super.onPause();
}
@Override @WorkerThread
public boolean handleMessage(Message message) {
//Return if activity has been recycled
final Activity activity = getActivity();
if (activity == null) {
return true;
}
final Context context = activity.getApplicationContext();
// Run heavy RPCs before switching to UI thread
final List<VpnProfile> vpnProfiles = loadVpnProfiles();
final List<AppVpnInfo> vpnApps = getVpnApps(context, /* includeProfiles */ true,
mFeatureProvider);
final Map<String, LegacyVpnInfo> connectedLegacyVpns = getConnectedLegacyVpns();
final Set<AppVpnInfo> connectedAppVpns = getConnectedAppVpns();
final Set<AppVpnInfo> alwaysOnAppVpnInfos = getAlwaysOnAppVpnInfos();
final String lockdownVpnKey = VpnUtils.getLockdownVpn();
// Refresh list of VPNs
activity.runOnUiThread(new UpdatePreferences(this)
.legacyVpns(vpnProfiles, connectedLegacyVpns, lockdownVpnKey)
.appVpns(vpnApps, connectedAppVpns, alwaysOnAppVpnInfos));
synchronized (this) {
if (mUpdater != null) {
mUpdater.removeMessages(RESCAN_MESSAGE);
mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS);
}
}
return true;
}
@VisibleForTesting
static class UpdatePreferences implements Runnable {
private List<VpnProfile> vpnProfiles = Collections.<VpnProfile>emptyList();
private List<AppVpnInfo> vpnApps = Collections.<AppVpnInfo>emptyList();
private Map<String, LegacyVpnInfo> connectedLegacyVpns =
Collections.<String, LegacyVpnInfo>emptyMap();
private Set<AppVpnInfo> connectedAppVpns = Collections.<AppVpnInfo>emptySet();
private Set<AppVpnInfo> alwaysOnAppVpnInfos = Collections.<AppVpnInfo>emptySet();
private String lockdownVpnKey = null;
private final VpnSettings mSettings;
UpdatePreferences(VpnSettings settings) {
mSettings = settings;
}
public final UpdatePreferences legacyVpns(List<VpnProfile> vpnProfiles,
Map<String, LegacyVpnInfo> connectedLegacyVpns, String lockdownVpnKey) {
this.vpnProfiles = vpnProfiles;
this.connectedLegacyVpns = connectedLegacyVpns;
this.lockdownVpnKey = lockdownVpnKey;
return this;
}
public final UpdatePreferences appVpns(List<AppVpnInfo> vpnApps,
Set<AppVpnInfo> connectedAppVpns, Set<AppVpnInfo> alwaysOnAppVpnInfos) {
this.vpnApps = vpnApps;
this.connectedAppVpns = connectedAppVpns;
this.alwaysOnAppVpnInfos = alwaysOnAppVpnInfos;
return this;
}
@Override @UiThread
public void run() {
if (!mSettings.canAddPreferences()) {
return;
}
// Find new VPNs by subtracting existing ones from the full set
final Set<Preference> updates = new ArraySet<>();
// Add legacy VPNs
for (VpnProfile profile : vpnProfiles) {
LegacyVpnPreference p = mSettings.findOrCreatePreference(profile, true);
if (connectedLegacyVpns.containsKey(profile.key)) {
p.setState(connectedLegacyVpns.get(profile.key).state);
} else {
p.setState(LegacyVpnPreference.STATE_NONE);
}
p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key));
p.setInsecureVpn(VpnProfile.isLegacyType(profile.type));
updates.add(p);
}
// Show connected VPNs even if the original entry in keystore is gone
for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) {
final VpnProfile stubProfile = new VpnProfile(vpn.key);
LegacyVpnPreference p = mSettings.findOrCreatePreference(stubProfile, false);
p.setState(vpn.state);
p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key));
// (b/184921649) do not call setInsecureVpn() for connectedLegacyVpns, since the
// LegacyVpnInfo does not contain VPN type information, and the profile already
// exists within vpnProfiles.
updates.add(p);
}
// Add VpnService VPNs
for (AppVpnInfo app : vpnApps) {
AppPreference p = mSettings.findOrCreatePreference(app);
if (connectedAppVpns.contains(app)) {
p.setState(AppPreference.STATE_CONNECTED);
} else {
p.setState(AppPreference.STATE_DISCONNECTED);
}
p.setAlwaysOn(alwaysOnAppVpnInfos.contains(app));
updates.add(p);
}
// Trim out deleted VPN preferences
if (DEBUG) {
Log.d(LOG_TAG, "isAdvancedVpnSupported() : " + mSettings.mIsAdvancedVpnSupported);
}
if (mSettings.mIsAdvancedVpnSupported) {
mSettings.setShownAdvancedPreferences(updates);
} else {
mSettings.setShownPreferences(updates);
}
}
}
@VisibleForTesting
public boolean canAddPreferences() {
return isAdded();
}
@VisibleForTesting @UiThread
public void setShownPreferences(final Collection<Preference> updates) {
retainAllPreference(updates);
final PreferenceGroup vpnGroup = mPreferenceScreen;
updatePreferenceGroup(vpnGroup, updates);
// Show all new preferences on the screen
for (Preference pref : updates) {
vpnGroup.addPreference(pref);
}
}
@VisibleForTesting @UiThread
void setShownAdvancedPreferences(final Collection<Preference> updates) {
retainAllPreference(updates);
PreferenceGroup advancedVpnGroup = mPreferenceScreen.findPreference(ADVANCED_VPN_GROUP_KEY);
PreferenceGroup vpnGroup = mPreferenceScreen.findPreference(VPN_GROUP_KEY);
advancedVpnGroup.setTitle(
mFeatureProvider.getAdvancedVpnPreferenceGroupTitle(getContext()));
vpnGroup.setTitle(mFeatureProvider.getVpnPreferenceGroupTitle(getContext()));
updatePreferenceGroup(advancedVpnGroup, updates);
updatePreferenceGroup(vpnGroup, updates);
// Show all new preferences on the screen
for (Preference pref : updates) {
String packageName = "";
if (pref instanceof LegacyVpnPreference) {
LegacyVpnPreference legacyPref = (LegacyVpnPreference) pref;
packageName = legacyPref.getPackageName();
} else if (pref instanceof AppPreference) {
AppPreference appPref = (AppPreference) pref;
packageName = appPref.getPackageName();
}
if (DEBUG) {
Log.d(LOG_TAG, "setShownAdvancedPreferences() package name : " + packageName);
}
if (TextUtils.equals(packageName, mFeatureProvider.getAdvancedVpnPackageName())) {
advancedVpnGroup.addPreference(pref);
} else {
vpnGroup.addPreference(pref);
}
}
advancedVpnGroup.setVisible(advancedVpnGroup.getPreferenceCount() > 0);
vpnGroup.setVisible(vpnGroup.getPreferenceCount() > 0);
}
private void retainAllPreference(Collection<Preference> updates) {
mLegacyVpnPreferences.values().retainAll(updates);
mAppPreferences.values().retainAll(updates);
}
private void updatePreferenceGroup(PreferenceGroup vpnGroup, Collection<Preference> updates) {
// Change {@param updates} in-place to only contain new preferences that were not already
// added to the preference screen.
for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) {
Preference p = vpnGroup.getPreference(i);
if (updates.contains(p)) {
updates.remove(p);
} else {
vpnGroup.removePreference(p);
}
}
}
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference instanceof LegacyVpnPreference) {
LegacyVpnPreference pref = (LegacyVpnPreference) preference;
VpnProfile profile = pref.getProfile();
if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) &&
mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) {
try {
mConnectedLegacyVpn.intent.send();
return true;
} catch (Exception e) {
Log.w(LOG_TAG, "Starting config intent failed", e);
}
}
ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */);
return true;
} else if (preference instanceof AppPreference) {
AppPreference pref = (AppPreference) preference;
boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED);
String vpnPackageName = pref.getPackageName();
if ((!connected) || (isAdvancedVpn(mFeatureProvider, vpnPackageName, getContext())
&& !mFeatureProvider.isDisconnectDialogEnabled())) {
try {
UserHandle user = UserHandle.of(pref.getUserId());
Context userContext = getContext().createPackageContextAsUser(
getContext().getPackageName(), 0 /* flags */, user);
PackageManager pm = userContext.getPackageManager();
Intent appIntent = pm.getLaunchIntentForPackage(vpnPackageName);
if (appIntent != null) {
userContext.startActivityAsUser(appIntent, user);
return true;
}
} catch (PackageManager.NameNotFoundException nnfe) {
Log.w(LOG_TAG, "VPN provider does not exist: " + pref.getPackageName(), nnfe);
}
}
// Already connected or no launch intent available - show an info dialog
PackageInfo pkgInfo = pref.getPackageInfo();
AppDialogFragment.show(this, pkgInfo, pref.getLabel(), false /* editing */, connected);
return true;
}
return false;
}
@Override
public int getHelpResource() {
return R.string.help_url_vpn;
}
private OnGearClickListener mGearListener = new OnGearClickListener() {
@Override
public void onGearClick(GearPreference p) {
if (p instanceof LegacyVpnPreference) {
LegacyVpnPreference pref = (LegacyVpnPreference) p;
ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */,
true /* exists */);
} else if (p instanceof AppPreference) {
AppPreference pref = (AppPreference) p;
AppManagementFragment.show(getPrefContext(), pref, getMetricsCategory());
}
}
};
private NetworkCallback mNetworkCallback = new NetworkCallback() {
@Override
public void onAvailable(Network network) {
if (mUpdater != null) {
mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
}
}
@Override
public void onLost(Network network) {
if (mUpdater != null) {
mUpdater.sendEmptyMessage(RESCAN_MESSAGE);
}
}
};
@VisibleForTesting @UiThread
public LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) {
LegacyVpnPreference pref = mLegacyVpnPreferences.get(profile.key);
boolean created = false;
if (pref == null ) {
pref = new LegacyVpnPreference(getPrefContext());
pref.setOnGearClickListener(mGearListener);
pref.setOnPreferenceClickListener(this);
mLegacyVpnPreferences.put(profile.key, pref);
created = true;
}
if (created || update) {
// This can change call-to-call because the profile can update and keep the same key.
pref.setProfile(profile);
}
return pref;
}
@VisibleForTesting @UiThread
public AppPreference findOrCreatePreference(AppVpnInfo app) {
AppPreference pref = mAppPreferences.get(app);
if (pref == null) {
pref = new AppPreference(getPrefContext(), app.userId, app.packageName);
pref.setOnGearClickListener(mGearListener);
pref.setOnPreferenceClickListener(this);
mAppPreferences.put(app, pref);
}
enableAdvancedVpnGearIconIfNecessary(pref);
return pref;
}
private void enableAdvancedVpnGearIconIfNecessary(AppPreference pref) {
Context context = getContext();
if (!isAdvancedVpn(mFeatureProvider, pref.getPackageName(), context)) {
return;
}
boolean isEnabled = false;
AppOpsManager appOpsManager = getContext().getSystemService(AppOpsManager.class);
List<AppOpsManager.PackageOps> apps =
appOpsManager.getPackagesForOps(
new int[] {OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN});
if (apps != null) {
for (AppOpsManager.PackageOps pkg : apps) {
if (isAdvancedVpn(mFeatureProvider, pkg.getPackageName(), context)) {
isEnabled = true;
break;
}
}
}
pref.setOnGearClickListener(isEnabled ? mGearListener : null);
}
@WorkerThread
private Map<String, LegacyVpnInfo> getConnectedLegacyVpns() {
mConnectedLegacyVpn = mVpnManager.getLegacyVpnInfo(UserHandle.myUserId());
if (mConnectedLegacyVpn != null) {
return Collections.singletonMap(mConnectedLegacyVpn.key, mConnectedLegacyVpn);
}
return Collections.emptyMap();
}
@WorkerThread
private Set<AppVpnInfo> getConnectedAppVpns() {
// Mark connected third-party services
Set<AppVpnInfo> connections = new ArraySet<>();
for (UserHandle profile : mUserManager.getUserProfiles()) {
VpnConfig config = mVpnManager.getVpnConfig(profile.getIdentifier());
if (config != null && !config.legacy) {
connections.add(new AppVpnInfo(profile.getIdentifier(), config.user));
}
}
return connections;
}
@WorkerThread
private Set<AppVpnInfo> getAlwaysOnAppVpnInfos() {
Set<AppVpnInfo> result = new ArraySet<>();
for (UserHandle profile : mUserManager.getUserProfiles()) {
final int profileId = profile.getIdentifier();
final String packageName = mVpnManager.getAlwaysOnVpnPackageForUser(profileId);
if (packageName != null) {
result.add(new AppVpnInfo(profileId, packageName));
}
}
return result;
}
static List<AppVpnInfo> getVpnApps(Context context, boolean includeProfiles,
AdvancedVpnFeatureProvider featureProvider) {
return getVpnApps(context, includeProfiles, featureProvider,
context.getSystemService(AppOpsManager.class));
}
@VisibleForTesting
static List<AppVpnInfo> getVpnApps(Context context, boolean includeProfiles,
AdvancedVpnFeatureProvider featureProvider, AppOpsManager aom) {
List<AppVpnInfo> result = Lists.newArrayList();
final Set<Integer> profileIds;
if (includeProfiles) {
profileIds = new ArraySet<>();
for (UserHandle profile : UserManager.get(context).getUserProfiles()) {
profileIds.add(profile.getIdentifier());
}
} else {
profileIds = Collections.singleton(UserHandle.myUserId());
}
if (featureProvider.isAdvancedVpnSupported(context)) {
PackageManager pm = context.getPackageManager();
try {
ApplicationInfo appInfo =
pm.getApplicationInfo(
featureProvider.getAdvancedVpnPackageName(), /* flags= */ 0);
int userId = UserHandle.getUserId(appInfo.uid);
result.add(new AppVpnInfo(userId, featureProvider.getAdvancedVpnPackageName()));
} catch (PackageManager.NameNotFoundException e) {
Log.e(LOG_TAG, "Advanced VPN package name not found.", e);
}
}
List<AppOpsManager.PackageOps> apps =
aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN});
if (apps != null) {
for (AppOpsManager.PackageOps pkg : apps) {
int userId = UserHandle.getUserId(pkg.getUid());
if (!profileIds.contains(userId)) {
// Skip packages for users outside of our profile group.
continue;
}
if (isAdvancedVpn(featureProvider, pkg.getPackageName(), context)) {
continue;
}
// Look for a MODE_ALLOWED permission to activate VPN.
boolean allowed = false;
for (AppOpsManager.OpEntry op : pkg.getOps()) {
if ((op.getOp() == OP_ACTIVATE_VPN || op.getOp() == OP_ACTIVATE_PLATFORM_VPN)
&& op.getMode() == AppOpsManager.MODE_ALLOWED) {
allowed = true;
}
}
if (allowed) {
result.add(new AppVpnInfo(userId, pkg.getPackageName()));
}
}
}
Collections.sort(result);
return result;
}
private static boolean isAdvancedVpn(AdvancedVpnFeatureProvider featureProvider,
String packageName, Context context) {
return featureProvider.isAdvancedVpnSupported(context)
&& TextUtils.equals(packageName, featureProvider.getAdvancedVpnPackageName());
}
private static List<VpnProfile> loadVpnProfiles() {
final ArrayList<VpnProfile> result = Lists.newArrayList();
for (String key : LegacyVpnProfileStore.list(Credentials.VPN)) {
final VpnProfile profile = VpnProfile.decode(key,
LegacyVpnProfileStore.get(Credentials.VPN + key));
if (profile != null) {
result.add(profile);
}
}
return result;
}
@VisibleForTesting
void init(PreferenceScreen preferenceScreen, AdvancedVpnFeatureProvider featureProvider) {
mPreferenceScreen = preferenceScreen;
mFeatureProvider = featureProvider;
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.VpnManager;
import android.os.RemoteException;
import android.provider.Settings;
import android.security.Credentials;
import android.security.LegacyVpnProfileStore;
import com.android.internal.net.LegacyVpnInfo;
import com.android.internal.net.VpnConfig;
/**
* Utility functions for vpn.
*
* LegacyVpnProfileStore methods should only be called in system user
*/
public class VpnUtils {
private static final String TAG = "VpnUtils";
public static String getLockdownVpn() {
final byte[] value = LegacyVpnProfileStore.get(Credentials.LOCKDOWN_VPN);
return value == null ? null : new String(value);
}
public static void clearLockdownVpn(Context context) {
LegacyVpnProfileStore.remove(Credentials.LOCKDOWN_VPN);
// Always notify VpnManager after keystore update
getVpnManager(context).updateLockdownVpn();
}
public static void setLockdownVpn(Context context, String lockdownKey) {
LegacyVpnProfileStore.put(Credentials.LOCKDOWN_VPN, lockdownKey.getBytes());
// Always notify VpnManager after keystore update
getVpnManager(context).updateLockdownVpn();
}
public static boolean isVpnLockdown(String key) {
return key.equals(getLockdownVpn());
}
public static boolean isAnyLockdownActive(Context context) {
final int userId = context.getUserId();
if (getLockdownVpn() != null) {
return true;
}
return getVpnManager(context).getAlwaysOnVpnPackageForUser(userId) != null
&& Settings.Secure.getIntForUser(context.getContentResolver(),
Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, /* default */ 0, userId) != 0;
}
public static boolean isVpnActive(Context context) throws RemoteException {
return getVpnManager(context).getVpnConfig(context.getUserId()) != null;
}
public static String getConnectedPackage(VpnManager vpnManager, final int userId) {
final VpnConfig config = vpnManager.getVpnConfig(userId);
return config != null ? config.user : null;
}
private static VpnManager getVpnManager(Context context) {
return context.getSystemService(VpnManager.class);
}
public static boolean isAlwaysOnVpnSet(VpnManager vm, final int userId) {
return vm.getAlwaysOnVpnPackageForUser(userId) != null;
}
public static boolean disconnectLegacyVpn(Context context) {
int userId = context.getUserId();
LegacyVpnInfo currentLegacyVpn = getVpnManager(context).getLegacyVpnInfo(userId);
if (currentLegacyVpn != null) {
clearLockdownVpn(context);
getVpnManager(context).prepareVpn(null, VpnConfig.LEGACY_VPN, userId);
return true;
}
return false;
}
}