/* * 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_VPN; import android.annotation.NonNull; import android.app.AppOpsManager; import android.app.Dialog; 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.ConnectivityManager; import android.net.IConnectivityManager; import android.os.Bundle; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.text.TextUtils; import android.util.Log; 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.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 ConnectivityManager mConnectivityManager; private IConnectivityManager mConnectivityService; // 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(); mConnectivityManager = getContext().getSystemService(ConnectivityManager.class); mConnectivityService = IConnectivityManager.Stub .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 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.setTitle( getPrefContext().getString(R.string.vpn_version, 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; } 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 mConnectivityManager.setAlwaysOnVpnPackageForUser(mUserId, isEnabled ? mPackageName : null, isLockdown, /* lockdownWhitelist */ null); } private void updateUI() { if (isAdded()) { final boolean alwaysOn = isVpnAlwaysOn(); final boolean lockdown = alwaysOn && VpnUtils.isAnyLockdownActive(getActivity()); mPreferenceAlwaysOn.setChecked(alwaysOn); mPreferenceLockdown.setChecked(lockdown); updateRestrictedViews(); } } private void updateRestrictedViews() { if (isAdded()) { mPreferenceAlwaysOn.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, mUserId); mPreferenceLockdown.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, mUserId); mPreferenceForget.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, mUserId); if (mConnectivityManager.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); } } } private String getAlwaysOnVpnPackage() { return mConnectivityManager.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 ops = service.getOpsForPackage(application.uid, application.packageName, new int[]{OP_ACTIVATE_VPN}); return !ArrayUtils.isEmpty(ops); } /** * @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on. */ private boolean isAnotherVpnActive() { try { final VpnConfig config = mConnectivityService.getVpnConfig(mUserId); return config != null && !TextUtils.equals(config.user, mPackageName); } catch (RemoteException e) { Log.w(TAG, "Failure to look up active VPN", e); return false; } } 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(); } } }