Add an API that allows VPNs to declare themselves as metered.
For VPN apps targeting Q and above, they will by default be treated as
metered unless they override this setting before establishing VPN.
Bug: 120145746
Test: atest FrameworksNetTests
Test: On device tests verifying meteredness setup correctly for apps
targeting Q and apps targeting P.
Change-Id: Ia6d1f7ef244bc04ae2e28faa59625302b5994875
diff --git a/api/current.txt b/api/current.txt
index bb40056..e248d71 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -27851,6 +27851,7 @@
method public android.net.VpnService.Builder setBlocking(boolean);
method public android.net.VpnService.Builder setConfigureIntent(android.app.PendingIntent);
method public android.net.VpnService.Builder setHttpProxy(android.net.ProxyInfo);
+ method public android.net.VpnService.Builder setMetered(boolean);
method public android.net.VpnService.Builder setMtu(int);
method public android.net.VpnService.Builder setSession(String);
method public android.net.VpnService.Builder setUnderlyingNetworks(android.net.Network[]);
diff --git a/core/java/android/net/VpnService.java b/core/java/android/net/VpnService.java
index dc099a4..784f233 100644
--- a/core/java/android/net/VpnService.java
+++ b/core/java/android/net/VpnService.java
@@ -791,6 +791,27 @@
}
/**
+ * Marks the VPN network as metered. A VPN network is classified as metered when the user is
+ * sensitive to heavy data usage due to monetary costs and/or data limitations. In such
+ * cases, you should set this to {@code true} so that apps on the system can avoid doing
+ * large data transfers. Otherwise, set this to {@code false}. Doing so would cause VPN
+ * network to inherit its meteredness from its underlying networks.
+ *
+ * <p>VPN apps targeting {@link android.os.Build.VERSION_CODES#Q} or above will be
+ * considered metered by default.
+ *
+ * @param isMetered {@code true} if VPN network should be treated as metered regardless of
+ * underlying network meteredness
+ * @return this {@link Builder} object to facilitate chaining method calls
+ * @see #setUnderlyingNetworks(Networks[])
+ * @see ConnectivityManager#isActiveNetworkMetered()
+ */
+ public Builder setMetered(boolean isMetered) {
+ mConfig.isMetered = isMetered;
+ return this;
+ }
+
+ /**
* Create a VPN interface using the parameters supplied to this
* builder. The interface works on IP packets, and a file descriptor
* is returned for the application to access them. Each read
diff --git a/core/java/com/android/internal/net/VpnConfig.java b/core/java/com/android/internal/net/VpnConfig.java
index da8605e..65b974b 100644
--- a/core/java/com/android/internal/net/VpnConfig.java
+++ b/core/java/com/android/internal/net/VpnConfig.java
@@ -104,6 +104,7 @@
public boolean allowBypass;
public boolean allowIPv4;
public boolean allowIPv6;
+ public boolean isMetered = true;
public Network[] underlyingNetworks;
public ProxyInfo proxyInfo;
@@ -165,6 +166,7 @@
out.writeInt(allowBypass ? 1 : 0);
out.writeInt(allowIPv4 ? 1 : 0);
out.writeInt(allowIPv6 ? 1 : 0);
+ out.writeInt(isMetered ? 1 : 0);
out.writeTypedArray(underlyingNetworks, flags);
out.writeParcelable(proxyInfo, flags);
}
@@ -191,6 +193,7 @@
config.allowBypass = in.readInt() != 0;
config.allowIPv4 = in.readInt() != 0;
config.allowIPv6 = in.readInt() != 0;
+ config.isMetered = in.readInt() != 0;
config.underlyingNetworks = in.createTypedArray(Network.CREATOR);
config.proxyInfo = in.readParcelable(null);
return config;
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 2508844..9141ccb 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -165,6 +165,7 @@
private final NetworkInfo mNetworkInfo;
private String mPackage;
private int mOwnerUID;
+ private boolean mIsPackageTargetingAtLeastQ;
private String mInterface;
private Connection mConnection;
private LegacyVpnRunner mLegacyVpnRunner;
@@ -226,6 +227,7 @@
mPackage = VpnConfig.LEGACY_VPN;
mOwnerUID = getAppUid(mPackage, mUserHandle);
+ mIsPackageTargetingAtLeastQ = doesPackageTargetAtLeastQ(mPackage);
try {
netService.registerObserver(mObserver);
@@ -267,8 +269,11 @@
public void updateCapabilities() {
final Network[] underlyingNetworks = (mConfig != null) ? mConfig.underlyingNetworks : null;
+ // Only apps targeting Q and above can explicitly declare themselves as metered.
+ final boolean isAlwaysMetered =
+ mIsPackageTargetingAtLeastQ && (mConfig == null || mConfig.isMetered);
updateCapabilities(mContext.getSystemService(ConnectivityManager.class), underlyingNetworks,
- mNetworkCapabilities);
+ mNetworkCapabilities, isAlwaysMetered);
if (mNetworkAgent != null) {
mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);
@@ -277,11 +282,13 @@
@VisibleForTesting
public static void updateCapabilities(ConnectivityManager cm, Network[] underlyingNetworks,
- NetworkCapabilities caps) {
+ NetworkCapabilities caps, boolean isAlwaysMetered) {
int[] transportTypes = new int[] { NetworkCapabilities.TRANSPORT_VPN };
int downKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
int upKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
- boolean metered = false;
+ // VPN's meteredness is OR'd with isAlwaysMetered and meteredness of its underlying
+ // networks.
+ boolean metered = isAlwaysMetered;
boolean roaming = false;
boolean congested = false;
@@ -724,6 +731,7 @@
Log.i(TAG, "Switched from " + mPackage + " to " + newPackage);
mPackage = newPackage;
mOwnerUID = getAppUid(newPackage, mUserHandle);
+ mIsPackageTargetingAtLeastQ = doesPackageTargetAtLeastQ(newPackage);
try {
mNetd.allowProtect(mOwnerUID);
} catch (Exception e) {
@@ -789,6 +797,21 @@
return result;
}
+ private boolean doesPackageTargetAtLeastQ(String packageName) {
+ if (VpnConfig.LEGACY_VPN.equals(packageName)) {
+ return true;
+ }
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo appInfo =
+ pm.getApplicationInfoAsUser(packageName, 0 /*flags*/, mUserHandle);
+ return appInfo.targetSdkVersion >= VERSION_CODES.Q;
+ } catch (NameNotFoundException unused) {
+ Log.w(TAG, "Can't find \"" + packageName + "\"");
+ return false;
+ }
+ }
+
public NetworkInfo getNetworkInfo() {
return mNetworkInfo;
}
@@ -1076,6 +1099,8 @@
// as rules are deleted. This prevents data leakage as the rules are moved over.
agentDisconnect(oldNetworkAgent);
}
+ // Set up VPN's capabilities such as meteredness.
+ updateCapabilities();
if (oldConnection != null) {
mContext.unbindService(oldConnection);
@@ -1776,6 +1801,7 @@
config.user = profile.key;
config.interfaze = iface;
config.session = profile.name;
+ config.isMetered = false;
config.addLegacyRoutes(profile.routes);
if (!profile.dnsServers.isEmpty()) {
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 923c7dd..67fe8e55 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -903,6 +903,7 @@
mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
mConnected = true;
mConfig = new VpnConfig();
+ mConfig.isMetered = false;
}
@Override
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index 5b17224..46de3d0 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -168,6 +168,8 @@
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.targetSdkVersion = VERSION_CODES.CUR_DEVELOPMENT;
when(mContext.getApplicationInfo()).thenReturn(applicationInfo);
+ when(mPackageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt()))
+ .thenReturn(applicationInfo);
doNothing().when(mNetService).registerObserver(any());
}
@@ -544,23 +546,28 @@
final Network wifi = new Network(2);
final Map<Network, NetworkCapabilities> networks = new HashMap<>();
- networks.put(mobile, new NetworkCapabilities()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_INTERNET)
- .addCapability(NET_CAPABILITY_NOT_METERED)
- .addCapability(NET_CAPABILITY_NOT_CONGESTED)
- .setLinkDownstreamBandwidthKbps(10));
- networks.put(wifi, new NetworkCapabilities()
- .addTransportType(TRANSPORT_WIFI)
- .addCapability(NET_CAPABILITY_INTERNET)
- .addCapability(NET_CAPABILITY_NOT_ROAMING)
- .addCapability(NET_CAPABILITY_NOT_CONGESTED)
- .setLinkUpstreamBandwidthKbps(20));
+ networks.put(
+ mobile,
+ new NetworkCapabilities()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .setLinkDownstreamBandwidthKbps(10));
+ networks.put(
+ wifi,
+ new NetworkCapabilities()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addCapability(NET_CAPABILITY_NOT_METERED)
+ .addCapability(NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NET_CAPABILITY_NOT_CONGESTED)
+ .setLinkUpstreamBandwidthKbps(20));
setMockedNetworks(networks);
final NetworkCapabilities caps = new NetworkCapabilities();
- Vpn.updateCapabilities(mConnectivityManager, new Network[] { }, caps);
+ Vpn.updateCapabilities(
+ mConnectivityManager, new Network[] {}, caps, false /* isAlwaysMetered */);
assertTrue(caps.hasTransport(TRANSPORT_VPN));
assertFalse(caps.hasTransport(TRANSPORT_CELLULAR));
assertFalse(caps.hasTransport(TRANSPORT_WIFI));
@@ -570,17 +577,33 @@
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_ROAMING));
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
- Vpn.updateCapabilities(mConnectivityManager, new Network[] { mobile }, caps);
+ Vpn.updateCapabilities(
+ mConnectivityManager,
+ new Network[] {mobile},
+ caps,
+ false /* isAlwaysMetered */);
assertTrue(caps.hasTransport(TRANSPORT_VPN));
assertTrue(caps.hasTransport(TRANSPORT_CELLULAR));
assertFalse(caps.hasTransport(TRANSPORT_WIFI));
assertEquals(10, caps.getLinkDownstreamBandwidthKbps());
assertEquals(LINK_BANDWIDTH_UNSPECIFIED, caps.getLinkUpstreamBandwidthKbps());
- assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+ assertFalse(caps.hasCapability(NET_CAPABILITY_NOT_METERED));
assertFalse(caps.hasCapability(NET_CAPABILITY_NOT_ROAMING));
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
- Vpn.updateCapabilities(mConnectivityManager, new Network[] { wifi }, caps);
+ Vpn.updateCapabilities(
+ mConnectivityManager, new Network[] {wifi}, caps, false /* isAlwaysMetered */);
+ assertTrue(caps.hasTransport(TRANSPORT_VPN));
+ assertFalse(caps.hasTransport(TRANSPORT_CELLULAR));
+ assertTrue(caps.hasTransport(TRANSPORT_WIFI));
+ assertEquals(LINK_BANDWIDTH_UNSPECIFIED, caps.getLinkDownstreamBandwidthKbps());
+ assertEquals(20, caps.getLinkUpstreamBandwidthKbps());
+ assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_METERED));
+ assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_ROAMING));
+ assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
+
+ Vpn.updateCapabilities(
+ mConnectivityManager, new Network[] {wifi}, caps, true /* isAlwaysMetered */);
assertTrue(caps.hasTransport(TRANSPORT_VPN));
assertFalse(caps.hasTransport(TRANSPORT_CELLULAR));
assertTrue(caps.hasTransport(TRANSPORT_WIFI));
@@ -590,7 +613,11 @@
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_ROAMING));
assertTrue(caps.hasCapability(NET_CAPABILITY_NOT_CONGESTED));
- Vpn.updateCapabilities(mConnectivityManager, new Network[] { mobile, wifi }, caps);
+ Vpn.updateCapabilities(
+ mConnectivityManager,
+ new Network[] {mobile, wifi},
+ caps,
+ false /* isAlwaysMetered */);
assertTrue(caps.hasTransport(TRANSPORT_VPN));
assertTrue(caps.hasTransport(TRANSPORT_CELLULAR));
assertTrue(caps.hasTransport(TRANSPORT_WIFI));