package com.android.server.connectivity.tethering;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.TrafficStats.UID_TETHERING;
import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
import android.content.ContentResolver;
+import android.net.ITetheringStatsProvider;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
+import android.net.NetworkStats;
import android.net.RouteInfo;
import android.net.util.SharedLog;
import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.RemoteException;
+import android.os.SystemClock;
import android.provider.Settings;
+import android.text.TextUtils;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
+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;
/**
* A class to encapsulate the business logic of programming the tethering
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;
// prefixes representing only the locally-assigned IP addresses.
private Set<String> mLastLocalPrefixStrs;
+ // Maps upstream interface names to offloaded traffic statistics.
+ private HashMap<String, OffloadHardwareInterface.ForwardedStats>
+ mForwardedStats = new HashMap<>();
+
public OffloadController(Handler h, OffloadHardwareInterface hwi,
- ContentResolver contentResolver, SharedLog log) {
+ ContentResolver contentResolver, INetworkManagementService nms, SharedLog log) {
mHandler = h;
mHwInterface = hwi;
mContentResolver = contentResolver;
mLog = log.forSubComponent(TAG);
mExemptPrefixes = new HashSet<>();
mLastLocalPrefixStrs = new HashSet<>();
+
+ try {
+ nms.registerTetheringStatsProvider(
+ new OffloadTetheringStatsProvider(), getClass().getSimpleName());
+ } catch (RemoteException e) {
+ mLog.e("Cannot register offload stats provider: " + e);
+ }
}
public void start() {
public void stop() {
final boolean wasStarted = started();
+ updateStatsForCurrentUpstream();
mUpstreamLinkProperties = null;
mHwInterface.stopOffloadControl();
mControlInitialized = false;
if (wasStarted) mLog.log("tethering offload stopped");
}
+ private class OffloadTetheringStatsProvider extends ITetheringStatsProvider.Stub {
+ @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;
+
+ 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();
+ }
+ });
+
+ 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;
+ }
+ }
+
+ private void maybeUpdateStats(String iface) {
+ if (TextUtils.isEmpty(iface)) {
+ return;
+ }
+
+ if (!mForwardedStats.containsKey(iface)) {
+ mForwardedStats.put(iface, new OffloadHardwareInterface.ForwardedStats());
+ }
+ mForwardedStats.get(iface).add(mHwInterface.getForwardedStats(iface));
+ }
+
+ private void updateStatsForCurrentUpstream() {
+ if (mUpstreamLinkProperties != null) {
+ maybeUpdateStats(mUpstreamLinkProperties.getInterfaceName());
+ }
+ }
+
public void setUpstreamLinkProperties(LinkProperties lp) {
if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return;
+ String prevUpstream = (mUpstreamLinkProperties != null) ?
+ mUpstreamLinkProperties.getInterfaceName() : null;
+
mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null;
+
// TODO: examine return code and decide what to do if programming
// upstream parameters fails (probably just wait for a subsequent
// onOffloadEvent() callback to tell us offload is available again and
// then reapply all state).
computeAndPushLocalPrefixes();
pushUpstreamParameters();
+
+ // Update stats after we've told the hardware to change routing so we don't miss packets.
+ maybeUpdateStats(prevUpstream);
}
public void setLocalPrefixes(Set<IpPrefix> localPrefixes) {
package com.android.server.connectivity.tethering;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.TrafficStats.UID_TETHERING;
import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
+import static com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats;
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.anyObject;
+import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.content.pm.ApplicationInfo;
+import android.net.ITetheringStatsProvider;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
+import android.net.NetworkStats;
import android.net.RouteInfo;
import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.INetworkManagementService;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
@Mock private OffloadHardwareInterface mHardware;
@Mock private ApplicationInfo mApplicationInfo;
@Mock private Context mContext;
+ @Mock private INetworkManagementService mNMService;
private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
ArgumentCaptor.forClass(ArrayList.class);
+ private final ArgumentCaptor<ITetheringStatsProvider.Stub> mTetherStatsProviderCaptor =
+ ArgumentCaptor.forClass(ITetheringStatsProvider.Stub.class);
private MockContentResolver mContentResolver;
- @Before public void setUp() throws Exception {
+ @Before public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo);
when(mContext.getPackageName()).thenReturn("OffloadControllerTest");
when(mHardware.initOffloadConfig()).thenReturn(true);
when(mHardware.initOffloadControl(any(OffloadHardwareInterface.ControlCallback.class)))
.thenReturn(true);
+ when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats());
}
private void enableOffload() {
Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
}
+ private OffloadController makeOffloadController() throws Exception {
+ OffloadController offload = new OffloadController(new Handler(Looper.getMainLooper()),
+ mHardware, mContentResolver, mNMService, new SharedLog("test"));
+ verify(mNMService).registerTetheringStatsProvider(
+ mTetherStatsProviderCaptor.capture(), anyString());
+ return offload;
+ }
+
// TODO: Restore when FakeSettingsProvider.clearSettingsProvider() is available.
// @Test
- public void testNoSettingsValueDefaultDisabledDoesNotStart() {
+ public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception {
setupFunctioningHardwareInterface();
when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1);
try {
fail();
} catch (SettingNotFoundException expected) {}
- final OffloadController offload =
- new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+ final OffloadController offload = makeOffloadController();
offload.start();
final InOrder inOrder = inOrder(mHardware);
// TODO: Restore when FakeSettingsProvider.clearSettingsProvider() is available.
// @Test
- public void testNoSettingsValueDefaultEnabledDoesStart() {
+ public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception {
setupFunctioningHardwareInterface();
when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0);
try {
fail();
} catch (SettingNotFoundException expected) {}
- final OffloadController offload =
- new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+ final OffloadController offload = makeOffloadController();
offload.start();
final InOrder inOrder = inOrder(mHardware);
}
@Test
- public void testSettingsAllowsStart() {
+ public void testSettingsAllowsStart() throws Exception {
setupFunctioningHardwareInterface();
Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
- final OffloadController offload =
- new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+ final OffloadController offload = makeOffloadController();
offload.start();
final InOrder inOrder = inOrder(mHardware);
}
@Test
- public void testSettingsDisablesStart() {
+ public void testSettingsDisablesStart() throws Exception {
setupFunctioningHardwareInterface();
Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1);
- final OffloadController offload =
- new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+ final OffloadController offload = makeOffloadController();
offload.start();
final InOrder inOrder = inOrder(mHardware);
setupFunctioningHardwareInterface();
enableOffload();
- final OffloadController offload =
- new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+ final OffloadController offload = makeOffloadController();
offload.start();
final InOrder inOrder = inOrder(mHardware);
inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
inOrder.verify(mHardware, times(1)).setUpstreamParameters(
eq(testIfName), eq(ipv4Addr), eq(null), eq(null));
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
inOrder.verifyNoMoreInteractions();
final String ipv4Gateway = "192.0.2.1";
inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
inOrder.verify(mHardware, times(1)).setUpstreamParameters(
eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), eq(null));
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
inOrder.verifyNoMoreInteractions();
final String ipv6Gw1 = "fe80::cafe";
inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
inOrder.verify(mHardware, times(1)).setUpstreamParameters(
eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
ArrayList<String> v6gws = mStringArrayCaptor.getValue();
assertEquals(1, v6gws.size());
assertTrue(v6gws.contains(ipv6Gw1));
inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
inOrder.verify(mHardware, times(1)).setUpstreamParameters(
eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
v6gws = mStringArrayCaptor.getValue();
assertEquals(2, v6gws.size());
assertTrue(v6gws.contains(ipv6Gw1));
inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
inOrder.verify(mHardware, times(1)).setUpstreamParameters(
eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
v6gws = mStringArrayCaptor.getValue();
assertEquals(2, v6gws.size());
assertTrue(v6gws.contains(ipv6Gw1));
assertEquals(2, v6gws.size());
assertTrue(v6gws.contains(ipv6Gw1));
assertTrue(v6gws.contains(ipv6Gw2));
+ inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
inOrder.verifyNoMoreInteractions();
// Completely identical LinkProperties updates are de-duped.
anyObject(), anyObject(), anyObject(), anyObject());
inOrder.verifyNoMoreInteractions();
}
+
+ private void assertNetworkStats(String iface, ForwardedStats stats, NetworkStats.Entry entry) {
+ assertEquals(iface, entry.iface);
+ assertEquals(stats.rxBytes, entry.rxBytes);
+ assertEquals(stats.txBytes, entry.txBytes);
+ assertEquals(SET_DEFAULT, entry.set);
+ assertEquals(TAG_NONE, entry.tag);
+ assertEquals(UID_TETHERING, entry.uid);
+ }
+
+ @Test
+ public void testGetForwardedStats() throws Exception {
+ setupFunctioningHardwareInterface();
+ enableOffload();
+
+ final OffloadController offload = makeOffloadController();
+ offload.start();
+
+ final String ethernetIface = "eth1";
+ final String mobileIface = "rmnet_data0";
+
+ ForwardedStats ethernetStats = new ForwardedStats();
+ ethernetStats.rxBytes = 12345;
+ ethernetStats.txBytes = 54321;
+
+ ForwardedStats mobileStats = new ForwardedStats();
+ mobileStats.rxBytes = 999;
+ mobileStats.txBytes = 99999;
+
+ when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(ethernetStats);
+ when(mHardware.getForwardedStats(eq(mobileIface))).thenReturn(mobileStats);
+
+ final LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName(ethernetIface);
+ offload.setUpstreamLinkProperties(lp);
+
+ lp.setInterfaceName(mobileIface);
+ offload.setUpstreamLinkProperties(lp);
+
+ lp.setInterfaceName(ethernetIface);
+ offload.setUpstreamLinkProperties(lp);
+
+ ethernetStats.rxBytes = 100000;
+ ethernetStats.txBytes = 100000;
+ offload.setUpstreamLinkProperties(null);
+
+ NetworkStats stats = mTetherStatsProviderCaptor.getValue().getTetherStats();
+ assertEquals(2, stats.size());
+
+ NetworkStats.Entry entry = null;
+ int ethernetPosition = ethernetIface.equals(stats.getValues(0, entry).iface) ? 0 : 1;
+ int mobilePosition = 1 - ethernetPosition;
+
+ entry = stats.getValues(mobilePosition, entry);
+ assertNetworkStats(mobileIface, mobileStats, entry);
+
+ ethernetStats.rxBytes = 12345 + 100000;
+ ethernetStats.txBytes = 54321 + 100000;
+ entry = stats.getValues(ethernetPosition, entry);
+ assertNetworkStats(ethernetIface, ethernetStats, entry);
+ }
}