1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settings.vpn2; 17 18 import static android.app.AppOpsManager.OP_ACTIVATE_PLATFORM_VPN; 19 import static android.app.AppOpsManager.OP_ACTIVATE_VPN; 20 21 import android.app.AppOpsManager; 22 import android.app.Dialog; 23 import android.app.admin.DevicePolicyManager; 24 import android.app.settings.SettingsEnums; 25 import android.content.Context; 26 import android.content.pm.ApplicationInfo; 27 import android.content.pm.PackageInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.PackageManager.NameNotFoundException; 30 import android.net.VpnManager; 31 import android.os.Bundle; 32 import android.os.UserHandle; 33 import android.os.UserManager; 34 import android.text.TextUtils; 35 import android.util.Log; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.VisibleForTesting; 39 import androidx.appcompat.app.AlertDialog; 40 import androidx.fragment.app.DialogFragment; 41 import androidx.preference.Preference; 42 43 import com.android.internal.net.VpnConfig; 44 import com.android.internal.util.ArrayUtils; 45 import com.android.settings.R; 46 import com.android.settings.SettingsPreferenceFragment; 47 import com.android.settings.core.SubSettingLauncher; 48 import com.android.settings.core.instrumentation.InstrumentedDialogFragment; 49 import com.android.settings.overlay.FeatureFactory; 50 import com.android.settingslib.RestrictedLockUtils; 51 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 52 import com.android.settingslib.RestrictedPreference; 53 import com.android.settingslib.RestrictedSwitchPreference; 54 55 import java.util.List; 56 57 public class AppManagementFragment extends SettingsPreferenceFragment 58 implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener, 59 ConfirmLockdownFragment.ConfirmLockdownListener { 60 61 private static final String TAG = "AppManagementFragment"; 62 63 private static final String ARG_PACKAGE_NAME = "package"; 64 65 private static final String KEY_VERSION = "version"; 66 private static final String KEY_ALWAYS_ON_VPN = "always_on_vpn"; 67 private static final String KEY_LOCKDOWN_VPN = "lockdown_vpn"; 68 private static final String KEY_FORGET_VPN = "forget_vpn"; 69 70 private PackageManager mPackageManager; 71 private DevicePolicyManager mDevicePolicyManager; 72 private VpnManager mVpnManager; 73 private AdvancedVpnFeatureProvider mFeatureProvider; 74 75 // VPN app info 76 private final int mUserId = UserHandle.myUserId(); 77 private String mPackageName; 78 private PackageInfo mPackageInfo; 79 private String mVpnLabel; 80 81 // UI preference 82 private Preference mPreferenceVersion; 83 private RestrictedSwitchPreference mPreferenceAlwaysOn; 84 private RestrictedSwitchPreference mPreferenceLockdown; 85 private RestrictedPreference mPreferenceForget; 86 87 // Listener 88 private final AppDialogFragment.Listener mForgetVpnDialogFragmentListener = 89 new AppDialogFragment.Listener() { 90 @Override 91 public void onForget() { 92 // Unset always-on-vpn when forgetting the VPN 93 if (isVpnAlwaysOn()) { 94 setAlwaysOnVpn(false, false); 95 } 96 // Also dismiss and go back to VPN list 97 finish(); 98 } 99 100 @Override 101 public void onCancel() { 102 // do nothing 103 } 104 }; 105 show(Context context, AppPreference pref, int sourceMetricsCategory)106 public static void show(Context context, AppPreference pref, int sourceMetricsCategory) { 107 final Bundle args = new Bundle(); 108 args.putString(ARG_PACKAGE_NAME, pref.getPackageName()); 109 new SubSettingLauncher(context) 110 .setDestination(AppManagementFragment.class.getName()) 111 .setArguments(args) 112 .setTitleText(pref.getLabel()) 113 .setSourceMetricsCategory(sourceMetricsCategory) 114 .setUserHandle(new UserHandle(pref.getUserId())) 115 .launch(); 116 } 117 118 @Override onCreate(Bundle savedState)119 public void onCreate(Bundle savedState) { 120 super.onCreate(savedState); 121 addPreferencesFromResource(R.xml.vpn_app_management); 122 123 mPackageManager = getContext().getPackageManager(); 124 mDevicePolicyManager = getContext().getSystemService(DevicePolicyManager.class); 125 mVpnManager = getContext().getSystemService(VpnManager.class); 126 mFeatureProvider = FeatureFactory.getFeatureFactory().getAdvancedVpnFeatureProvider(); 127 128 mPreferenceVersion = findPreference(KEY_VERSION); 129 mPreferenceAlwaysOn = (RestrictedSwitchPreference) findPreference(KEY_ALWAYS_ON_VPN); 130 mPreferenceLockdown = (RestrictedSwitchPreference) findPreference(KEY_LOCKDOWN_VPN); 131 mPreferenceForget = (RestrictedPreference) findPreference(KEY_FORGET_VPN); 132 133 mPreferenceAlwaysOn.setOnPreferenceChangeListener(this); 134 mPreferenceLockdown.setOnPreferenceChangeListener(this); 135 mPreferenceForget.setOnPreferenceClickListener(this); 136 } 137 138 @Override onResume()139 public void onResume() { 140 super.onResume(); 141 142 boolean isInfoLoaded = loadInfo(); 143 if (isInfoLoaded) { 144 mPreferenceVersion.setSummary(mPackageInfo.versionName); 145 updateUI(); 146 } else { 147 finish(); 148 } 149 } 150 151 @Override onPreferenceClick(Preference preference)152 public boolean onPreferenceClick(Preference preference) { 153 String key = preference.getKey(); 154 switch (key) { 155 case KEY_FORGET_VPN: 156 return onForgetVpnClick(); 157 default: 158 Log.w(TAG, "unknown key is clicked: " + key); 159 return false; 160 } 161 } 162 163 @Override onPreferenceChange(Preference preference, Object newValue)164 public boolean onPreferenceChange(Preference preference, Object newValue) { 165 switch (preference.getKey()) { 166 case KEY_ALWAYS_ON_VPN: 167 return onAlwaysOnVpnClick((Boolean) newValue, mPreferenceLockdown.isChecked()); 168 case KEY_LOCKDOWN_VPN: 169 return onAlwaysOnVpnClick(mPreferenceAlwaysOn.isChecked(), (Boolean) newValue); 170 default: 171 Log.w(TAG, "unknown key is clicked: " + preference.getKey()); 172 return false; 173 } 174 } 175 176 @Override getMetricsCategory()177 public int getMetricsCategory() { 178 return SettingsEnums.VPN_APP_MANAGEMENT; 179 } 180 onForgetVpnClick()181 private boolean onForgetVpnClick() { 182 updateRestrictedViews(); 183 if (!mPreferenceForget.isEnabled()) { 184 return false; 185 } 186 AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel, 187 true /* editing */, true); 188 return true; 189 } 190 onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown)191 private boolean onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown) { 192 final boolean replacing = isAnotherVpnActive(); 193 final boolean wasLockdown = VpnUtils.isAnyLockdownActive(getActivity()); 194 if (ConfirmLockdownFragment.shouldShow(replacing, wasLockdown, lockdown)) { 195 // Place a dialog to confirm that traffic should be locked down. 196 final Bundle options = null; 197 ConfirmLockdownFragment.show( 198 this, replacing, alwaysOnSetting, wasLockdown, lockdown, options); 199 return false; 200 } 201 // No need to show the dialog. Change the setting straight away. 202 return setAlwaysOnVpnByUI(alwaysOnSetting, lockdown); 203 } 204 205 @Override onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown)206 public void onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown) { 207 setAlwaysOnVpnByUI(isEnabled, isLockdown); 208 } 209 setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown)210 private boolean setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown) { 211 updateRestrictedViews(); 212 if (!mPreferenceAlwaysOn.isEnabled()) { 213 return false; 214 } 215 // Only clear legacy lockdown vpn in system user. 216 if (mUserId == UserHandle.USER_SYSTEM) { 217 VpnUtils.clearLockdownVpn(getContext()); 218 } 219 final boolean success = setAlwaysOnVpn(isEnabled, isLockdown); 220 if (isEnabled && (!success || !isVpnAlwaysOn())) { 221 CannotConnectFragment.show(this, mVpnLabel); 222 } else { 223 updateUI(); 224 } 225 return success; 226 } 227 setAlwaysOnVpn(boolean isEnabled, boolean isLockdown)228 private boolean setAlwaysOnVpn(boolean isEnabled, boolean isLockdown) { 229 return mVpnManager.setAlwaysOnVpnPackageForUser(mUserId, 230 isEnabled ? mPackageName : null, isLockdown, /* lockdownAllowlist */ null); 231 } 232 updateUI()233 private void updateUI() { 234 if (isAdded()) { 235 final boolean alwaysOn = isVpnAlwaysOn(); 236 final boolean lockdown = alwaysOn 237 && VpnUtils.isAnyLockdownActive(getActivity()); 238 239 mPreferenceAlwaysOn.setChecked(alwaysOn); 240 mPreferenceLockdown.setChecked(lockdown); 241 updateRestrictedViews(); 242 } 243 } 244 245 @VisibleForTesting updateRestrictedViews()246 void updateRestrictedViews() { 247 if (mFeatureProvider.isAdvancedVpnSupported(getContext()) 248 && !mFeatureProvider.isAdvancedVpnRemovable() 249 && TextUtils.equals(mPackageName, mFeatureProvider.getAdvancedVpnPackageName())) { 250 mPreferenceForget.setVisible(false); 251 } else { 252 mPreferenceForget.setVisible(true); 253 } 254 255 if (isAdded()) { 256 mPreferenceAlwaysOn.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, 257 mUserId); 258 mPreferenceLockdown.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, 259 mUserId); 260 mPreferenceForget.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, 261 mUserId); 262 263 if (mPackageName.equals(mDevicePolicyManager.getAlwaysOnVpnPackage())) { 264 EnforcedAdmin admin = RestrictedLockUtils.getProfileOrDeviceOwner( 265 getContext(), UserHandle.of(mUserId)); 266 mPreferenceAlwaysOn.setDisabledByAdmin(admin); 267 mPreferenceForget.setDisabledByAdmin(admin); 268 if (mDevicePolicyManager.isAlwaysOnVpnLockdownEnabled()) { 269 mPreferenceLockdown.setDisabledByAdmin(admin); 270 } 271 } 272 if (mVpnManager.isAlwaysOnVpnPackageSupportedForUser(mUserId, mPackageName)) { 273 // setSummary doesn't override the admin message when user restriction is applied 274 mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary); 275 // setEnabled is not required here, as checkRestrictionAndSetDisabled 276 // should have refreshed the enable state. 277 } else { 278 mPreferenceAlwaysOn.setEnabled(false); 279 mPreferenceLockdown.setEnabled(false); 280 mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary_not_supported); 281 } 282 } 283 } 284 285 @VisibleForTesting init(String packageName, AdvancedVpnFeatureProvider featureProvider, RestrictedPreference preference)286 void init(String packageName, AdvancedVpnFeatureProvider featureProvider, 287 RestrictedPreference preference) { 288 mPackageName = packageName; 289 mFeatureProvider = featureProvider; 290 mPreferenceForget = preference; 291 } 292 getAlwaysOnVpnPackage()293 private String getAlwaysOnVpnPackage() { 294 return mVpnManager.getAlwaysOnVpnPackageForUser(mUserId); 295 } 296 isVpnAlwaysOn()297 private boolean isVpnAlwaysOn() { 298 return mPackageName.equals(getAlwaysOnVpnPackage()); 299 } 300 301 /** 302 * @return false if the intent doesn't contain an existing package or can't retrieve activated 303 * vpn info. 304 */ loadInfo()305 private boolean loadInfo() { 306 final Bundle args = getArguments(); 307 if (args == null) { 308 Log.e(TAG, "empty bundle"); 309 return false; 310 } 311 312 mPackageName = args.getString(ARG_PACKAGE_NAME); 313 if (mPackageName == null) { 314 Log.e(TAG, "empty package name"); 315 return false; 316 } 317 318 try { 319 mPackageInfo = mPackageManager.getPackageInfo(mPackageName, /* PackageInfoFlags */ 0); 320 mVpnLabel = VpnConfig.getVpnLabel(getPrefContext(), mPackageName).toString(); 321 } catch (NameNotFoundException nnfe) { 322 Log.e(TAG, "package not found", nnfe); 323 return false; 324 } 325 326 if (mPackageInfo.applicationInfo == null) { 327 Log.e(TAG, "package does not include an application"); 328 return false; 329 } 330 if (!appHasVpnPermission(getContext(), mPackageInfo.applicationInfo)) { 331 Log.e(TAG, "package didn't register VPN profile"); 332 return false; 333 } 334 335 return true; 336 } 337 338 @VisibleForTesting appHasVpnPermission(Context context, @NonNull ApplicationInfo application)339 static boolean appHasVpnPermission(Context context, @NonNull ApplicationInfo application) { 340 final AppOpsManager service = 341 (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 342 final List<AppOpsManager.PackageOps> ops = service.getOpsForPackage(application.uid, 343 application.packageName, new int[]{OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN}); 344 return !ArrayUtils.isEmpty(ops); 345 } 346 347 /** 348 * @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on. 349 */ isAnotherVpnActive()350 private boolean isAnotherVpnActive() { 351 final VpnConfig config = mVpnManager.getVpnConfig(mUserId); 352 return config != null && !TextUtils.equals(config.user, mPackageName); 353 } 354 355 public static class CannotConnectFragment extends InstrumentedDialogFragment { 356 private static final String TAG = "CannotConnect"; 357 private static final String ARG_VPN_LABEL = "label"; 358 359 @Override getMetricsCategory()360 public int getMetricsCategory() { 361 return SettingsEnums.DIALOG_VPN_CANNOT_CONNECT; 362 } 363 show(AppManagementFragment parent, String vpnLabel)364 public static void show(AppManagementFragment parent, String vpnLabel) { 365 if (parent.getFragmentManager().findFragmentByTag(TAG) == null) { 366 final Bundle args = new Bundle(); 367 args.putString(ARG_VPN_LABEL, vpnLabel); 368 369 final DialogFragment frag = new CannotConnectFragment(); 370 frag.setArguments(args); 371 frag.show(parent.getFragmentManager(), TAG); 372 } 373 } 374 375 @Override onCreateDialog(Bundle savedInstanceState)376 public Dialog onCreateDialog(Bundle savedInstanceState) { 377 final String vpnLabel = getArguments().getString(ARG_VPN_LABEL); 378 return new AlertDialog.Builder(getActivity()) 379 .setTitle(getActivity().getString(R.string.vpn_cant_connect_title, vpnLabel)) 380 .setMessage(getActivity().getString(R.string.vpn_cant_connect_message)) 381 .setPositiveButton(R.string.okay, null) 382 .create(); 383 } 384 } 385 } 386