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