Merge changes I82d3bee0,I9c9413d7
am: deb4eb5d05
Change-Id: Iaa6aecf3ec3a91dc04f4533c0ea5b649bdfeb83d
This commit is contained in:
@@ -19,7 +19,7 @@ package android.net;
|
||||
import android.net.NetworkStats;
|
||||
|
||||
/**
|
||||
* Interface that allows NetworkManagementService to query for tethering statistics.
|
||||
* Interface for NetworkManagementService to query tethering statistics and set data limits.
|
||||
*
|
||||
* TODO: this does not really need to be an interface since Tethering runs in the same process
|
||||
* as NetworkManagementService. Consider refactoring Tethering to use direct access to
|
||||
@@ -29,5 +29,14 @@ import android.net.NetworkStats;
|
||||
* @hide
|
||||
*/
|
||||
interface ITetheringStatsProvider {
|
||||
// Returns cumulative statistics for all tethering sessions since boot, on all upstreams.
|
||||
NetworkStats getTetherStats();
|
||||
|
||||
// Sets the interface quota for the specified upstream interface. This is defined as the number
|
||||
// of bytes, starting from zero and counting from now, after which data should stop being
|
||||
// forwarded to/from the specified upstream. A value of QUOTA_UNLIMITED means there is no limit.
|
||||
void setInterfaceQuota(String iface, long quotaBytes);
|
||||
|
||||
// Indicates that no data usage limit is set.
|
||||
const int QUOTA_UNLIMITED = -1;
|
||||
}
|
||||
|
||||
@@ -1567,6 +1567,17 @@ public class NetworkManagementService extends INetworkManagementService.Stub
|
||||
} catch (NativeDaemonConnectorException e) {
|
||||
throw e.rethrowAsParcelableException();
|
||||
}
|
||||
|
||||
synchronized (mTetheringStatsProviders) {
|
||||
for (ITetheringStatsProvider provider : mTetheringStatsProviders.keySet()) {
|
||||
try {
|
||||
provider.setInterfaceQuota(iface, quotaBytes);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Problem setting tethering data limit on provider " +
|
||||
mTetheringStatsProviders.get(provider) + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1593,6 +1604,17 @@ public class NetworkManagementService extends INetworkManagementService.Stub
|
||||
} catch (NativeDaemonConnectorException e) {
|
||||
throw e.rethrowAsParcelableException();
|
||||
}
|
||||
|
||||
synchronized (mTetheringStatsProviders) {
|
||||
for (ITetheringStatsProvider provider : mTetheringStatsProviders.keySet()) {
|
||||
try {
|
||||
provider.setInterfaceQuota(iface, ITetheringStatsProvider.QUOTA_UNLIMITED);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Problem removing tethering data limit on provider " +
|
||||
mTetheringStatsProviders.get(provider) + ": " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1864,6 +1886,11 @@ public class NetworkManagementService extends INetworkManagementService.Stub
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInterfaceQuota(String iface, long quotaBytes) {
|
||||
// Do nothing. netd is already informed of quota changes in setInterfaceQuota.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -46,7 +46,6 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
@@ -58,8 +57,6 @@ import java.util.concurrent.TimeUnit;
|
||||
public class OffloadController {
|
||||
private static final String TAG = OffloadController.class.getSimpleName();
|
||||
|
||||
private static final int STATS_FETCH_TIMEOUT_MS = 1000;
|
||||
|
||||
private final Handler mHandler;
|
||||
private final OffloadHardwareInterface mHwInterface;
|
||||
private final ContentResolver mContentResolver;
|
||||
@@ -76,9 +73,17 @@ public class OffloadController {
|
||||
private Set<String> mLastLocalPrefixStrs;
|
||||
|
||||
// Maps upstream interface names to offloaded traffic statistics.
|
||||
// Always contains the latest value received from the hardware for each interface, regardless of
|
||||
// whether offload is currently running on that interface.
|
||||
private HashMap<String, OffloadHardwareInterface.ForwardedStats>
|
||||
mForwardedStats = new HashMap<>();
|
||||
|
||||
// Maps upstream interface names to interface quotas.
|
||||
// Always contains the latest value received from the framework for each interface, regardless
|
||||
// of whether offload is currently running (or is even supported) on that interface. Only
|
||||
// includes upstream interfaces that have a quota set.
|
||||
private HashMap<String, Long> mInterfaceQuotas = new HashMap<>();
|
||||
|
||||
public OffloadController(Handler h, OffloadHardwareInterface hwi,
|
||||
ContentResolver contentResolver, INetworkManagementService nms, SharedLog log) {
|
||||
mHandler = h;
|
||||
@@ -177,36 +182,38 @@ public class OffloadController {
|
||||
@Override
|
||||
public NetworkStats getTetherStats() {
|
||||
NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0);
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
mHandler.post(() -> {
|
||||
try {
|
||||
NetworkStats.Entry entry = new NetworkStats.Entry();
|
||||
entry.set = SET_DEFAULT;
|
||||
entry.tag = TAG_NONE;
|
||||
entry.uid = UID_TETHERING;
|
||||
// We can't just post to mHandler because we are mostly (but not always) called by
|
||||
// NetworkStatsService#performPollLocked, which is (currently) on the same thread as us.
|
||||
mHandler.runWithScissors(() -> {
|
||||
NetworkStats.Entry entry = new NetworkStats.Entry();
|
||||
entry.set = SET_DEFAULT;
|
||||
entry.tag = TAG_NONE;
|
||||
entry.uid = UID_TETHERING;
|
||||
|
||||
updateStatsForCurrentUpstream();
|
||||
updateStatsForCurrentUpstream();
|
||||
|
||||
for (String iface : mForwardedStats.keySet()) {
|
||||
entry.iface = iface;
|
||||
entry.rxBytes = mForwardedStats.get(iface).rxBytes;
|
||||
entry.txBytes = mForwardedStats.get(iface).txBytes;
|
||||
stats.addValues(entry);
|
||||
}
|
||||
} finally {
|
||||
latch.countDown();
|
||||
for (String iface : mForwardedStats.keySet()) {
|
||||
entry.iface = iface;
|
||||
entry.rxBytes = mForwardedStats.get(iface).rxBytes;
|
||||
entry.txBytes = mForwardedStats.get(iface).txBytes;
|
||||
stats.addValues(entry);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await(STATS_FETCH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
mLog.e("Tethering stats fetch timed out after " + STATS_FETCH_TIMEOUT_MS + "ms");
|
||||
}
|
||||
}, 0);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public void setInterfaceQuota(String iface, long quotaBytes) {
|
||||
mHandler.post(() -> {
|
||||
if (quotaBytes == ITetheringStatsProvider.QUOTA_UNLIMITED) {
|
||||
mInterfaceQuotas.remove(iface);
|
||||
} else {
|
||||
mInterfaceQuotas.put(iface, quotaBytes);
|
||||
}
|
||||
maybeUpdateDataLimit(iface);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateStats(String iface) {
|
||||
@@ -220,6 +227,22 @@ public class OffloadController {
|
||||
mForwardedStats.get(iface).add(mHwInterface.getForwardedStats(iface));
|
||||
}
|
||||
|
||||
private boolean maybeUpdateDataLimit(String iface) {
|
||||
// setDataLimit may only be called while offload is occuring on this upstream.
|
||||
if (!started() ||
|
||||
mUpstreamLinkProperties == null ||
|
||||
!TextUtils.equals(iface, mUpstreamLinkProperties.getInterfaceName())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
Long limit = mInterfaceQuotas.get(iface);
|
||||
if (limit == null) {
|
||||
limit = Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
return mHwInterface.setDataLimit(iface, limit);
|
||||
}
|
||||
|
||||
private void updateStatsForCurrentUpstream() {
|
||||
if (mUpstreamLinkProperties != null) {
|
||||
maybeUpdateStats(mUpstreamLinkProperties.getInterfaceName());
|
||||
@@ -309,8 +332,21 @@ public class OffloadController {
|
||||
}
|
||||
}
|
||||
|
||||
return mHwInterface.setUpstreamParameters(
|
||||
boolean success = mHwInterface.setUpstreamParameters(
|
||||
iface, v4addr, v4gateway, (v6gateways.isEmpty() ? null : v6gateways));
|
||||
|
||||
if (!success) {
|
||||
return success;
|
||||
}
|
||||
|
||||
// Data limits can only be set once offload is running on the upstream.
|
||||
success = maybeUpdateDataLimit(iface);
|
||||
if (!success) {
|
||||
mLog.log("Setting data limit for " + iface + " failed, disabling offload.");
|
||||
stop();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private boolean computeAndPushLocalPrefixes() {
|
||||
|
||||
@@ -188,6 +188,27 @@ public class OffloadHardwareInterface {
|
||||
return results.success;
|
||||
}
|
||||
|
||||
public boolean setDataLimit(String iface, long limit) {
|
||||
|
||||
final String logmsg = String.format("setDataLimit(%s, %d)", iface, limit);
|
||||
|
||||
final CbResults results = new CbResults();
|
||||
try {
|
||||
mOffloadControl.setDataLimit(
|
||||
iface, limit,
|
||||
(boolean success, String errMsg) -> {
|
||||
results.success = success;
|
||||
results.errMsg = errMsg;
|
||||
});
|
||||
} catch (RemoteException e) {
|
||||
record(logmsg, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
record(logmsg, results);
|
||||
return results.success;
|
||||
}
|
||||
|
||||
public boolean setUpstreamParameters(
|
||||
String iface, String v4addr, String v4gateway, ArrayList<String> v6gws) {
|
||||
iface = (iface != null) ? iface : NO_INTERFACE_NAME;
|
||||
|
||||
@@ -25,6 +25,7 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyLong;
|
||||
import static org.mockito.Matchers.anyObject;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
@@ -45,6 +46,7 @@ import android.net.LinkProperties;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.RouteInfo;
|
||||
import android.net.util.SharedLog;
|
||||
import android.os.ConditionVariable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.INetworkManagementService;
|
||||
@@ -112,6 +114,12 @@ public class OffloadControllerTest {
|
||||
Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
|
||||
}
|
||||
|
||||
private void waitForIdle() {
|
||||
ConditionVariable cv = new ConditionVariable();
|
||||
new Handler(Looper.getMainLooper()).post(() -> { cv.open(); });
|
||||
cv.block();
|
||||
}
|
||||
|
||||
private OffloadController makeOffloadController() throws Exception {
|
||||
OffloadController offload = new OffloadController(new Handler(Looper.getMainLooper()),
|
||||
mHardware, mContentResolver, mNMService, new SharedLog("test"));
|
||||
@@ -421,4 +429,68 @@ public class OffloadControllerTest {
|
||||
entry = stats.getValues(ethernetPosition, entry);
|
||||
assertNetworkStats(ethernetIface, ethernetStats, entry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetInterfaceQuota() throws Exception {
|
||||
setupFunctioningHardwareInterface();
|
||||
enableOffload();
|
||||
|
||||
final OffloadController offload = makeOffloadController();
|
||||
offload.start();
|
||||
|
||||
final String ethernetIface = "eth1";
|
||||
final String mobileIface = "rmnet_data0";
|
||||
final long ethernetLimit = 12345;
|
||||
final long mobileLimit = 12345678;
|
||||
|
||||
final LinkProperties lp = new LinkProperties();
|
||||
lp.setInterfaceName(ethernetIface);
|
||||
offload.setUpstreamLinkProperties(lp);
|
||||
|
||||
ITetheringStatsProvider provider = mTetherStatsProviderCaptor.getValue();
|
||||
final InOrder inOrder = inOrder(mHardware);
|
||||
when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true);
|
||||
when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
|
||||
|
||||
// Applying an interface quota to the current upstream immediately sends it to the hardware.
|
||||
provider.setInterfaceQuota(ethernetIface, ethernetLimit);
|
||||
waitForIdle();
|
||||
inOrder.verify(mHardware).setDataLimit(ethernetIface, ethernetLimit);
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
|
||||
// Applying an interface quota to another upstream does not take any immediate action.
|
||||
provider.setInterfaceQuota(mobileIface, mobileLimit);
|
||||
waitForIdle();
|
||||
inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
|
||||
|
||||
// Switching to that upstream causes the quota to be applied if the parameters were applied
|
||||
// correctly.
|
||||
lp.setInterfaceName(mobileIface);
|
||||
offload.setUpstreamLinkProperties(lp);
|
||||
waitForIdle();
|
||||
inOrder.verify(mHardware).setDataLimit(mobileIface, mobileLimit);
|
||||
|
||||
// Setting a limit of ITetheringStatsProvider.QUOTA_UNLIMITED causes the limit to be set
|
||||
// to Long.MAX_VALUE.
|
||||
provider.setInterfaceQuota(mobileIface, ITetheringStatsProvider.QUOTA_UNLIMITED);
|
||||
waitForIdle();
|
||||
inOrder.verify(mHardware).setDataLimit(mobileIface, Long.MAX_VALUE);
|
||||
|
||||
// If setting upstream parameters fails, then the data limit is not set.
|
||||
when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(false);
|
||||
lp.setInterfaceName(ethernetIface);
|
||||
offload.setUpstreamLinkProperties(lp);
|
||||
provider.setInterfaceQuota(mobileIface, mobileLimit);
|
||||
waitForIdle();
|
||||
inOrder.verify(mHardware, never()).setDataLimit(anyString(), anyLong());
|
||||
|
||||
// If setting the data limit fails while changing upstreams, offload is stopped.
|
||||
when(mHardware.setUpstreamParameters(any(), any(), any(), any())).thenReturn(true);
|
||||
when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(false);
|
||||
lp.setInterfaceName(mobileIface);
|
||||
offload.setUpstreamLinkProperties(lp);
|
||||
provider.setInterfaceQuota(mobileIface, mobileLimit);
|
||||
waitForIdle();
|
||||
inOrder.verify(mHardware).stopOffloadControl();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user