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
* @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;
}
} 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);
+ }
+ }
+ }
}
}
} 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);
+ }
+ }
+ }
}
}
}
return stats;
}
+
+ @Override
+ public void setInterfaceQuota(String iface, long quotaBytes) {
+ // Do nothing. netd is already informed of quota changes in setInterfaceQuota.
+ }
}
@Override
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;
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) {
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());
}
}
- 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() {
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;
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;
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;
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"));
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();
+ }
}