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