Files
app_Settings/src/com/android/settings/vpn2/VpnSettings.java
Jeremy Goldman 5be88d11de Remove the "+" button on the help menu if no secure vpns can be created
If the provider model is turned on, all new VPNs must have the secure
type. However, there is a system property which is necessary in order to
create secure VPNs. So if the FEATURE_IPSEC_TUNNELS is missing from the
device then we should not enable new 1st party VPNs.

Note, it has been confirmed with the VPN team that we don't expect any
devices released on S to actually be missing this property, but it's
still good to check just in case.

Bug: 176821216
Test: atest -c SettingsUnitTest
Change-Id: I712d342e1970243520612196ba57227b1ff05bbf
2021-04-19 16:47:46 +08:00

568 lines
22 KiB
Java

/*
* 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.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.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 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.RestrictedSettingsFragment;
import com.android.settings.Utils;
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 RestrictedSettingsFragment implements
Handler.Callback, Preference.OnPreferenceClickListener {
private static final String LOG_TAG = "VpnSettings";
private static final int RESCAN_MESSAGE = 0;
private static final int RESCAN_INTERVAL_MS = 1000;
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;
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);
mUnavailable = isUiRestricted();
setHasOptionsMenu(!mUnavailable);
addPreferencesFromResource(R.xml.vpn_settings2);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
// Although FEATURE_IPSEC_TUNNELS should always be present in android S,
// keep this check here just to be safe.
if (Utils.isProviderModelEnabled(getContext())
&& !getContext().getPackageManager().hasSystemFeature(
PackageManager.FEATURE_IPSEC_TUNNELS)) {
Log.w(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(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
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);
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;
public 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
mSettings.setShownPreferences(updates);
}
}
@VisibleForTesting
public boolean canAddPreferences() {
return isAdded();
}
@VisibleForTesting @UiThread
public void setShownPreferences(final Collection<Preference> updates) {
mLegacyVpnPreferences.values().retainAll(updates);
mAppPreferences.values().retainAll(updates);
// Change {@param updates} in-place to only contain new preferences that were not already
// added to the preference screen.
final PreferenceGroup vpnGroup = getPreferenceScreen();
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);
}
}
// Show any new preferences on the screen
for (Preference pref : updates) {
vpnGroup.addPreference(pref);
}
}
@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);
if (!connected) {
try {
UserHandle user = UserHandle.of(pref.getUserId());
Context userContext = getActivity().createPackageContextAsUser(
getActivity().getPackageName(), 0 /* flags */, user);
PackageManager pm = userContext.getPackageManager();
Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName());
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);
}
return pref;
}
@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) {
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());
}
// Fetch VPN-enabled apps from AppOps.
AppOpsManager aom = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
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;
}
// 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 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;
}
}