1 /* 2 * Copyright (C) 2011 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 17 package com.android.settings.vpn2; 18 19 import static android.app.AppOpsManager.OP_ACTIVATE_PLATFORM_VPN; 20 import static android.app.AppOpsManager.OP_ACTIVATE_VPN; 21 22 import android.annotation.UiThread; 23 import android.annotation.WorkerThread; 24 import android.app.Activity; 25 import android.app.AppOpsManager; 26 import android.app.settings.SettingsEnums; 27 import android.content.Context; 28 import android.content.Intent; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager; 31 import android.net.ConnectivityManager; 32 import android.net.ConnectivityManager.NetworkCallback; 33 import android.net.IConnectivityManager; 34 import android.net.Network; 35 import android.net.NetworkCapabilities; 36 import android.net.NetworkRequest; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.HandlerThread; 40 import android.os.Message; 41 import android.os.RemoteException; 42 import android.os.ServiceManager; 43 import android.os.UserHandle; 44 import android.os.UserManager; 45 import android.security.Credentials; 46 import android.security.KeyStore; 47 import android.util.ArrayMap; 48 import android.util.ArraySet; 49 import android.util.Log; 50 import android.view.Menu; 51 import android.view.MenuInflater; 52 import android.view.MenuItem; 53 54 import androidx.annotation.VisibleForTesting; 55 import androidx.preference.Preference; 56 import androidx.preference.PreferenceGroup; 57 58 import com.android.internal.annotations.GuardedBy; 59 import com.android.internal.net.LegacyVpnInfo; 60 import com.android.internal.net.VpnConfig; 61 import com.android.internal.net.VpnProfile; 62 import com.android.internal.util.ArrayUtils; 63 import com.android.settings.R; 64 import com.android.settings.RestrictedSettingsFragment; 65 import com.android.settings.widget.GearPreference; 66 import com.android.settings.widget.GearPreference.OnGearClickListener; 67 import com.android.settingslib.RestrictedLockUtilsInternal; 68 69 import com.google.android.collect.Lists; 70 71 import java.util.ArrayList; 72 import java.util.Collection; 73 import java.util.Collections; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.Set; 77 78 /** 79 * Settings screen listing VPNs. Configured VPNs and networks managed by apps 80 * are shown in the same list. 81 */ 82 public class VpnSettings extends RestrictedSettingsFragment implements 83 Handler.Callback, Preference.OnPreferenceClickListener { 84 private static final String LOG_TAG = "VpnSettings"; 85 86 private static final int RESCAN_MESSAGE = 0; 87 private static final int RESCAN_INTERVAL_MS = 1000; 88 89 private static final NetworkRequest VPN_REQUEST = new NetworkRequest.Builder() 90 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) 91 .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) 92 .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) 93 .build(); 94 95 private final IConnectivityManager mConnectivityService = IConnectivityManager.Stub 96 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 97 private ConnectivityManager mConnectivityManager; 98 private UserManager mUserManager; 99 100 private final KeyStore mKeyStore = KeyStore.getInstance(); 101 102 private Map<String, LegacyVpnPreference> mLegacyVpnPreferences = new ArrayMap<>(); 103 private Map<AppVpnInfo, AppPreference> mAppPreferences = new ArrayMap<>(); 104 105 @GuardedBy("this") 106 private Handler mUpdater; 107 private HandlerThread mUpdaterThread; 108 private LegacyVpnInfo mConnectedLegacyVpn; 109 110 private boolean mUnavailable; 111 VpnSettings()112 public VpnSettings() { 113 super(UserManager.DISALLOW_CONFIG_VPN); 114 } 115 116 @Override getMetricsCategory()117 public int getMetricsCategory() { 118 return SettingsEnums.VPN; 119 } 120 121 @Override onActivityCreated(Bundle savedInstanceState)122 public void onActivityCreated(Bundle savedInstanceState) { 123 super.onActivityCreated(savedInstanceState); 124 125 mUserManager = (UserManager) getSystemService(Context.USER_SERVICE); 126 mConnectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 127 128 mUnavailable = isUiRestricted(); 129 setHasOptionsMenu(!mUnavailable); 130 131 addPreferencesFromResource(R.xml.vpn_settings2); 132 } 133 134 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)135 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 136 super.onCreateOptionsMenu(menu, inflater); 137 inflater.inflate(R.menu.vpn, menu); 138 } 139 140 @Override onPrepareOptionsMenu(Menu menu)141 public void onPrepareOptionsMenu(Menu menu) { 142 super.onPrepareOptionsMenu(menu); 143 144 // Disable all actions if VPN configuration has been disallowed 145 for (int i = 0; i < menu.size(); i++) { 146 if (isUiRestrictedByOnlyAdmin()) { 147 RestrictedLockUtilsInternal.setMenuItemAsDisabledByAdmin(getPrefContext(), 148 menu.getItem(i), getRestrictionEnforcedAdmin()); 149 } else { 150 menu.getItem(i).setEnabled(!mUnavailable); 151 } 152 } 153 } 154 155 @Override onOptionsItemSelected(MenuItem item)156 public boolean onOptionsItemSelected(MenuItem item) { 157 // Generate a new key. Here we just use the current time. 158 if (item.getItemId() == R.id.vpn_create) { 159 long millis = System.currentTimeMillis(); 160 while (mLegacyVpnPreferences.containsKey(Long.toHexString(millis))) { 161 ++millis; 162 } 163 VpnProfile profile = new VpnProfile(Long.toHexString(millis)); 164 ConfigDialogFragment.show(this, profile, true /* editing */, false /* exists */); 165 return true; 166 } 167 return super.onOptionsItemSelected(item); 168 } 169 170 @Override onResume()171 public void onResume() { 172 super.onResume(); 173 174 mUnavailable = mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_VPN); 175 if (mUnavailable) { 176 // Show a message to explain that VPN settings have been disabled 177 if (!isUiRestrictedByOnlyAdmin()) { 178 getEmptyTextView().setText(R.string.vpn_settings_not_available); 179 } 180 getPreferenceScreen().removeAll(); 181 return; 182 } else { 183 setEmptyView(getEmptyTextView()); 184 getEmptyTextView().setText(R.string.vpn_no_vpns_added); 185 } 186 187 // Start monitoring 188 mConnectivityManager.registerNetworkCallback(VPN_REQUEST, mNetworkCallback); 189 190 // Trigger a refresh 191 mUpdaterThread = new HandlerThread("Refresh VPN list in background"); 192 mUpdaterThread.start(); 193 mUpdater = new Handler(mUpdaterThread.getLooper(), this); 194 mUpdater.sendEmptyMessage(RESCAN_MESSAGE); 195 } 196 197 @Override onPause()198 public void onPause() { 199 if (mUnavailable) { 200 super.onPause(); 201 return; 202 } 203 204 // Stop monitoring 205 mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); 206 207 synchronized (this) { 208 mUpdater.removeCallbacksAndMessages(null); 209 mUpdater = null; 210 mUpdaterThread.quit(); 211 mUpdaterThread = null; 212 } 213 214 super.onPause(); 215 } 216 217 @Override @WorkerThread handleMessage(Message message)218 public boolean handleMessage(Message message) { 219 //Return if activity has been recycled 220 final Activity activity = getActivity(); 221 if (activity == null) { 222 return true; 223 } 224 final Context context = activity.getApplicationContext(); 225 226 // Run heavy RPCs before switching to UI thread 227 final List<VpnProfile> vpnProfiles = loadVpnProfiles(mKeyStore); 228 final List<AppVpnInfo> vpnApps = getVpnApps(context, /* includeProfiles */ true); 229 230 final Map<String, LegacyVpnInfo> connectedLegacyVpns = getConnectedLegacyVpns(); 231 final Set<AppVpnInfo> connectedAppVpns = getConnectedAppVpns(); 232 233 final Set<AppVpnInfo> alwaysOnAppVpnInfos = getAlwaysOnAppVpnInfos(); 234 final String lockdownVpnKey = VpnUtils.getLockdownVpn(); 235 236 // Refresh list of VPNs 237 activity.runOnUiThread(new UpdatePreferences(this) 238 .legacyVpns(vpnProfiles, connectedLegacyVpns, lockdownVpnKey) 239 .appVpns(vpnApps, connectedAppVpns, alwaysOnAppVpnInfos)); 240 241 synchronized (this) { 242 if (mUpdater != null) { 243 mUpdater.removeMessages(RESCAN_MESSAGE); 244 mUpdater.sendEmptyMessageDelayed(RESCAN_MESSAGE, RESCAN_INTERVAL_MS); 245 } 246 } 247 return true; 248 } 249 250 @VisibleForTesting 251 static class UpdatePreferences implements Runnable { 252 private List<VpnProfile> vpnProfiles = Collections.<VpnProfile>emptyList(); 253 private List<AppVpnInfo> vpnApps = Collections.<AppVpnInfo>emptyList(); 254 255 private Map<String, LegacyVpnInfo> connectedLegacyVpns = 256 Collections.<String, LegacyVpnInfo>emptyMap(); 257 private Set<AppVpnInfo> connectedAppVpns = Collections.<AppVpnInfo>emptySet(); 258 259 private Set<AppVpnInfo> alwaysOnAppVpnInfos = Collections.<AppVpnInfo>emptySet(); 260 private String lockdownVpnKey = null; 261 262 private final VpnSettings mSettings; 263 UpdatePreferences(VpnSettings settings)264 public UpdatePreferences(VpnSettings settings) { 265 mSettings = settings; 266 } 267 legacyVpns(List<VpnProfile> vpnProfiles, Map<String, LegacyVpnInfo> connectedLegacyVpns, String lockdownVpnKey)268 public final UpdatePreferences legacyVpns(List<VpnProfile> vpnProfiles, 269 Map<String, LegacyVpnInfo> connectedLegacyVpns, String lockdownVpnKey) { 270 this.vpnProfiles = vpnProfiles; 271 this.connectedLegacyVpns = connectedLegacyVpns; 272 this.lockdownVpnKey = lockdownVpnKey; 273 return this; 274 } 275 appVpns(List<AppVpnInfo> vpnApps, Set<AppVpnInfo> connectedAppVpns, Set<AppVpnInfo> alwaysOnAppVpnInfos)276 public final UpdatePreferences appVpns(List<AppVpnInfo> vpnApps, 277 Set<AppVpnInfo> connectedAppVpns, Set<AppVpnInfo> alwaysOnAppVpnInfos) { 278 this.vpnApps = vpnApps; 279 this.connectedAppVpns = connectedAppVpns; 280 this.alwaysOnAppVpnInfos = alwaysOnAppVpnInfos; 281 return this; 282 } 283 284 @Override @UiThread run()285 public void run() { 286 if (!mSettings.canAddPreferences()) { 287 return; 288 } 289 290 // Find new VPNs by subtracting existing ones from the full set 291 final Set<Preference> updates = new ArraySet<>(); 292 293 // Add legacy VPNs 294 for (VpnProfile profile : vpnProfiles) { 295 LegacyVpnPreference p = mSettings.findOrCreatePreference(profile, true); 296 if (connectedLegacyVpns.containsKey(profile.key)) { 297 p.setState(connectedLegacyVpns.get(profile.key).state); 298 } else { 299 p.setState(LegacyVpnPreference.STATE_NONE); 300 } 301 p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(profile.key)); 302 updates.add(p); 303 } 304 305 // Show connected VPNs even if the original entry in keystore is gone 306 for (LegacyVpnInfo vpn : connectedLegacyVpns.values()) { 307 final VpnProfile stubProfile = new VpnProfile(vpn.key); 308 LegacyVpnPreference p = mSettings.findOrCreatePreference(stubProfile, false); 309 p.setState(vpn.state); 310 p.setAlwaysOn(lockdownVpnKey != null && lockdownVpnKey.equals(vpn.key)); 311 updates.add(p); 312 } 313 314 // Add VpnService VPNs 315 for (AppVpnInfo app : vpnApps) { 316 AppPreference p = mSettings.findOrCreatePreference(app); 317 if (connectedAppVpns.contains(app)) { 318 p.setState(AppPreference.STATE_CONNECTED); 319 } else { 320 p.setState(AppPreference.STATE_DISCONNECTED); 321 } 322 p.setAlwaysOn(alwaysOnAppVpnInfos.contains(app)); 323 updates.add(p); 324 } 325 326 // Trim out deleted VPN preferences 327 mSettings.setShownPreferences(updates); 328 } 329 } 330 331 @VisibleForTesting canAddPreferences()332 public boolean canAddPreferences() { 333 return isAdded(); 334 } 335 336 @VisibleForTesting @UiThread setShownPreferences(final Collection<Preference> updates)337 public void setShownPreferences(final Collection<Preference> updates) { 338 mLegacyVpnPreferences.values().retainAll(updates); 339 mAppPreferences.values().retainAll(updates); 340 341 // Change {@param updates} in-place to only contain new preferences that were not already 342 // added to the preference screen. 343 final PreferenceGroup vpnGroup = getPreferenceScreen(); 344 for (int i = vpnGroup.getPreferenceCount() - 1; i >= 0; i--) { 345 Preference p = vpnGroup.getPreference(i); 346 if (updates.contains(p)) { 347 updates.remove(p); 348 } else { 349 vpnGroup.removePreference(p); 350 } 351 } 352 353 // Show any new preferences on the screen 354 for (Preference pref : updates) { 355 vpnGroup.addPreference(pref); 356 } 357 } 358 359 @Override onPreferenceClick(Preference preference)360 public boolean onPreferenceClick(Preference preference) { 361 if (preference instanceof LegacyVpnPreference) { 362 LegacyVpnPreference pref = (LegacyVpnPreference) preference; 363 VpnProfile profile = pref.getProfile(); 364 if (mConnectedLegacyVpn != null && profile.key.equals(mConnectedLegacyVpn.key) && 365 mConnectedLegacyVpn.state == LegacyVpnInfo.STATE_CONNECTED) { 366 try { 367 mConnectedLegacyVpn.intent.send(); 368 return true; 369 } catch (Exception e) { 370 Log.w(LOG_TAG, "Starting config intent failed", e); 371 } 372 } 373 ConfigDialogFragment.show(this, profile, false /* editing */, true /* exists */); 374 return true; 375 } else if (preference instanceof AppPreference) { 376 AppPreference pref = (AppPreference) preference; 377 boolean connected = (pref.getState() == AppPreference.STATE_CONNECTED); 378 379 if (!connected) { 380 try { 381 UserHandle user = UserHandle.of(pref.getUserId()); 382 Context userContext = getActivity().createPackageContextAsUser( 383 getActivity().getPackageName(), 0 /* flags */, user); 384 PackageManager pm = userContext.getPackageManager(); 385 Intent appIntent = pm.getLaunchIntentForPackage(pref.getPackageName()); 386 if (appIntent != null) { 387 userContext.startActivityAsUser(appIntent, user); 388 return true; 389 } 390 } catch (PackageManager.NameNotFoundException nnfe) { 391 Log.w(LOG_TAG, "VPN provider does not exist: " + pref.getPackageName(), nnfe); 392 } 393 } 394 395 // Already connected or no launch intent available - show an info dialog 396 PackageInfo pkgInfo = pref.getPackageInfo(); 397 AppDialogFragment.show(this, pkgInfo, pref.getLabel(), false /* editing */, connected); 398 return true; 399 } 400 return false; 401 } 402 403 @Override getHelpResource()404 public int getHelpResource() { 405 return R.string.help_url_vpn; 406 } 407 408 private OnGearClickListener mGearListener = new OnGearClickListener() { 409 @Override 410 public void onGearClick(GearPreference p) { 411 if (p instanceof LegacyVpnPreference) { 412 LegacyVpnPreference pref = (LegacyVpnPreference) p; 413 ConfigDialogFragment.show(VpnSettings.this, pref.getProfile(), true /* editing */, 414 true /* exists */); 415 } else if (p instanceof AppPreference) { 416 AppPreference pref = (AppPreference) p; 417 AppManagementFragment.show(getPrefContext(), pref, getMetricsCategory()); 418 } 419 } 420 }; 421 422 private NetworkCallback mNetworkCallback = new NetworkCallback() { 423 @Override 424 public void onAvailable(Network network) { 425 if (mUpdater != null) { 426 mUpdater.sendEmptyMessage(RESCAN_MESSAGE); 427 } 428 } 429 430 @Override 431 public void onLost(Network network) { 432 if (mUpdater != null) { 433 mUpdater.sendEmptyMessage(RESCAN_MESSAGE); 434 } 435 } 436 }; 437 438 @VisibleForTesting @UiThread findOrCreatePreference(VpnProfile profile, boolean update)439 public LegacyVpnPreference findOrCreatePreference(VpnProfile profile, boolean update) { 440 LegacyVpnPreference pref = mLegacyVpnPreferences.get(profile.key); 441 boolean created = false; 442 if (pref == null ) { 443 pref = new LegacyVpnPreference(getPrefContext()); 444 pref.setOnGearClickListener(mGearListener); 445 pref.setOnPreferenceClickListener(this); 446 mLegacyVpnPreferences.put(profile.key, pref); 447 created = true; 448 } 449 if (created || update) { 450 // This can change call-to-call because the profile can update and keep the same key. 451 pref.setProfile(profile); 452 } 453 return pref; 454 } 455 456 @VisibleForTesting @UiThread findOrCreatePreference(AppVpnInfo app)457 public AppPreference findOrCreatePreference(AppVpnInfo app) { 458 AppPreference pref = mAppPreferences.get(app); 459 if (pref == null) { 460 pref = new AppPreference(getPrefContext(), app.userId, app.packageName); 461 pref.setOnGearClickListener(mGearListener); 462 pref.setOnPreferenceClickListener(this); 463 mAppPreferences.put(app, pref); 464 } 465 return pref; 466 } 467 468 @WorkerThread getConnectedLegacyVpns()469 private Map<String, LegacyVpnInfo> getConnectedLegacyVpns() { 470 try { 471 mConnectedLegacyVpn = mConnectivityService.getLegacyVpnInfo(UserHandle.myUserId()); 472 if (mConnectedLegacyVpn != null) { 473 return Collections.singletonMap(mConnectedLegacyVpn.key, mConnectedLegacyVpn); 474 } 475 } catch (RemoteException e) { 476 Log.e(LOG_TAG, "Failure updating VPN list with connected legacy VPNs", e); 477 } 478 return Collections.emptyMap(); 479 } 480 481 @WorkerThread getConnectedAppVpns()482 private Set<AppVpnInfo> getConnectedAppVpns() { 483 // Mark connected third-party services 484 Set<AppVpnInfo> connections = new ArraySet<>(); 485 try { 486 for (UserHandle profile : mUserManager.getUserProfiles()) { 487 VpnConfig config = mConnectivityService.getVpnConfig(profile.getIdentifier()); 488 if (config != null && !config.legacy) { 489 connections.add(new AppVpnInfo(profile.getIdentifier(), config.user)); 490 } 491 } 492 } catch (RemoteException e) { 493 Log.e(LOG_TAG, "Failure updating VPN list with connected app VPNs", e); 494 } 495 return connections; 496 } 497 498 @WorkerThread getAlwaysOnAppVpnInfos()499 private Set<AppVpnInfo> getAlwaysOnAppVpnInfos() { 500 Set<AppVpnInfo> result = new ArraySet<>(); 501 for (UserHandle profile : mUserManager.getUserProfiles()) { 502 final int profileId = profile.getIdentifier(); 503 final String packageName = mConnectivityManager.getAlwaysOnVpnPackageForUser(profileId); 504 if (packageName != null) { 505 result.add(new AppVpnInfo(profileId, packageName)); 506 } 507 } 508 return result; 509 } 510 getVpnApps(Context context, boolean includeProfiles)511 static List<AppVpnInfo> getVpnApps(Context context, boolean includeProfiles) { 512 List<AppVpnInfo> result = Lists.newArrayList(); 513 514 final Set<Integer> profileIds; 515 if (includeProfiles) { 516 profileIds = new ArraySet<>(); 517 for (UserHandle profile : UserManager.get(context).getUserProfiles()) { 518 profileIds.add(profile.getIdentifier()); 519 } 520 } else { 521 profileIds = Collections.singleton(UserHandle.myUserId()); 522 } 523 524 // Fetch VPN-enabled apps from AppOps. 525 AppOpsManager aom = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); 526 List<AppOpsManager.PackageOps> apps = 527 aom.getPackagesForOps(new int[] {OP_ACTIVATE_VPN, OP_ACTIVATE_PLATFORM_VPN}); 528 if (apps != null) { 529 for (AppOpsManager.PackageOps pkg : apps) { 530 int userId = UserHandle.getUserId(pkg.getUid()); 531 if (!profileIds.contains(userId)) { 532 // Skip packages for users outside of our profile group. 533 continue; 534 } 535 // Look for a MODE_ALLOWED permission to activate VPN. 536 boolean allowed = false; 537 for (AppOpsManager.OpEntry op : pkg.getOps()) { 538 if ((op.getOp() == OP_ACTIVATE_VPN || op.getOp() == OP_ACTIVATE_PLATFORM_VPN) 539 && op.getMode() == AppOpsManager.MODE_ALLOWED) { 540 allowed = true; 541 } 542 } 543 if (allowed) { 544 result.add(new AppVpnInfo(userId, pkg.getPackageName())); 545 } 546 } 547 } 548 549 Collections.sort(result); 550 return result; 551 } 552 loadVpnProfiles(KeyStore keyStore, int... excludeTypes)553 static List<VpnProfile> loadVpnProfiles(KeyStore keyStore, int... excludeTypes) { 554 final ArrayList<VpnProfile> result = Lists.newArrayList(); 555 556 for (String key : keyStore.list(Credentials.VPN)) { 557 final VpnProfile profile = VpnProfile.decode(key, keyStore.get(Credentials.VPN + key)); 558 if (profile != null && !ArrayUtils.contains(excludeTypes, profile.type)) { 559 result.add(profile); 560 } 561 } 562 return result; 563 } 564 } 565