| /* |
| * 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.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 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(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; |
| } |
| } |