/* * 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.Utils; 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 mLegacyVpnPreferences = new ArrayMap<>(); private Map 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 vpnProfiles = loadVpnProfiles(); final List vpnApps = getVpnApps(context, /* includeProfiles */ true, mFeatureProvider); final Map connectedLegacyVpns = getConnectedLegacyVpns(); final Set connectedAppVpns = getConnectedAppVpns(); final Set 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 vpnProfiles = Collections.emptyList(); private List vpnApps = Collections.emptyList(); private Map connectedLegacyVpns = Collections.emptyMap(); private Set connectedAppVpns = Collections.emptySet(); private Set alwaysOnAppVpnInfos = Collections.emptySet(); private String lockdownVpnKey = null; private final VpnSettings mSettings; UpdatePreferences(VpnSettings settings) { mSettings = settings; } public final UpdatePreferences legacyVpns(List vpnProfiles, Map connectedLegacyVpns, String lockdownVpnKey) { this.vpnProfiles = vpnProfiles; this.connectedLegacyVpns = connectedLegacyVpns; this.lockdownVpnKey = lockdownVpnKey; return this; } public final UpdatePreferences appVpns(List vpnApps, Set connectedAppVpns, Set 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 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 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 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 updates) { mLegacyVpnPreferences.values().retainAll(updates); mAppPreferences.values().retainAll(updates); } private void updatePreferenceGroup(PreferenceGroup vpnGroup, Collection 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 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 getConnectedLegacyVpns() { mConnectedLegacyVpn = mVpnManager.getLegacyVpnInfo(UserHandle.myUserId()); if (mConnectedLegacyVpn != null) { return Collections.singletonMap(mConnectedLegacyVpn.key, mConnectedLegacyVpn); } return Collections.emptyMap(); } @WorkerThread private Set getConnectedAppVpns() { // Mark connected third-party services Set connections = new ArraySet<>(); for (UserHandle profile : mUserManager.getUserProfiles()) { if (Utils.shouldHideUser(profile, mUserManager)) { continue; } VpnConfig config = mVpnManager.getVpnConfig(profile.getIdentifier()); if (config != null && !config.legacy) { connections.add(new AppVpnInfo(profile.getIdentifier(), config.user)); } } return connections; } @WorkerThread private Set getAlwaysOnAppVpnInfos() { Set result = new ArraySet<>(); for (UserHandle profile : mUserManager.getUserProfiles()) { if (Utils.shouldHideUser(profile, mUserManager)) { continue; } final int profileId = profile.getIdentifier(); final String packageName = mVpnManager.getAlwaysOnVpnPackageForUser(profileId); if (packageName != null) { result.add(new AppVpnInfo(profileId, packageName)); } } return result; } static List getVpnApps(Context context, boolean includeProfiles, AdvancedVpnFeatureProvider featureProvider) { return getVpnApps(context, includeProfiles, featureProvider, context.getSystemService(AppOpsManager.class)); } @VisibleForTesting static List getVpnApps(Context context, boolean includeProfiles, AdvancedVpnFeatureProvider featureProvider, AppOpsManager aom) { List result = Lists.newArrayList(); final Set profileIds; if (includeProfiles) { profileIds = new ArraySet<>(); UserManager userManager = UserManager.get(context); for (UserHandle profile : userManager.getUserProfiles()) { if (Utils.shouldHideUser(profile, userManager)) { continue; } 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 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 loadVpnProfiles() { final ArrayList 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; } }