Loading core/java/android/net/ITetheringStatsProvider.aidl +10 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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; } services/core/java/com/android/server/NetworkManagementService.java +27 −0 Original line number Diff line number Diff line Loading @@ -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); } } } } } Loading @@ -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); } } } } } Loading Loading @@ -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 Loading services/core/java/com/android/server/connectivity/tethering/OffloadController.java +65 −29 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; Loading @@ -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; Loading Loading @@ -177,10 +182,10 @@ public class OffloadController { @Override public NetworkStats getTetherStats() { NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); CountDownLatch latch = new CountDownLatch(1); mHandler.post(() -> { try { // 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; Loading @@ -194,18 +199,20 @@ public class OffloadController { entry.txBytes = mForwardedStats.get(iface).txBytes; stats.addValues(entry); } } finally { latch.countDown(); } }); }, 0); 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"); return stats; } 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); }); } } Loading @@ -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()); Loading Loading @@ -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() { Loading services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java +21 −0 Original line number Diff line number Diff line Loading @@ -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; Loading tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java +72 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -110,6 +112,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")); Loading Loading @@ -417,4 +425,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(); } } Loading
core/java/android/net/ITetheringStatsProvider.aidl +10 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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; }
services/core/java/com/android/server/NetworkManagementService.java +27 −0 Original line number Diff line number Diff line Loading @@ -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); } } } } } Loading @@ -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); } } } } } Loading Loading @@ -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 Loading
services/core/java/com/android/server/connectivity/tethering/OffloadController.java +65 −29 Original line number Diff line number Diff line Loading @@ -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; /** Loading @@ -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; Loading @@ -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; Loading Loading @@ -177,10 +182,10 @@ public class OffloadController { @Override public NetworkStats getTetherStats() { NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0); CountDownLatch latch = new CountDownLatch(1); mHandler.post(() -> { try { // 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; Loading @@ -194,18 +199,20 @@ public class OffloadController { entry.txBytes = mForwardedStats.get(iface).txBytes; stats.addValues(entry); } } finally { latch.countDown(); } }); }, 0); 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"); return stats; } 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); }); } } Loading @@ -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()); Loading Loading @@ -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() { Loading
services/core/java/com/android/server/connectivity/tethering/OffloadHardwareInterface.java +21 −0 Original line number Diff line number Diff line Loading @@ -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; Loading
tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java +72 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -110,6 +112,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")); Loading Loading @@ -417,4 +425,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(); } }