Merge "Add an API that allows VPNs to declare themselves as metered." am: 62a9b66a38
am: 9563c15d09
Change-Id: Iab1d2a3bf27ec9c033253ccd48ce8026e3b18799
This commit is contained in:
@@ -27852,6 +27852,7 @@ package android.net {
|
||||
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[]);
|
||||
|
||||
@@ -790,6 +790,27 @@ public class VpnService extends Service {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
@@ -104,6 +104,7 @@ public class VpnConfig implements Parcelable {
|
||||
public boolean allowBypass;
|
||||
public boolean allowIPv4;
|
||||
public boolean allowIPv6;
|
||||
public boolean isMetered = true;
|
||||
public Network[] underlyingNetworks;
|
||||
public ProxyInfo proxyInfo;
|
||||
|
||||
@@ -165,6 +166,7 @@ public class VpnConfig implements Parcelable {
|
||||
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 @@ public class VpnConfig implements Parcelable {
|
||||
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;
|
||||
|
||||
@@ -165,6 +165,7 @@ public class Vpn {
|
||||
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 @@ public class Vpn {
|
||||
|
||||
mPackage = VpnConfig.LEGACY_VPN;
|
||||
mOwnerUID = getAppUid(mPackage, mUserHandle);
|
||||
mIsPackageTargetingAtLeastQ = doesPackageTargetAtLeastQ(mPackage);
|
||||
|
||||
try {
|
||||
netService.registerObserver(mObserver);
|
||||
@@ -267,8 +269,11 @@ public class Vpn {
|
||||
|
||||
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 @@ public class Vpn {
|
||||
|
||||
@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 @@ public class Vpn {
|
||||
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 @@ public class Vpn {
|
||||
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 @@ public class Vpn {
|
||||
// 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 @@ public class Vpn {
|
||||
config.user = profile.key;
|
||||
config.interfaze = iface;
|
||||
config.session = profile.name;
|
||||
config.isMetered = false;
|
||||
|
||||
config.addLegacyRoutes(profile.routes);
|
||||
if (!profile.dnsServers.isEmpty()) {
|
||||
|
||||
@@ -906,6 +906,7 @@ public class ConnectivityServiceTest {
|
||||
mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
|
||||
mConnected = true;
|
||||
mConfig = new VpnConfig();
|
||||
mConfig.isMetered = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -168,6 +168,8 @@ public class VpnTest {
|
||||
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 @@ public class VpnTest {
|
||||
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 @@ public class VpnTest {
|
||||
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 @@ public class VpnTest {
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user