resolve merge conflicts of a1d67fb38ec0 to stage-aosp-master
am: 8228eb3446

Change-Id: Ic9ba31da8e5281c9dbc8fa7ff56a63956a495b72
diff --git a/Android.mk b/Android.mk
index f54984f..7bdc572 100644
--- a/Android.mk
+++ b/Android.mk
@@ -234,6 +234,7 @@
 	core/java/android/net/INetworkScoreService.aidl \
 	core/java/android/net/INetworkStatsService.aidl \
 	core/java/android/net/INetworkStatsSession.aidl \
+	core/java/android/net/ITetheringStatsProvider.aidl \
 	core/java/android/net/nsd/INsdManager.aidl \
 	core/java/android/nfc/IAppCallback.aidl \
 	core/java/android/nfc/INfcAdapter.aidl \
diff --git a/core/java/android/net/ITetheringStatsProvider.aidl b/core/java/android/net/ITetheringStatsProvider.aidl
new file mode 100644
index 0000000..769086d
--- /dev/null
+++ b/core/java/android/net/ITetheringStatsProvider.aidl
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.NetworkStats;
+
+/**
+ * Interface that allows NetworkManagementService to query for tethering statistics.
+ *
+ * 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
+ * NetworkManagementService instead of using INetworkManagementService, and then deleting this
+ * interface.
+ *
+ * @hide
+ */
+interface ITetheringStatsProvider {
+    NetworkStats getTetherStats();
+}
diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl
index 92e78bc..3de2174 100644
--- a/core/java/android/os/INetworkManagementService.aidl
+++ b/core/java/android/os/INetworkManagementService.aidl
@@ -20,6 +20,7 @@
 import android.net.InterfaceConfiguration;
 import android.net.INetd;
 import android.net.INetworkManagementEventObserver;
+import android.net.ITetheringStatsProvider;
 import android.net.Network;
 import android.net.NetworkStats;
 import android.net.RouteInfo;
@@ -207,6 +208,18 @@
     void disableNat(String internalInterface, String externalInterface);
 
     /**
+     * Registers a {@code ITetheringStatsProvider} to provide tethering statistics.
+     * All registered providers will be called in order, and their results will be added together.
+     * Netd is always registered as a tethering stats provider.
+     */
+    void registerTetheringStatsProvider(ITetheringStatsProvider provider, String name);
+
+    /**
+     * Unregisters a previously-registered {@code ITetheringStatsProvider}.
+     */
+    void unregisterTetheringStatsProvider(ITetheringStatsProvider provider);
+
+    /**
      ** PPPD
      **/
 
diff --git a/services/core/java/com/android/server/NetworkManagementService.java b/services/core/java/com/android/server/NetworkManagementService.java
index 8ea334d..7959e39 100644
--- a/services/core/java/com/android/server/NetworkManagementService.java
+++ b/services/core/java/com/android/server/NetworkManagementService.java
@@ -18,6 +18,7 @@
 
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.SHUTDOWN;
 import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_DOZABLE;
 import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE;
@@ -55,6 +56,7 @@
 import android.net.ConnectivityManager;
 import android.net.INetd;
 import android.net.INetworkManagementEventObserver;
+import android.net.ITetheringStatsProvider;
 import android.net.InterfaceConfiguration;
 import android.net.IpPrefix;
 import android.net.LinkAddress;
@@ -225,6 +227,10 @@
 
     private final NetworkStatsFactory mStatsFactory = new NetworkStatsFactory();
 
+    @GuardedBy("mTetheringStatsProviders")
+    private final HashMap<ITetheringStatsProvider, String>
+            mTetheringStatsProviders = Maps.newHashMap();
+
     /**
      * If both locks need to be held, then they should be obtained in the order:
      * first {@link #mQuotaLock} and then {@link #mRulesLock}.
@@ -331,6 +337,10 @@
         Watchdog.getInstance().addMonitor(this);
 
         LocalServices.addService(NetworkManagementInternal.class, new LocalService());
+
+        synchronized (mTetheringStatsProviders) {
+            mTetheringStatsProviders.put(new NetdTetheringStatsProvider(), "netd");
+        }
     }
 
     @VisibleForTesting
@@ -520,6 +530,23 @@
         }
     }
 
+    @Override
+    public void registerTetheringStatsProvider(ITetheringStatsProvider provider, String name) {
+        mContext.enforceCallingOrSelfPermission(NETWORK_STACK, TAG);
+        Preconditions.checkNotNull(provider);
+        synchronized(mTetheringStatsProviders) {
+            mTetheringStatsProviders.put(provider, name);
+        }
+    }
+
+    @Override
+    public void unregisterTetheringStatsProvider(ITetheringStatsProvider provider) {
+        mContext.enforceCallingOrSelfPermission(NETWORK_STACK, TAG);
+        synchronized(mTetheringStatsProviders) {
+            mTetheringStatsProviders.remove(provider);
+        }
+    }
+
     // Sync the state of the given chain with the native daemon.
     private void syncFirewallChainLocked(int chain, String name) {
         SparseIntArray rules;
@@ -1789,14 +1816,16 @@
         }
     }
 
-    @Override
-    public NetworkStats getNetworkStatsTethering() {
-        mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
-
-        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1);
-        try {
-            final NativeDaemonEvent[] events = mConnector.executeForList(
-                    "bandwidth", "gettetherstats");
+    private class NetdTetheringStatsProvider extends ITetheringStatsProvider.Stub {
+        @Override
+        public NetworkStats getTetherStats() {
+            final NativeDaemonEvent[] events;
+            try {
+                events = mConnector.executeForList("bandwidth", "gettetherstats");
+            } catch (NativeDaemonConnectorException e) {
+                throw e.rethrowAsParcelableException();
+            }
+            final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1);
             for (NativeDaemonEvent event : events) {
                 if (event.getCode() != TetheringStatsListResult) continue;
 
@@ -1822,8 +1851,24 @@
                     throw new IllegalStateException("problem parsing tethering stats: " + event);
                 }
             }
-        } catch (NativeDaemonConnectorException e) {
-            throw e.rethrowAsParcelableException();
+            return stats;
+        }
+    }
+
+    @Override
+    public NetworkStats getNetworkStatsTethering() {
+        mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
+
+        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 1);
+        synchronized (mTetheringStatsProviders) {
+            for (ITetheringStatsProvider provider: mTetheringStatsProviders.keySet()) {
+                try {
+                    stats.combineAllValues(provider.getTetherStats());
+                } catch (RemoteException e) {
+                    Log.e(TAG, "Problem reading tethering stats from " +
+                            mTetheringStatsProviders.get(provider) + ": " + e);
+                }
+            }
         }
         return stats;
     }
diff --git a/services/core/java/com/android/server/connectivity/Tethering.java b/services/core/java/com/android/server/connectivity/Tethering.java
index 1fb944f..b0be8f7 100644
--- a/services/core/java/com/android/server/connectivity/Tethering.java
+++ b/services/core/java/com/android/server/connectivity/Tethering.java
@@ -214,7 +214,7 @@
         final Handler smHandler = mTetherMasterSM.getHandler();
         mOffloadController = new OffloadController(smHandler,
                 deps.getOffloadHardwareInterface(smHandler, mLog),
-                mContext.getContentResolver(),
+                mContext.getContentResolver(), mNMService,
                 mLog);
         mUpstreamNetworkMonitor = new UpstreamNetworkMonitor(
                 mContext, mTetherMasterSM, mLog, TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
@@ -1759,6 +1759,11 @@
             pw.decreaseIndent();
         }
 
+        pw.println("Hardware offload:");
+        pw.increaseIndent();
+        mOffloadController.dump(pw);
+        pw.decreaseIndent();
+
         pw.println("Log:");
         pw.increaseIndent();
         if (argsContain(args, SHORT_ARG)) {
diff --git a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
index b473867..1a5ff77 100644
--- a/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
+++ b/services/core/java/com/android/server/connectivity/tethering/OffloadController.java
@@ -16,24 +16,38 @@
 
 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 com.android.internal.util.IndentingPrintWriter;
 
 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
@@ -44,6 +58,8 @@
 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;
@@ -59,14 +75,25 @@
     // 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() {
@@ -138,6 +165,7 @@
 
     public void stop() {
         final boolean wasStarted = started();
+        updateStatsForCurrentUpstream();
         mUpstreamLinkProperties = null;
         mHwInterface.stopOffloadControl();
         mControlInitialized = false;
@@ -145,16 +173,76 @@
         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) {
@@ -262,4 +350,16 @@
         for (IpPrefix pfx : prefixSet) localPrefixStrs.add(pfx.toString());
         return localPrefixStrs;
     }
+
+    public void dump(IndentingPrintWriter pw) {
+        if (isOffloadDisabled()) {
+            pw.println("Offload disabled");
+            return;
+        }
+        pw.println("Offload HALs " + (started() ? "started" : "not started"));
+        LinkProperties lp = mUpstreamLinkProperties;
+        String upstream = (lp != null) ? lp.getInterfaceName() : null;
+        pw.println("Current upstream: " + upstream);
+        pw.println("Exempt prefixes: " + mLastLocalPrefixStrs);
+    }
 }
diff --git a/tests/net/java/com/android/server/connectivity/TetheringTest.java b/tests/net/java/com/android/server/connectivity/TetheringTest.java
index a214223..3f0eacf 100644
--- a/tests/net/java/com/android/server/connectivity/TetheringTest.java
+++ b/tests/net/java/com/android/server/connectivity/TetheringTest.java
@@ -178,6 +178,7 @@
         mTethering = new Tethering(mServiceContext, mNMService, mStatsService, mPolicyManager,
                                    mLooper.getLooper(), mSystemProperties,
                                    mTetheringDependencies);
+        verify(mNMService).registerTetheringStatsProvider(any(), anyString());
     }
 
     @After
diff --git a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
index 789ce6c..45525e6 100644
--- a/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
+++ b/tests/net/java/com/android/server/connectivity/tethering/OffloadControllerTest.java
@@ -16,26 +16,38 @@
 
 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;
 
@@ -66,11 +78,14 @@
     @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");
@@ -90,15 +105,24 @@
         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 {
@@ -106,8 +130,7 @@
             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);
@@ -120,7 +143,7 @@
 
     // TODO: Restore when FakeSettingsProvider.clearSettingsProvider() is available.
     // @Test
-    public void testNoSettingsValueDefaultEnabledDoesStart() {
+    public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception {
         setupFunctioningHardwareInterface();
         when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0);
         try {
@@ -128,8 +151,7 @@
             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);
@@ -141,12 +163,11 @@
     }
 
     @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);
@@ -158,12 +179,11 @@
     }
 
     @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);
@@ -178,8 +198,7 @@
         setupFunctioningHardwareInterface();
         enableOffload();
 
-        final OffloadController offload =
-                new OffloadController(null, mHardware, mContentResolver, new SharedLog("test"));
+        final OffloadController offload = makeOffloadController();
         offload.start();
 
         final InOrder inOrder = inOrder(mHardware);
@@ -244,6 +263,7 @@
         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";
@@ -253,6 +273,7 @@
         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";
@@ -262,6 +283,7 @@
         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));
@@ -274,6 +296,7 @@
         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));
@@ -291,6 +314,7 @@
         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));
@@ -325,6 +349,7 @@
         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.
@@ -335,4 +360,65 @@
                 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);
+    }
 }