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.VpnManager; 32 import android.os.Bundle; 33 import android.os.UserHandle; 34 import android.os.UserManager; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.widget.TextView; 38 39 import androidx.annotation.VisibleForTesting; 40 import androidx.appcompat.app.AlertDialog; 41 import androidx.fragment.app.DialogFragment; 42 import androidx.preference.Preference; 43 import androidx.preference.PreferenceViewHolder; 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.RestrictedLockUtils; 52 import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 53 import com.android.settingslib.RestrictedPreference; 54 import com.android.settingslib.RestrictedSwitchPreference; 55 56 import java.util.List; 57 58 public class AppManagementFragment extends SettingsPreferenceFragment 59 implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener, 60 ConfirmLockdownFragment.ConfirmLockdownListener { 61 62 private static final String TAG = "AppManagementFragment"; 63 64 private static final String ARG_PACKAGE_NAME = "package"; 65 66 private static final String KEY_VERSION = "version"; 67 private static final String KEY_ALWAYS_ON_VPN = "always_on_vpn"; 68 private static final String KEY_LOCKDOWN_VPN = "lockdown_vpn"; 69 private static final String KEY_FORGET_VPN = "forget_vpn"; 70 71 private PackageManager mPackageManager; 72 private DevicePolicyManager mDevicePolicyManager; 73 private VpnManager mVpnManager; 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 RestrictedSwitchPreference mPreferenceAlwaysOn; 83 private RestrictedSwitchPreference mPreferenceLockdown; 84 private RestrictedPreference mPreferenceForget; 85 86 // Listener 87 private final AppDialogFragment.Listener mForgetVpnDialogFragmentListener = 88 new AppDialogFragment.Listener() { 89 @Override 90 public void onForget() { 91 // Unset always-on-vpn when forgetting the VPN 92 if (isVpnAlwaysOn()) { 93 setAlwaysOnVpn(false, false); 94 } 95 // Also dismiss and go back to VPN list 96 finish(); 97 } 98 99 @Override 100 public void onCancel() { 101 // do nothing 102 } 103 }; 104 show(Context context, AppPreference pref, int sourceMetricsCategory)105 public static void show(Context context, AppPreference pref, int sourceMetricsCategory) { 106 final Bundle args = new Bundle(); 107 args.putString(ARG_PACKAGE_NAME, pref.getPackageName()); 108 new SubSettingLauncher(context) 109 .setDestination(AppManagementFragment.class.getName()) 110 .setArguments(args) 111 .setTitleText(pref.getLabel()) 112 .setSourceMetricsCategory(sourceMetricsCategory) 113 .setUserHandle(new UserHandle(pref.getUserId())) 114 .launch(); 115 } 116 117 @Override onCreate(Bundle savedState)118 public void onCreate(Bundle savedState) { 119 super.onCreate(savedState); 120 addPreferencesFromResource(R.xml.vpn_app_management); 121 122 mPackageManager = getContext().getPackageManager(); 123 mDevicePolicyManager = getContext().getSystemService(DevicePolicyManager.class); 124 mVpnManager = getContext().getSystemService(VpnManager.class); 125 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 updateUI(); 142 143 Preference version = getPreferenceScreen().findPreference(KEY_VERSION); 144 if (version != null) { 145 // Version field has been added. 146 return; 147 } 148 149 /** 150 * Create version field at runtime, and set max height on the display area. 151 * 152 * When long length of text given within version field, a large text area 153 * might be created and inconvenient to the user (User need to scroll 154 * for a long time in order to get to the Preferences after this field.) 155 */ 156 version = new Preference(getPrefContext()) { 157 @Override 158 public void onBindViewHolder(PreferenceViewHolder holder) { 159 super.onBindViewHolder(holder); 160 161 TextView titleView = 162 (TextView) holder.findViewById(android.R.id.title); 163 if (titleView != null) { 164 titleView.setTextAppearance(R.style.vpn_app_management_version_title); 165 } 166 167 TextView summaryView = 168 (TextView) holder.findViewById(android.R.id.summary); 169 if (summaryView != null) { 170 summaryView.setTextAppearance(R.style.vpn_app_management_version_summary); 171 172 // Set max height in summary area. 173 int versionMaxHeight = getListView().getHeight(); 174 summaryView.setMaxHeight(versionMaxHeight); 175 summaryView.setVerticalScrollBarEnabled(false); 176 summaryView.setHorizontallyScrolling(false); 177 } 178 } 179 }; 180 version.setOrder(0); // Set order to 0 in order to be placed 181 // in front of other Preference(s). 182 version.setKey(KEY_VERSION); // Set key to avoid from creating multi instance. 183 version.setTitle(R.string.vpn_version); 184 version.setSummary(mPackageInfo.versionName); 185 version.setSelectable(false); 186 getPreferenceScreen().addPreference(version); 187 } else { 188 finish(); 189 } 190 } 191 192 @Override onPreferenceClick(Preference preference)193 public boolean onPreferenceClick(Preference preference) { 194 String key = preference.getKey(); 195 switch (key) { 196 case KEY_FORGET_VPN: 197 return onForgetVpnClick(); 198 default: 199 Log.w(TAG, "unknown key is clicked: " + key); 200 return false; 201 } 202 } 203 204 @Override onPreferenceChange(Preference preference, Object newValue)205 public boolean onPreferenceChange(Preference preference, Object newValue) { 206 switch (preference.getKey()) { 207 case KEY_ALWAYS_ON_VPN: 208 return onAlwaysOnVpnClick((Boolean) newValue, mPreferenceLockdown.isChecked()); 209 case KEY_LOCKDOWN_VPN: 210 return onAlwaysOnVpnClick(mPreferenceAlwaysOn.isChecked(), (Boolean) newValue); 211 default: 212 Log.w(TAG, "unknown key is clicked: " + preference.getKey()); 213 return false; 214 } 215 } 216 217 @Override getMetricsCategory()218 public int getMetricsCategory() { 219 return SettingsEnums.VPN; 220 } 221 onForgetVpnClick()222 private boolean onForgetVpnClick() { 223 updateRestrictedViews(); 224 if (!mPreferenceForget.isEnabled()) { 225 return false; 226 } 227 AppDialogFragment.show(this, mForgetVpnDialogFragmentListener, mPackageInfo, mVpnLabel, 228 true /* editing */, true); 229 return true; 230 } 231 onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown)232 private boolean onAlwaysOnVpnClick(final boolean alwaysOnSetting, final boolean lockdown) { 233 final boolean replacing = isAnotherVpnActive(); 234 final boolean wasLockdown = VpnUtils.isAnyLockdownActive(getActivity()); 235 if (ConfirmLockdownFragment.shouldShow(replacing, wasLockdown, lockdown)) { 236 // Place a dialog to confirm that traffic should be locked down. 237 final Bundle options = null; 238 ConfirmLockdownFragment.show( 239 this, replacing, alwaysOnSetting, wasLockdown, lockdown, options); 240 return false; 241 } 242 // No need to show the dialog. Change the setting straight away. 243 return setAlwaysOnVpnByUI(alwaysOnSetting, lockdown); 244 } 245 246 @Override onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown)247 public void onConfirmLockdown(Bundle options, boolean isEnabled, boolean isLockdown) { 248 setAlwaysOnVpnByUI(isEnabled, isLockdown); 249 } 250 setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown)251 private boolean setAlwaysOnVpnByUI(boolean isEnabled, boolean isLockdown) { 252 updateRestrictedViews(); 253 if (!mPreferenceAlwaysOn.isEnabled()) { 254 return false; 255 } 256 // Only clear legacy lockdown vpn in system user. 257 if (mUserId == UserHandle.USER_SYSTEM) { 258 VpnUtils.clearLockdownVpn(getContext()); 259 } 260 final boolean success = setAlwaysOnVpn(isEnabled, isLockdown); 261 if (isEnabled && (!success || !isVpnAlwaysOn())) { 262 CannotConnectFragment.show(this, mVpnLabel); 263 } else { 264 updateUI(); 265 } 266 return success; 267 } 268 setAlwaysOnVpn(boolean isEnabled, boolean isLockdown)269 private boolean setAlwaysOnVpn(boolean isEnabled, boolean isLockdown) { 270 return mVpnManager.setAlwaysOnVpnPackageForUser(mUserId, 271 isEnabled ? mPackageName : null, isLockdown, /* lockdownAllowlist */ null); 272 } 273 updateUI()274 private void updateUI() { 275 if (isAdded()) { 276 final boolean alwaysOn = isVpnAlwaysOn(); 277 final boolean lockdown = alwaysOn 278 && VpnUtils.isAnyLockdownActive(getActivity()); 279 280 mPreferenceAlwaysOn.setChecked(alwaysOn); 281 mPreferenceLockdown.setChecked(lockdown); 282 updateRestrictedViews(); 283 } 284 } 285 updateRestrictedViews()286 private void updateRestrictedViews() { 287 if (isAdded()) { 288 mPreferenceAlwaysOn.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, 289 mUserId); 290 mPreferenceLockdown.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, 291 mUserId); 292 mPreferenceForget.checkRestrictionAndSetDisabled(UserManager.DISALLOW_CONFIG_VPN, 293 mUserId); 294 295 if (mPackageName.equals(mDevicePolicyManager.getAlwaysOnVpnPackage())) { 296 EnforcedAdmin admin = RestrictedLockUtils.getProfileOrDeviceOwner( 297 getContext(), UserHandle.of(mUserId)); 298 mPreferenceAlwaysOn.setDisabledByAdmin(admin); 299 mPreferenceForget.setDisabledByAdmin(admin); 300 if (mDevicePolicyManager.isAlwaysOnVpnLockdownEnabled()) { 301 mPreferenceLockdown.setDisabledByAdmin(admin); 302 } 303 } 304 if (mVpnManager.isAlwaysOnVpnPackageSupportedForUser(mUserId, mPackageName)) { 305 // setSummary doesn't override the admin message when user restriction is applied 306 mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary); 307 // setEnabled is not required here, as checkRestrictionAndSetDisabled 308 // should have refreshed the enable state. 309 } else { 310 mPreferenceAlwaysOn.setEnabled(false); 311 mPreferenceLockdown.setEnabled(false); 312 mPreferenceAlwaysOn.setSummary(R.string.vpn_always_on_summary_not_supported); 313 } 314 } 315 } 316 getAlwaysOnVpnPackage()317 private String getAlwaysOnVpnPackage() { 318 return mVpnManager.getAlwaysOnVpnPackageForUser(mUserId); 319 } 320 isVpnAlwaysOn()321 private boolean isVpnAlwaysOn() { 322 return mPackageName.equals(getAlwaysOnVpnPackage()); 323 } 324 325 /** 326 * @return false if the intent doesn't contain an existing package or can't retrieve activated 327 * vpn info. 328 */ loadInfo()329 private boolean loadInfo() { 330 final Bundle args = getArguments(); 331 if (args == null) { 332 Log.e(TAG, "empty bundle"); 333 return false; 334 } 335 336 mPackageName = args.getString(ARG_PACKAGE_NAME); 337 if (mPackageName == null) { 338 Log.e(TAG, "empty package name"); 339 return false; 340 } 341 342 try { 343 mPackageInfo = mPackageManager.getPackageInfo(mPackageName, /* PackageInfoFlags */ 0); 344 mVpnLabel = VpnConfig.getVpnLabel(getPrefContext(), mPackageName).toString(); 345 } catch (NameNotFoundException nnfe) { 346 Log.e(TAG, "package not found", nnfe); 347 return false; 348 } 349 350 if (mPackageInfo.applicationInfo == null) { 351 Log.e(TAG, "package does not include an application"); 352 return false; 353 } 354 if (!appHasVpnPermission(getContext(), mPackageInfo.applicationInfo)) { 355 Log.e(TAG, "package didn't register VPN profile"); 356 return false; 357 } 358 359 return true; 360 } 361 362 @VisibleForTesting appHasVpnPermission(Context context, @NonNull ApplicationInfo application)363 static boolean appHasVpnPermission(Context context, @NonNull ApplicationInfo application) { 364 final AppOpsManager service = 365 (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 366 final List<AppOpsManager.PackageOps> ops = service.getOpsForPackage(application.uid, 367 application.packageName, new int[]{OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN}); 368 return !ArrayUtils.isEmpty(ops); 369 } 370 371 /** 372 * @return {@code true} if another VPN (VpnService or legacy) is connected or set as always-on. 373 */ isAnotherVpnActive()374 private boolean isAnotherVpnActive() { 375 final VpnConfig config = mVpnManager.getVpnConfig(mUserId); 376 return config != null && !TextUtils.equals(config.user, mPackageName); 377 } 378 379 public static class CannotConnectFragment extends InstrumentedDialogFragment { 380 private static final String TAG = "CannotConnect"; 381 private static final String ARG_VPN_LABEL = "label"; 382 383 @Override getMetricsCategory()384 public int getMetricsCategory() { 385 return SettingsEnums.DIALOG_VPN_CANNOT_CONNECT; 386 } 387 show(AppManagementFragment parent, String vpnLabel)388 public static void show(AppManagementFragment parent, String vpnLabel) { 389 if (parent.getFragmentManager().findFragmentByTag(TAG) == null) { 390 final Bundle args = new Bundle(); 391 args.putString(ARG_VPN_LABEL, vpnLabel); 392 393 final DialogFragment frag = new CannotConnectFragment(); 394 frag.setArguments(args); 395 frag.show(parent.getFragmentManager(), TAG); 396 } 397 } 398 399 @Override onCreateDialog(Bundle savedInstanceState)400 public Dialog onCreateDialog(Bundle savedInstanceState) { 401 final String vpnLabel = getArguments().getString(ARG_VPN_LABEL); 402 return new AlertDialog.Builder(getActivity()) 403 .setTitle(getActivity().getString(R.string.vpn_cant_connect_title, vpnLabel)) 404 .setMessage(getActivity().getString(R.string.vpn_cant_connect_message)) 405 .setPositiveButton(R.string.okay, null) 406 .create(); 407 } 408 } 409 } 410