/* * 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.app.AppOpsManager; 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.IConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.security.Credentials; import android.security.KeyStore; import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.TextView; import com.android.internal.logging.MetricsLogger; import com.android.internal.net.LegacyVpnInfo; import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnProfile; import com.android.internal.util.ArrayUtils; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; import com.google.android.collect.Lists; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import static android.app.AppOpsManager.OP_ACTIVATE_VPN; /** * Settings screen listing VPNs. Configured VPNs and networks managed by apps * are shown in the same list. */ public class VpnSettings extends SettingsPreferenceFragment 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 String EXTRA_PICK_LOCKDOWN = "android.net.vpn.PICK_LOCKDOWN"; 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 final IConnectivityManager mConnectivityService = IConnectivityManager.Stub .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); private ConnectivityManager mConnectivityManager; private UserManager mUserManager; private final KeyStore mKeyStore = KeyStore.getInstance(); private HashMap mConfigPreferences = new HashMap<>(); private HashMap mAppPreferences = new HashMap<>(); private Handler mUpdater; private LegacyVpnInfo mConnectedLegacyVpn; private boolean mUnavailable; @Override protected int getMetricsCategory() { return MetricsLogger.VPN; } @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); if (mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN)) { mUnavailable = true; setPreferenceScreen(new PreferenceScreen(getActivity(), null)); setHasOptionsMenu(false); return; } mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); setHasOptionsMenu(true); addPreferencesFromResource(R.xml.vpn_settings2); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.vpn, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); // Hide lockdown VPN on devices that require IMS authentication if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { menu.findItem(R.id.vpn_lockdown).setVisible(false); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.vpn_create: { // Generate a new key. Here we just use the current time. long millis = System.currentTimeMillis(); while (mConfigPreferences.containsKey(Long.toHexString(millis))) { ++millis; } VpnProfile profile = new VpnProfile(Long.toHexString(millis)); ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */); return true; } case R.id.vpn_lockdown: { LockdownConfigFragment.show(this); return true; } } return super.onOptionsItemSelected(item); } @Override public void onResume() { super.onResume(); if (mUnavailable) { // Show a message to explain that VPN settings have been disabled TextView emptyView = (TextView) getView().findViewById(android.R.id.empty); getListView().setEmptyView(emptyView); if (emptyView != null) { emptyView.setText(R.string.vpn_settings_not_available); } return; } final boolean pickLockdown = getActivity() .getIntent().getBooleanExtra(EXTRA_PICK_LOCKDOWN, false); if (pickLockdown) { LockdownConfigFragment.show(this); } // Start monitoring mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback); // Trigger a refresh if (mUpdater == null) { mUpdater = new Handler(this); } mUpdater.sendEmptyMessage(RESCAN_MESSAGE); } @Override public void onPause() { if (mUnavailable) { super.onPause(); return; } // Stop monitoring mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); if (mUpdater != null) { mUpdater.removeCallbacksAndMessages(null); } super.onPause(); } @Override public boolean handleMessage(Message message) { mUpdater.removeMessages(RESCAN_MESSAGE); // Pref group within which to list VPNs PreferenceGroup vpnGroup = getPreferenceScreen(); vpnGroup.removeAll(); mConfigPreferences.clear(); mAppPreferences.clear(); // Fetch configured VPN profiles from KeyStore for (VpnProfile profile : loadVpnProfiles(mKeyStore)) { final ConfigPreference pref = new ConfigPreference(getActivity(), mManageListener, profile); pref.setOnPreferenceClickListener(this); mConfigPreferences.put(profile.key, pref); vpnGroup.addPreference(pref); } // 3rd-party VPN apps can change elsewhere. Reload them every time. for (AppOpsManager.PackageOps pkg : getVpnApps()) { String key = getVpnIdentifier(UserHandle.getUserId(pkg.getUid()), pkg.getPackageName()); final AppPreference pref = new AppPreference(getActivity(), mManageListener, pkg.getPackageName(), pkg.getUid()); pref.setOnPreferenceClickListener(this); mAppPreferences.put(key, pref); vpnGroup.addPreference(pref); } // Mark out connections with a subtitle try { // Legacy VPNs mConnectedLegacyVpn = null; LegacyVpnInfo info = mConnectivityService.getLegacyVpnInfo(UserHandle.myUserId()); if (info != null) { ConfigPreference preference = mConfigPreferences.get(info.key); if (preference != null) { preference.setState(info.state); mConnectedLegacyVpn = info; } } // Third-party VPNs for (UserHandle profile : mUserManager.getUserProfiles()) { VpnConfig cfg = mConnectivityService.getVpnConfig(profile.getIdentifier()); if (cfg != null) { final String key = getVpnIdentifier(profile.getIdentifier(), cfg.user); final AppPreference preference = mAppPreferences.get(key); if (preference != null) { preference.setState(AppPreference.STATE_CONNECTED); } } } } catch (RemoteException e) { // ignore } mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS); return true; } @Override public boolean onPreferenceClick(Preference preference) { if (preference instanceof ConfigPreference) { VpnProfile profile = ((ConfigPreference) preference).getProfile(); if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) && mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) { try { mConnectedLegacyVpn.intent.send(); return true; } catch (Exception e) { // ignore } } 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 = new UserHandle(UserHandle.getUserId(pref.getUid())); 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) { // Fall through } } // Already onnected 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; } private View.OnClickListener mManageListener = new View.OnClickListener() { @Override public void onClick(View view) { Object tag = view.getTag(); if (tag instanceof ConfigPreference) { ConfigPreference pref = (ConfigPreference) tag; ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */, true /* exists */); } else if (tag instanceof AppPreference) { AppPreference pref = (AppPreference) tag; boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); AppDialogFragment.show(VpnSettings.this, pref.getPackageInfo(), pref.getLabel(), true /* editing */, connected); } } }; private static String getVpnIdentifier(int userId, String packageName) { return Integer.toString(userId)+ "_" + packageName; } 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); } } }; @Override protected int getHelpResource() { return R.string.help_url_vpn; } private List getVpnApps() { List result = Lists.newArrayList(); // Build a filter of currently active user profiles. SparseArray currentProfileIds = new SparseArray<>(); for (UserHandle profile : mUserManager.getUserProfiles()) { currentProfileIds.put(profile.getIdentifier(), Boolean.TRUE); } // Fetch VPN-enabled apps from AppOps. AppOpsManager aom = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE); List apps = aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN}); if (apps != null) { for (AppOpsManager.PackageOps pkg : apps) { int userId = UserHandle.getUserId(pkg.getUid()); if (currentProfileIds.get(userId) == null) { // 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.getMode() == AppOpsManager.MODE_ALLOWED) { allowed = true; } } if (allowed) { result.add(pkg); } } } return result; } protected static List loadVpnProfiles(KeyStore keyStore, int... excludeTypes) { final ArrayList result = Lists.newArrayList(); // This might happen if the user does not yet have a keystore. Quietly short-circuit because // no keystore means no VPN configs. if (!keyStore.isUnlocked()) { return result; } // We are the only user of profiles in KeyStore so no locks are needed. for (String key : keyStore.list(Credentials.VPN)) { final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key)); if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) { result.add(profile); } } return result; } }