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