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