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