Merge "[Tether06] Migrate tether offload controller into module"
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 998572f..61bfb92 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -27,6 +27,7 @@
         "androidx.annotation_annotation",
         "netd_aidl_interface-java",
         "networkstack-aidl-interfaces-java",
+        "android.hardware.tetheroffload.control-V1.0-java",
         "tethering-client",
     ],
     manifest: "AndroidManifestBase.xml",
@@ -38,11 +39,39 @@
     defaults: ["TetheringAndroidLibraryDefaults"],
 }
 
+cc_library_shared {
+    name: "libtetheroffloadjni",
+    srcs: [
+        "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp",
+    ],
+    shared_libs: [
+        "libnativehelper",
+        "libcutils",
+        "android.hardware.tetheroffload.config@1.0",
+    ],
+    static_libs: [
+        "liblog",
+        "libbase",
+        "libhidlbase",
+        "libutils",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wno-unused-parameter",
+        "-Wthread-safety",
+    ],
+}
+
 // Common defaults for compiling the actual APK.
 java_defaults {
     name: "TetheringAppDefaults",
     platform_apis: true,
     privileged: true,
+    jni_libs: [
+        "libtetheroffloadjni",
+    ],
     resource_dirs: [
         "res",
     ],
@@ -71,6 +100,8 @@
     name: "tethering-servicescore-srcs",
     srcs: [
         "src/com/android/server/connectivity/tethering/EntitlementManager.java",
+        "src/com/android/server/connectivity/tethering/OffloadController.java",
+        "src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java",
         "src/com/android/server/connectivity/tethering/TetheringConfiguration.java",
         "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java",
     ],
@@ -88,3 +119,11 @@
         "src/android/net/util/PrefixUtils.java",
     ],
 }
+
+// This group would be removed when tethering migration is done.
+filegroup {
+    name: "tethering-jni-srcs",
+    srcs: [
+        "jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp",
+    ],
+}
diff --git a/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
new file mode 100644
index 0000000..3eaf488
--- /dev/null
+++ b/Tethering/jni/com_android_server_connectivity_tethering_OffloadHardwareInterface.cpp
@@ -0,0 +1,148 @@
+/*
+ * 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.
+ */
+
+#include <errno.h>
+#include <error.h>
+#include <hidl/HidlSupport.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <linux/netfilter/nfnetlink.h>
+#include <linux/netlink.h>
+#include <sys/socket.h>
+#include <android-base/unique_fd.h>
+#include <android/hardware/tetheroffload/config/1.0/IOffloadConfig.h>
+
+#define LOG_TAG "OffloadHardwareInterface"
+#include <utils/Log.h>
+
+namespace android {
+
+using hardware::hidl_handle;
+using hardware::hidl_string;
+using hardware::tetheroffload::config::V1_0::IOffloadConfig;
+
+namespace {
+
+inline const sockaddr * asSockaddr(const sockaddr_nl *nladdr) {
+    return reinterpret_cast<const sockaddr *>(nladdr);
+}
+
+int conntrackSocket(unsigned groups) {
+    base::unique_fd s(socket(AF_NETLINK, SOCK_DGRAM, NETLINK_NETFILTER));
+    if (s.get() < 0) return -errno;
+
+    const struct sockaddr_nl bind_addr = {
+        .nl_family = AF_NETLINK,
+        .nl_pad = 0,
+        .nl_pid = 0,
+        .nl_groups = groups,
+    };
+    if (bind(s.get(), asSockaddr(&bind_addr), sizeof(bind_addr)) != 0) {
+        return -errno;
+    }
+
+    const struct sockaddr_nl kernel_addr = {
+        .nl_family = AF_NETLINK,
+        .nl_pad = 0,
+        .nl_pid = 0,
+        .nl_groups = groups,
+    };
+    if (connect(s.get(), asSockaddr(&kernel_addr), sizeof(kernel_addr)) != 0) {
+        return -errno;
+    }
+
+    return s.release();
+}
+
+// Return a hidl_handle that owns the file descriptor owned by fd, and will
+// auto-close it (otherwise there would be double-close problems).
+//
+// Rely upon the compiler to eliminate the constexprs used for clarity.
+hidl_handle handleFromFileDescriptor(base::unique_fd fd) {
+    hidl_handle h;
+
+    static constexpr int kNumFds = 1;
+    static constexpr int kNumInts = 0;
+    native_handle_t *nh = native_handle_create(kNumFds, kNumInts);
+    nh->data[0] = fd.release();
+
+    static constexpr bool kTakeOwnership = true;
+    h.setTo(nh, kTakeOwnership);
+
+    return h;
+}
+
+}  // namespace
+
+static jboolean android_server_connectivity_tethering_OffloadHardwareInterface_configOffload(
+        JNIEnv* /* env */) {
+    sp<IOffloadConfig> configInterface = IOffloadConfig::getService();
+    if (configInterface.get() == nullptr) {
+        ALOGD("Could not find IOffloadConfig service.");
+        return false;
+    }
+
+    // Per the IConfigOffload definition:
+    //
+    // fd1   A file descriptor bound to the following netlink groups
+    //       (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY).
+    //
+    // fd2   A file descriptor bound to the following netlink groups
+    //       (NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY).
+    base::unique_fd
+            fd1(conntrackSocket(NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY)),
+            fd2(conntrackSocket(NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY));
+    if (fd1.get() < 0 || fd2.get() < 0) {
+        ALOGE("Unable to create conntrack handles: %d/%s", errno, strerror(errno));
+        return false;
+    }
+
+    hidl_handle h1(handleFromFileDescriptor(std::move(fd1))),
+                h2(handleFromFileDescriptor(std::move(fd2)));
+
+    bool rval(false);
+    hidl_string msg;
+    const auto status = configInterface->setHandles(h1, h2,
+            [&rval, &msg](bool success, const hidl_string& errMsg) {
+                rval = success;
+                msg = errMsg;
+            });
+    if (!status.isOk() || !rval) {
+        ALOGE("IOffloadConfig::setHandles() error: '%s' / '%s'",
+              status.description().c_str(), msg.c_str());
+        // If status is somehow not ok, make sure rval captures this too.
+        rval = false;
+    }
+
+    return rval;
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+    /* name, signature, funcPtr */
+    { "configOffload", "()Z",
+      (void*) android_server_connectivity_tethering_OffloadHardwareInterface_configOffload },
+};
+
+int register_android_server_connectivity_tethering_OffloadHardwareInterface(JNIEnv* env) {
+    return jniRegisterNativeMethods(env,
+            "com/android/server/connectivity/tethering/OffloadHardwareInterface",
+            gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java
new file mode 100644
index 0000000..16734d8
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadController.java
@@ -0,0 +1,671 @@
+/*
+ * 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 com.android.server.connectivity.tethering;
+
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.STATS_PER_UID;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+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.netlink.ConntrackMessage;
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkSocket;
+import android.net.util.IpUtils;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.text.TextUtils;
+
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.connectivity.tethering.OffloadHardwareInterface.ForwardedStats;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A class to encapsulate the business logic of programming the tethering
+ * hardware offload interface.
+ *
+ * @hide
+ */
+public class OffloadController {
+    private static final String TAG = OffloadController.class.getSimpleName();
+    private static final boolean DBG = false;
+    private static final String ANYIP = "0.0.0.0";
+    private static final ForwardedStats EMPTY_STATS = new ForwardedStats();
+
+    private enum UpdateType { IF_NEEDED, FORCE };
+
+    private final Handler mHandler;
+    private final OffloadHardwareInterface mHwInterface;
+    private final ContentResolver mContentResolver;
+    private final INetworkManagementService mNms;
+    private final ITetheringStatsProvider mStatsProvider;
+    private final SharedLog mLog;
+    private final HashMap<String, LinkProperties> mDownstreams;
+    private boolean mConfigInitialized;
+    private boolean mControlInitialized;
+    private LinkProperties mUpstreamLinkProperties;
+    // The complete set of offload-exempt prefixes passed in via Tethering from
+    // all upstream and downstream sources.
+    private Set<IpPrefix> mExemptPrefixes;
+    // A strictly "smaller" set of prefixes, wherein offload-approved prefixes
+    // (e.g. downstream on-link prefixes) have been removed and replaced with
+    // prefixes representing only the locally-assigned IP addresses.
+    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 ConcurrentHashMap<String, ForwardedStats> mForwardedStats =
+            new ConcurrentHashMap<>(16, 0.75F, 1);
+
+    // 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<>();
+
+    private int mNatUpdateCallbacksReceived;
+    private int mNatUpdateNetlinkErrors;
+
+    public OffloadController(Handler h, OffloadHardwareInterface hwi,
+            ContentResolver contentResolver, INetworkManagementService nms, SharedLog log) {
+        mHandler = h;
+        mHwInterface = hwi;
+        mContentResolver = contentResolver;
+        mNms = nms;
+        mStatsProvider = new OffloadTetheringStatsProvider();
+        mLog = log.forSubComponent(TAG);
+        mDownstreams = new HashMap<>();
+        mExemptPrefixes = new HashSet<>();
+        mLastLocalPrefixStrs = new HashSet<>();
+
+        try {
+            mNms.registerTetheringStatsProvider(mStatsProvider, getClass().getSimpleName());
+        } catch (RemoteException e) {
+            mLog.e("Cannot register offload stats provider: " + e);
+        }
+    }
+
+    /** Start hardware offload. */
+    public boolean start() {
+        if (started()) return true;
+
+        if (isOffloadDisabled()) {
+            mLog.i("tethering offload disabled");
+            return false;
+        }
+
+        if (!mConfigInitialized) {
+            mConfigInitialized = mHwInterface.initOffloadConfig();
+            if (!mConfigInitialized) {
+                mLog.i("tethering offload config not supported");
+                stop();
+                return false;
+            }
+        }
+
+        mControlInitialized = mHwInterface.initOffloadControl(
+                // OffloadHardwareInterface guarantees that these callback
+                // methods are called on the handler passed to it, which is the
+                // same as mHandler, as coordinated by the setup in Tethering.
+                new OffloadHardwareInterface.ControlCallback() {
+                    @Override
+                    public void onStarted() {
+                        if (!started()) return;
+                        mLog.log("onStarted");
+                    }
+
+                    @Override
+                    public void onStoppedError() {
+                        if (!started()) return;
+                        mLog.log("onStoppedError");
+                    }
+
+                    @Override
+                    public void onStoppedUnsupported() {
+                        if (!started()) return;
+                        mLog.log("onStoppedUnsupported");
+                        // Poll for statistics and trigger a sweep of tethering
+                        // stats by observers. This might not succeed, but it's
+                        // worth trying anyway. We need to do this because from
+                        // this point on we continue with software forwarding,
+                        // and we need to synchronize stats and limits between
+                        // software and hardware forwarding.
+                        updateStatsForAllUpstreams();
+                        forceTetherStatsPoll();
+                    }
+
+                    @Override
+                    public void onSupportAvailable() {
+                        if (!started()) return;
+                        mLog.log("onSupportAvailable");
+
+                        // [1] Poll for statistics and trigger a sweep of stats
+                        // by observers. We need to do this to ensure that any
+                        // limits set take into account any software tethering
+                        // traffic that has been happening in the meantime.
+                        updateStatsForAllUpstreams();
+                        forceTetherStatsPoll();
+                        // [2] (Re)Push all state.
+                        computeAndPushLocalPrefixes(UpdateType.FORCE);
+                        pushAllDownstreamState();
+                        pushUpstreamParameters(null);
+                    }
+
+                    @Override
+                    public void onStoppedLimitReached() {
+                        if (!started()) return;
+                        mLog.log("onStoppedLimitReached");
+
+                        // We cannot reliably determine on which interface the limit was reached,
+                        // because the HAL interface does not specify it. We cannot just use the
+                        // current upstream, because that might have changed since the time that
+                        // the HAL queued the callback.
+                        // TODO: rev the HAL so that it provides an interface name.
+
+                        // Fetch current stats, so that when our notification reaches
+                        // NetworkStatsService and triggers a poll, we will respond with
+                        // current data (which will be above the limit that was reached).
+                        // Note that if we just changed upstream, this is unnecessary but harmless.
+                        // The stats for the previous upstream were already updated on this thread
+                        // just after the upstream was changed, so they are also up-to-date.
+                        updateStatsForCurrentUpstream();
+                        forceTetherStatsPoll();
+                    }
+
+                    @Override
+                    public void onNatTimeoutUpdate(int proto,
+                                                   String srcAddr, int srcPort,
+                                                   String dstAddr, int dstPort) {
+                        if (!started()) return;
+                        updateNatTimeout(proto, srcAddr, srcPort, dstAddr, dstPort);
+                    }
+                });
+
+        final boolean isStarted = started();
+        if (!isStarted) {
+            mLog.i("tethering offload control not supported");
+            stop();
+        } else {
+            mLog.log("tethering offload started");
+            mNatUpdateCallbacksReceived = 0;
+            mNatUpdateNetlinkErrors = 0;
+        }
+        return isStarted;
+    }
+
+    /** Stop hardware offload. */
+    public void stop() {
+        // Completely stops tethering offload. After this method is called, it is no longer safe to
+        // call any HAL method, no callbacks from the hardware will be delivered, and any in-flight
+        // callbacks must be ignored. Offload may be started again by calling start().
+        final boolean wasStarted = started();
+        updateStatsForCurrentUpstream();
+        mUpstreamLinkProperties = null;
+        mHwInterface.stopOffloadControl();
+        mControlInitialized = false;
+        mConfigInitialized = false;
+        if (wasStarted) mLog.log("tethering offload stopped");
+    }
+
+    private boolean started() {
+        return mConfigInitialized && mControlInitialized;
+    }
+
+    private class OffloadTetheringStatsProvider extends ITetheringStatsProvider.Stub {
+        @Override
+        public NetworkStats getTetherStats(int how) {
+            // getTetherStats() is the only function in OffloadController that can be called from
+            // a different thread. Do not attempt to update stats by querying the offload HAL
+            // synchronously from a different thread than our Handler thread. http://b/64771555.
+            Runnable updateStats = () -> {
+                updateStatsForCurrentUpstream();
+            };
+            if (Looper.myLooper() == mHandler.getLooper()) {
+                updateStats.run();
+            } else {
+                mHandler.post(updateStats);
+            }
+
+            NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 0);
+            NetworkStats.Entry entry = new NetworkStats.Entry();
+            entry.set = SET_DEFAULT;
+            entry.tag = TAG_NONE;
+            entry.uid = (how == STATS_PER_UID) ? UID_TETHERING : UID_ALL;
+
+            for (Map.Entry<String, ForwardedStats> kv : mForwardedStats.entrySet()) {
+                ForwardedStats value = kv.getValue();
+                entry.iface = kv.getKey();
+                entry.rxBytes = value.rxBytes;
+                entry.txBytes = value.txBytes;
+                stats.addValues(entry);
+            }
+
+            return stats;
+        }
+
+        @Override
+        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 String currentUpstreamInterface() {
+        return (mUpstreamLinkProperties != null)
+                ? mUpstreamLinkProperties.getInterfaceName() : null;
+    }
+
+    private void maybeUpdateStats(String iface) {
+        if (TextUtils.isEmpty(iface)) {
+            return;
+        }
+
+        // Always called on the handler thread.
+        //
+        // Use get()/put() instead of updating ForwardedStats in place because we can be called
+        // concurrently with getTetherStats. In combination with the guarantees provided by
+        // ConcurrentHashMap, this ensures that getTetherStats always gets the most recent copy of
+        // the stats for each interface, and does not observe partial writes where rxBytes is
+        // updated and txBytes is not.
+        ForwardedStats diff = mHwInterface.getForwardedStats(iface);
+        ForwardedStats base = mForwardedStats.get(iface);
+        if (base != null) {
+            diff.add(base);
+        }
+        mForwardedStats.put(iface, diff);
+        // diff is a new object, just created by getForwardedStats(). Therefore, anyone reading from
+        // mForwardedStats (i.e., any caller of getTetherStats) will see the new stats immediately.
+    }
+
+    private boolean maybeUpdateDataLimit(String iface) {
+        // setDataLimit may only be called while offload is occurring on this upstream.
+        if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
+            return true;
+        }
+
+        Long limit = mInterfaceQuotas.get(iface);
+        if (limit == null) {
+            limit = Long.MAX_VALUE;
+        }
+
+        return mHwInterface.setDataLimit(iface, limit);
+    }
+
+    private void updateStatsForCurrentUpstream() {
+        maybeUpdateStats(currentUpstreamInterface());
+    }
+
+    private void updateStatsForAllUpstreams() {
+        // In practice, there should only ever be a single digit number of
+        // upstream interfaces over the lifetime of an active tethering session.
+        // Roughly speaking, imagine a very ambitious one or two of each of the
+        // following interface types: [ "rmnet_data", "wlan", "eth", "rndis" ].
+        for (Map.Entry<String, ForwardedStats> kv : mForwardedStats.entrySet()) {
+            maybeUpdateStats(kv.getKey());
+        }
+    }
+
+    private void forceTetherStatsPoll() {
+        try {
+            mNms.tetherLimitReached(mStatsProvider);
+        } catch (RemoteException e) {
+            mLog.e("Cannot report data limit reached: " + e);
+        }
+    }
+
+    /** Set current tethering upstream LinkProperties. */
+    public void setUpstreamLinkProperties(LinkProperties lp) {
+        if (!started() || Objects.equals(mUpstreamLinkProperties, lp)) return;
+
+        final String prevUpstream = currentUpstreamInterface();
+
+        mUpstreamLinkProperties = (lp != null) ? new LinkProperties(lp) : null;
+        // Make sure we record this interface in the ForwardedStats map.
+        final String iface = currentUpstreamInterface();
+        if (!TextUtils.isEmpty(iface)) mForwardedStats.putIfAbsent(iface, EMPTY_STATS);
+
+        // 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(UpdateType.IF_NEEDED);
+        pushUpstreamParameters(prevUpstream);
+    }
+
+    /** Set local prefixes. */
+    public void setLocalPrefixes(Set<IpPrefix> localPrefixes) {
+        mExemptPrefixes = localPrefixes;
+
+        if (!started()) return;
+        computeAndPushLocalPrefixes(UpdateType.IF_NEEDED);
+    }
+
+    /** Update current downstream LinkProperties. */
+    public void notifyDownstreamLinkProperties(LinkProperties lp) {
+        final String ifname = lp.getInterfaceName();
+        final LinkProperties oldLp = mDownstreams.put(ifname, new LinkProperties(lp));
+        if (Objects.equals(oldLp, lp)) return;
+
+        if (!started()) return;
+        pushDownstreamState(oldLp, lp);
+    }
+
+    private void pushDownstreamState(LinkProperties oldLp, LinkProperties newLp) {
+        final String ifname = newLp.getInterfaceName();
+        final List<RouteInfo> oldRoutes =
+                (oldLp != null) ? oldLp.getRoutes() : Collections.EMPTY_LIST;
+        final List<RouteInfo> newRoutes = newLp.getRoutes();
+
+        // For each old route, if not in new routes: remove.
+        for (RouteInfo ri : oldRoutes) {
+            if (shouldIgnoreDownstreamRoute(ri)) continue;
+            if (!newRoutes.contains(ri)) {
+                mHwInterface.removeDownstreamPrefix(ifname, ri.getDestination().toString());
+            }
+        }
+
+        // For each new route, if not in old routes: add.
+        for (RouteInfo ri : newRoutes) {
+            if (shouldIgnoreDownstreamRoute(ri)) continue;
+            if (!oldRoutes.contains(ri)) {
+                mHwInterface.addDownstreamPrefix(ifname, ri.getDestination().toString());
+            }
+        }
+    }
+
+    private void pushAllDownstreamState() {
+        for (LinkProperties lp : mDownstreams.values()) {
+            pushDownstreamState(null, lp);
+        }
+    }
+
+    /** Remove downstream interface from offload hardware. */
+    public void removeDownstreamInterface(String ifname) {
+        final LinkProperties lp = mDownstreams.remove(ifname);
+        if (lp == null) return;
+
+        if (!started()) return;
+
+        for (RouteInfo route : lp.getRoutes()) {
+            if (shouldIgnoreDownstreamRoute(route)) continue;
+            mHwInterface.removeDownstreamPrefix(ifname, route.getDestination().toString());
+        }
+    }
+
+    private boolean isOffloadDisabled() {
+        final int defaultDisposition = mHwInterface.getDefaultTetherOffloadDisabled();
+        return (Settings.Global.getInt(
+                mContentResolver, TETHER_OFFLOAD_DISABLED, defaultDisposition) != 0);
+    }
+
+    private boolean pushUpstreamParameters(String prevUpstream) {
+        final String iface = currentUpstreamInterface();
+
+        if (TextUtils.isEmpty(iface)) {
+            final boolean rval = mHwInterface.setUpstreamParameters("", ANYIP, ANYIP, null);
+            // Update stats after we've told the hardware to stop forwarding so
+            // we don't miss packets.
+            maybeUpdateStats(prevUpstream);
+            return rval;
+        }
+
+        // A stacked interface cannot be an upstream for hardware offload.
+        // Consequently, we examine only the primary interface name, look at
+        // getAddresses() rather than getAllAddresses(), and check getRoutes()
+        // rather than getAllRoutes().
+        final ArrayList<String> v6gateways = new ArrayList<>();
+        String v4addr = null;
+        String v4gateway = null;
+
+        for (InetAddress ip : mUpstreamLinkProperties.getAddresses()) {
+            if (ip instanceof Inet4Address) {
+                v4addr = ip.getHostAddress();
+                break;
+            }
+        }
+
+        // Find the gateway addresses of all default routes of either address family.
+        for (RouteInfo ri : mUpstreamLinkProperties.getRoutes()) {
+            if (!ri.hasGateway()) continue;
+
+            final String gateway = ri.getGateway().getHostAddress();
+            if (ri.isIPv4Default()) {
+                v4gateway = gateway;
+            } else if (ri.isIPv6Default()) {
+                v6gateways.add(gateway);
+            }
+        }
+
+        boolean success = mHwInterface.setUpstreamParameters(
+                iface, v4addr, v4gateway, (v6gateways.isEmpty() ? null : v6gateways));
+
+        if (!success) {
+            return success;
+        }
+
+        // Update stats after we've told the hardware to change routing so we don't miss packets.
+        maybeUpdateStats(prevUpstream);
+
+        // Data limits can only be set once offload is running on the upstream.
+        success = maybeUpdateDataLimit(iface);
+        if (!success) {
+            // If we failed to set a data limit, don't use this upstream, because we don't want to
+            // blow through the data limit that we were told to apply.
+            mLog.log("Setting data limit for " + iface + " failed, disabling offload.");
+            stop();
+        }
+
+        return success;
+    }
+
+    private boolean computeAndPushLocalPrefixes(UpdateType how) {
+        final boolean force = (how == UpdateType.FORCE);
+        final Set<String> localPrefixStrs = computeLocalPrefixStrings(
+                mExemptPrefixes, mUpstreamLinkProperties);
+        if (!force && mLastLocalPrefixStrs.equals(localPrefixStrs)) return true;
+
+        mLastLocalPrefixStrs = localPrefixStrs;
+        return mHwInterface.setLocalPrefixes(new ArrayList<>(localPrefixStrs));
+    }
+
+    // TODO: Factor in downstream LinkProperties once that information is available.
+    private static Set<String> computeLocalPrefixStrings(
+            Set<IpPrefix> localPrefixes, LinkProperties upstreamLinkProperties) {
+        // Create an editable copy.
+        final Set<IpPrefix> prefixSet = new HashSet<>(localPrefixes);
+
+        // TODO: If a downstream interface (not currently passed in) is reusing
+        // the /64 of the upstream (64share) then:
+        //
+        //     [a] remove that /64 from the local prefixes
+        //     [b] add in /128s for IP addresses on the downstream interface
+        //     [c] add in /128s for IP addresses on the upstream interface
+        //
+        // Until downstream information is available here, simply add /128s from
+        // the upstream network; they'll just be redundant with their /64.
+        if (upstreamLinkProperties != null) {
+            for (LinkAddress linkAddr : upstreamLinkProperties.getLinkAddresses()) {
+                if (!linkAddr.isGlobalPreferred()) continue;
+                final InetAddress ip = linkAddr.getAddress();
+                if (!(ip instanceof Inet6Address)) continue;
+                prefixSet.add(new IpPrefix(ip, 128));
+            }
+        }
+
+        final HashSet<String> localPrefixStrs = new HashSet<>();
+        for (IpPrefix pfx : prefixSet) localPrefixStrs.add(pfx.toString());
+        return localPrefixStrs;
+    }
+
+    private static boolean shouldIgnoreDownstreamRoute(RouteInfo route) {
+        // Ignore any link-local routes.
+        if (!route.getDestinationLinkAddress().isGlobalPreferred()) return true;
+
+        return false;
+    }
+
+    /** Dump information. */
+    public void dump(IndentingPrintWriter pw) {
+        if (isOffloadDisabled()) {
+            pw.println("Offload disabled");
+            return;
+        }
+        final boolean isStarted = started();
+        pw.println("Offload HALs " + (isStarted ? "started" : "not started"));
+        LinkProperties lp = mUpstreamLinkProperties;
+        String upstream = (lp != null) ? lp.getInterfaceName() : null;
+        pw.println("Current upstream: " + upstream);
+        pw.println("Exempt prefixes: " + mLastLocalPrefixStrs);
+        pw.println("NAT timeout update callbacks received during the "
+                + (isStarted ? "current" : "last")
+                + " offload session: "
+                + mNatUpdateCallbacksReceived);
+        pw.println("NAT timeout update netlink errors during the "
+                + (isStarted ? "current" : "last")
+                + " offload session: "
+                + mNatUpdateNetlinkErrors);
+    }
+
+    private void updateNatTimeout(
+            int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) {
+        final String protoName = protoNameFor(proto);
+        if (protoName == null) {
+            mLog.e("Unknown NAT update callback protocol: " + proto);
+            return;
+        }
+
+        final Inet4Address src = parseIPv4Address(srcAddr);
+        if (src == null) {
+            mLog.e("Failed to parse IPv4 address: " + srcAddr);
+            return;
+        }
+
+        if (!IpUtils.isValidUdpOrTcpPort(srcPort)) {
+            mLog.e("Invalid src port: " + srcPort);
+            return;
+        }
+
+        final Inet4Address dst = parseIPv4Address(dstAddr);
+        if (dst == null) {
+            mLog.e("Failed to parse IPv4 address: " + dstAddr);
+            return;
+        }
+
+        if (!IpUtils.isValidUdpOrTcpPort(dstPort)) {
+            mLog.e("Invalid dst port: " + dstPort);
+            return;
+        }
+
+        mNatUpdateCallbacksReceived++;
+        final String natDescription = String.format("%s (%s, %s) -> (%s, %s)",
+                protoName, srcAddr, srcPort, dstAddr, dstPort);
+        if (DBG) {
+            mLog.log("NAT timeout update: " + natDescription);
+        }
+
+        final int timeoutSec = connectionTimeoutUpdateSecondsFor(proto);
+        final byte[] msg = ConntrackMessage.newIPv4TimeoutUpdateRequest(
+                proto, src, srcPort, dst, dstPort, timeoutSec);
+
+        try {
+            NetlinkSocket.sendOneShotKernelMessage(OsConstants.NETLINK_NETFILTER, msg);
+        } catch (ErrnoException e) {
+            mNatUpdateNetlinkErrors++;
+            mLog.e("Error updating NAT conntrack entry >" + natDescription + "<: " + e
+                    + ", msg: " + NetlinkConstants.hexify(msg));
+            mLog.log("NAT timeout update callbacks received: " + mNatUpdateCallbacksReceived);
+            mLog.log("NAT timeout update netlink errors: " + mNatUpdateNetlinkErrors);
+        }
+    }
+
+    private static Inet4Address parseIPv4Address(String addrString) {
+        try {
+            final InetAddress ip = InetAddress.parseNumericAddress(addrString);
+            // TODO: Consider other sanitization steps here, including perhaps:
+            //           not eql to 0.0.0.0
+            //           not within 169.254.0.0/16
+            //           not within ::ffff:0.0.0.0/96
+            //           not within ::/96
+            // et cetera.
+            if (ip instanceof Inet4Address) {
+                return (Inet4Address) ip;
+            }
+        } catch (IllegalArgumentException iae) { }
+        return null;
+    }
+
+    private static String protoNameFor(int proto) {
+        // OsConstants values are not constant expressions; no switch statement.
+        if (proto == OsConstants.IPPROTO_UDP) {
+            return "UDP";
+        } else if (proto == OsConstants.IPPROTO_TCP) {
+            return "TCP";
+        }
+        return null;
+    }
+
+    private static int connectionTimeoutUpdateSecondsFor(int proto) {
+        // TODO: Replace this with more thoughtful work, perhaps reading from
+        // and maybe writing to any required
+        //
+        //     /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_*
+        //     /proc/sys/net/netfilter/nf_conntrack_udp_timeout{,_stream}
+        //
+        // entries.  TBD.
+        if (proto == OsConstants.IPPROTO_TCP) {
+            // Cf. /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
+            return 432000;
+        } else {
+            // Cf. /proc/sys/net/netfilter/nf_conntrack_udp_timeout_stream
+            return 180;
+        }
+    }
+}
diff --git a/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
new file mode 100644
index 0000000..01339a4
--- /dev/null
+++ b/Tethering/src/com/android/server/connectivity/tethering/OffloadHardwareInterface.java
@@ -0,0 +1,395 @@
+/*
+ * 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 com.android.server.connectivity.tethering;
+
+import static com.android.internal.util.BitUtils.uint16;
+
+import android.hardware.tetheroffload.control.V1_0.IOffloadControl;
+import android.hardware.tetheroffload.control.V1_0.ITetheringOffloadCallback;
+import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate;
+import android.hardware.tetheroffload.control.V1_0.NetworkProtocol;
+import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.system.OsConstants;
+
+import java.util.ArrayList;
+
+
+/**
+ * Capture tethering dependencies, for injection.
+ *
+ * @hide
+ */
+public class OffloadHardwareInterface {
+    private static final String TAG = OffloadHardwareInterface.class.getSimpleName();
+    private static final String YIELDS = " -> ";
+    // Change this value to control whether tether offload is enabled or
+    // disabled by default in the absence of an explicit Settings value.
+    // See accompanying unittest to distinguish 0 from non-0 values.
+    private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0;
+    private static final String NO_INTERFACE_NAME = "";
+    private static final String NO_IPV4_ADDRESS = "";
+    private static final String NO_IPV4_GATEWAY = "";
+
+    private static native boolean configOffload();
+
+    private final Handler mHandler;
+    private final SharedLog mLog;
+    private IOffloadControl mOffloadControl;
+    private TetheringOffloadCallback mTetheringOffloadCallback;
+    private ControlCallback mControlCallback;
+
+    /** The callback to notify status of offload management process. */
+    public static class ControlCallback {
+        /** Offload started. */
+        public void onStarted() {}
+        /**
+         * Offload stopped because an error has occurred in lower layer.
+         */
+        public void onStoppedError() {}
+        /**
+         * Offload stopped because the device has moved to a bearer on which hardware offload is
+         * not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will
+         * likely fail and cannot be presumed to be saved inside of the hardware management process.
+         * Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin
+         * offload again.
+         */
+        public void onStoppedUnsupported() {}
+        /** Indicate that offload is able to proivde support for this time. */
+        public void onSupportAvailable() {}
+        /** Offload stopped because of usage limit reached. */
+        public void onStoppedLimitReached() {}
+
+        /** Indicate to update NAT timeout. */
+        public void onNatTimeoutUpdate(int proto,
+                                       String srcAddr, int srcPort,
+                                       String dstAddr, int dstPort) {}
+    }
+
+    /** The object which records Tx/Rx forwarded bytes. */
+    public static class ForwardedStats {
+        public long rxBytes;
+        public long txBytes;
+
+        public ForwardedStats() {
+            rxBytes = 0;
+            txBytes = 0;
+        }
+
+        /** Add Tx/Rx bytes. */
+        public void add(ForwardedStats other) {
+            rxBytes += other.rxBytes;
+            txBytes += other.txBytes;
+        }
+
+        /** Returns the string representation of this object. */
+        public String toString() {
+            return String.format("rx:%s tx:%s", rxBytes, txBytes);
+        }
+    }
+
+    public OffloadHardwareInterface(Handler h, SharedLog log) {
+        mHandler = h;
+        mLog = log.forSubComponent(TAG);
+    }
+
+    /** Get default value indicating whether offload is supported. */
+    public int getDefaultTetherOffloadDisabled() {
+        return DEFAULT_TETHER_OFFLOAD_DISABLED;
+    }
+
+    /** Configure offload management process. */
+    public boolean initOffloadConfig() {
+        return configOffload();
+    }
+
+    /** Initialize the tethering offload HAL. */
+    public boolean initOffloadControl(ControlCallback controlCb) {
+        mControlCallback = controlCb;
+
+        if (mOffloadControl == null) {
+            try {
+                mOffloadControl = IOffloadControl.getService();
+            } catch (RemoteException e) {
+                mLog.e("tethering offload control not supported: " + e);
+                return false;
+            }
+            if (mOffloadControl == null) {
+                mLog.e("tethering IOffloadControl.getService() returned null");
+                return false;
+            }
+        }
+
+        final String logmsg = String.format("initOffloadControl(%s)",
+                (controlCb == null) ? "null"
+                        : "0x" + Integer.toHexString(System.identityHashCode(controlCb)));
+
+        mTetheringOffloadCallback = new TetheringOffloadCallback(mHandler, mControlCallback, mLog);
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.initOffload(
+                    mTetheringOffloadCallback,
+                    (boolean success, String errMsg) -> {
+                        results.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    /** Stop IOffloadControl. */
+    public void stopOffloadControl() {
+        if (mOffloadControl != null) {
+            try {
+                mOffloadControl.stopOffload(
+                        (boolean success, String errMsg) -> {
+                            if (!success) mLog.e("stopOffload failed: " + errMsg);
+                        });
+            } catch (RemoteException e) {
+                mLog.e("failed to stopOffload: " + e);
+            }
+        }
+        mOffloadControl = null;
+        mTetheringOffloadCallback = null;
+        mControlCallback = null;
+        mLog.log("stopOffloadControl()");
+    }
+
+    /** Get Tx/Rx usage from last query. */
+    public ForwardedStats getForwardedStats(String upstream) {
+        final String logmsg = String.format("getForwardedStats(%s)",  upstream);
+
+        final ForwardedStats stats = new ForwardedStats();
+        try {
+            mOffloadControl.getForwardedStats(
+                    upstream,
+                    (long rxBytes, long txBytes) -> {
+                        stats.rxBytes = (rxBytes > 0) ? rxBytes : 0;
+                        stats.txBytes = (txBytes > 0) ? txBytes : 0;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return stats;
+        }
+
+        mLog.log(logmsg + YIELDS + stats);
+        return stats;
+    }
+
+    /** Set local prefixes to offload management process. */
+    public boolean setLocalPrefixes(ArrayList<String> localPrefixes) {
+        final String logmsg = String.format("setLocalPrefixes([%s])",
+                String.join(",", localPrefixes));
+
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.setLocalPrefixes(localPrefixes,
+                    (boolean success, String errMsg) -> {
+                        results.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    /** Set data limit value to offload management process. */
+    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.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    /** Set upstream parameters to offload management process. */
+    public boolean setUpstreamParameters(
+            String iface, String v4addr, String v4gateway, ArrayList<String> v6gws) {
+        iface = (iface != null) ? iface : NO_INTERFACE_NAME;
+        v4addr = (v4addr != null) ? v4addr : NO_IPV4_ADDRESS;
+        v4gateway = (v4gateway != null) ? v4gateway : NO_IPV4_GATEWAY;
+        v6gws = (v6gws != null) ? v6gws : new ArrayList<>();
+
+        final String logmsg = String.format("setUpstreamParameters(%s, %s, %s, [%s])",
+                iface, v4addr, v4gateway, String.join(",", v6gws));
+
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.setUpstreamParameters(
+                    iface, v4addr, v4gateway, v6gws,
+                    (boolean success, String errMsg) -> {
+                        results.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    /** Add downstream prefix to offload management process. */
+    public boolean addDownstreamPrefix(String ifname, String prefix) {
+        final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix);
+
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.addDownstream(ifname, prefix,
+                    (boolean success, String errMsg) -> {
+                        results.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    /** Remove downstream prefix from offload management process. */
+    public boolean removeDownstreamPrefix(String ifname, String prefix) {
+        final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix);
+
+        final CbResults results = new CbResults();
+        try {
+            mOffloadControl.removeDownstream(ifname, prefix,
+                    (boolean success, String errMsg) -> {
+                        results.mSuccess = success;
+                        results.mErrMsg = errMsg;
+                    });
+        } catch (RemoteException e) {
+            record(logmsg, e);
+            return false;
+        }
+
+        record(logmsg, results);
+        return results.mSuccess;
+    }
+
+    private void record(String msg, Throwable t) {
+        mLog.e(msg + YIELDS + "exception: " + t);
+    }
+
+    private void record(String msg, CbResults results) {
+        final String logmsg = msg + YIELDS + results;
+        if (!results.mSuccess) {
+            mLog.e(logmsg);
+        } else {
+            mLog.log(logmsg);
+        }
+    }
+
+    private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub {
+        public final Handler handler;
+        public final ControlCallback controlCb;
+        public final SharedLog log;
+
+        TetheringOffloadCallback(Handler h, ControlCallback cb, SharedLog sharedLog) {
+            handler = h;
+            controlCb = cb;
+            log = sharedLog;
+        }
+
+        @Override
+        public void onEvent(int event) {
+            handler.post(() -> {
+                switch (event) {
+                    case OffloadCallbackEvent.OFFLOAD_STARTED:
+                        controlCb.onStarted();
+                        break;
+                    case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR:
+                        controlCb.onStoppedError();
+                        break;
+                    case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED:
+                        controlCb.onStoppedUnsupported();
+                        break;
+                    case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE:
+                        controlCb.onSupportAvailable();
+                        break;
+                    case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED:
+                        controlCb.onStoppedLimitReached();
+                        break;
+                    default:
+                        log.e("Unsupported OffloadCallbackEvent: " + event);
+                }
+            });
+        }
+
+        @Override
+        public void updateTimeout(NatTimeoutUpdate params) {
+            handler.post(() -> {
+                controlCb.onNatTimeoutUpdate(
+                        networkProtocolToOsConstant(params.proto),
+                        params.src.addr, uint16(params.src.port),
+                        params.dst.addr, uint16(params.dst.port));
+            });
+        }
+    }
+
+    private static int networkProtocolToOsConstant(int proto) {
+        switch (proto) {
+            case NetworkProtocol.TCP: return OsConstants.IPPROTO_TCP;
+            case NetworkProtocol.UDP: return OsConstants.IPPROTO_UDP;
+            default:
+                // The caller checks this value and will log an error. Just make
+                // sure it won't collide with valid OsContants.IPPROTO_* values.
+                return -Math.abs(proto);
+        }
+    }
+
+    private static class CbResults {
+        boolean mSuccess;
+        String mErrMsg;
+
+        @Override
+        public String toString() {
+            if (mSuccess) {
+                return "ok";
+            } else {
+                return "fail: " + mErrMsg;
+            }
+        }
+    }
+}
diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp
index 7c06e5f..363be18 100644
--- a/Tethering/tests/unit/Android.bp
+++ b/Tethering/tests/unit/Android.bp
@@ -25,6 +25,7 @@
     static_libs: [
         "androidx.test.rules",
         "frameworks-base-testutils",
+        "net-tests-utils",
         "mockito-target-extended-minus-junit4",
         "TetheringApiCurrentLib",
         "testables",
@@ -46,6 +47,7 @@
     name: "tethering-tests-src",
     srcs: [
         "src/com/android/server/connectivity/tethering/EntitlementManagerTest.java",
+        "src/com/android/server/connectivity/tethering/OffloadControllerTest.java",
         "src/com/android/server/connectivity/tethering/TetheringConfigurationTest.java",
         "src/com/android/server/connectivity/tethering/UpstreamNetworkMonitorTest.java",
         "src/android/net/dhcp/DhcpServingParamsParcelExtTest.java",
diff --git a/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java
new file mode 100644
index 0000000..8574f54
--- /dev/null
+++ b/Tethering/tests/unit/src/com/android/server/connectivity/tethering/OffloadControllerTest.java
@@ -0,0 +1,730 @@
+/*
+ * 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 com.android.server.connectivity.tethering;
+
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.STATS_PER_IFACE;
+import static android.net.NetworkStats.STATS_PER_UID;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+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 com.android.testutils.MiscAssertsKt.assertContainsAll;
+import static com.android.testutils.MiscAssertsKt.assertThrows;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+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 static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+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.INetworkManagementService;
+import android.os.Looper;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.test.mock.MockContentResolver;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.testutils.HandlerUtilsKt;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class OffloadControllerTest {
+    private static final String RNDIS0 = "test_rndis0";
+    private static final String RMNET0 = "test_rmnet_data0";
+    private static final String WLAN0 = "test_wlan0";
+
+    private static final String IPV6_LINKLOCAL = "fe80::/64";
+    private static final String IPV6_DOC_PREFIX = "2001:db8::/64";
+    private static final String IPV6_DISCARD_PREFIX = "100::/64";
+    private static final String USB_PREFIX = "192.168.42.0/24";
+    private static final String WIFI_PREFIX = "192.168.43.0/24";
+    private static final long WAIT_FOR_IDLE_TIMEOUT = 2 * 1000;
+
+    @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 final ArgumentCaptor<OffloadHardwareInterface.ControlCallback> mControlCallbackCaptor =
+            ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class);
+    private MockContentResolver mContentResolver;
+
+    @Before public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo);
+        when(mContext.getPackageName()).thenReturn("OffloadControllerTest");
+        mContentResolver = new MockContentResolver(mContext);
+        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        FakeSettingsProvider.clearSettingsProvider();
+    }
+
+    @After public void tearDown() throws Exception {
+        FakeSettingsProvider.clearSettingsProvider();
+    }
+
+    private void setupFunctioningHardwareInterface() {
+        when(mHardware.initOffloadConfig()).thenReturn(true);
+        when(mHardware.initOffloadControl(mControlCallbackCaptor.capture()))
+                .thenReturn(true);
+        when(mHardware.setUpstreamParameters(anyString(), any(), any(), any())).thenReturn(true);
+        when(mHardware.getForwardedStats(any())).thenReturn(new ForwardedStats());
+        when(mHardware.setDataLimit(anyString(), anyLong())).thenReturn(true);
+    }
+
+    private void enableOffload() {
+        Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
+    }
+
+    private void waitForIdle() {
+        HandlerUtilsKt.waitForIdle(new Handler(Looper.getMainLooper()), WAIT_FOR_IDLE_TIMEOUT);
+    }
+
+    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;
+    }
+
+    @Test
+    public void testNoSettingsValueDefaultDisabledDoesNotStart() throws Exception {
+        setupFunctioningHardwareInterface();
+        when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(1);
+        assertThrows(SettingNotFoundException.class, () ->
+                Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
+        inOrder.verify(mHardware, never()).initOffloadConfig();
+        inOrder.verify(mHardware, never()).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testNoSettingsValueDefaultEnabledDoesStart() throws Exception {
+        setupFunctioningHardwareInterface();
+        when(mHardware.getDefaultTetherOffloadDisabled()).thenReturn(0);
+        assertThrows(SettingNotFoundException.class, () ->
+                Settings.Global.getInt(mContentResolver, TETHER_OFFLOAD_DISABLED));
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
+        inOrder.verify(mHardware, times(1)).initOffloadConfig();
+        inOrder.verify(mHardware, times(1)).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testSettingsAllowsStart() throws Exception {
+        setupFunctioningHardwareInterface();
+        Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
+        inOrder.verify(mHardware, times(1)).initOffloadConfig();
+        inOrder.verify(mHardware, times(1)).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testSettingsDisablesStart() throws Exception {
+        setupFunctioningHardwareInterface();
+        Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 1);
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
+        inOrder.verify(mHardware, never()).initOffloadConfig();
+        inOrder.verify(mHardware, never()).initOffloadControl(anyObject());
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testSetUpstreamLinkPropertiesWorking() throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).getDefaultTetherOffloadDisabled();
+        inOrder.verify(mHardware, times(1)).initOffloadConfig();
+        inOrder.verify(mHardware, times(1)).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+
+        // In reality, the UpstreamNetworkMonitor would have passed down to us
+        // a covering set of local prefixes representing a minimum essential
+        // set plus all the prefixes on networks with network agents.
+        //
+        // We simulate that there, and then add upstream elements one by one
+        // and watch what happens.
+        final Set<IpPrefix> minimumLocalPrefixes = new HashSet<>();
+        for (String s : new String[]{
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) {
+            minimumLocalPrefixes.add(new IpPrefix(s));
+        }
+        offload.setLocalPrefixes(minimumLocalPrefixes);
+        inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
+        ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
+        assertEquals(4, localPrefixes.size());
+        assertContainsAll(localPrefixes,
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
+        inOrder.verifyNoMoreInteractions();
+
+        offload.setUpstreamLinkProperties(null);
+        // No change in local addresses means no call to setLocalPrefixes().
+        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+        // This LinkProperties value does not differ from the default upstream.
+        // There should be no extraneous call to setUpstreamParameters().
+        inOrder.verify(mHardware, never()).setUpstreamParameters(
+                anyObject(), anyObject(), anyObject(), anyObject());
+        inOrder.verifyNoMoreInteractions();
+
+        final LinkProperties lp = new LinkProperties();
+
+        final String testIfName = "rmnet_data17";
+        lp.setInterfaceName(testIfName);
+        offload.setUpstreamLinkProperties(lp);
+        // No change in local addresses means no call to setLocalPrefixes().
+        inOrder.verify(mHardware, never()).setLocalPrefixes(mStringArrayCaptor.capture());
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(null), eq(null), eq(null));
+        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv4Addr = "192.0.2.5";
+        final String linkAddr = ipv4Addr + "/24";
+        lp.addLinkAddress(new LinkAddress(linkAddr));
+        lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24")));
+        offload.setUpstreamLinkProperties(lp);
+        // IPv4 prefixes and addresses on the upstream are simply left as whole
+        // prefixes (already passed in from UpstreamNetworkMonitor code). If a
+        // tethering client sends traffic to the IPv4 default router or other
+        // clients on the upstream this will not be hardware-forwarded, and that
+        // should be fine for now. Ergo: no change in local addresses, no call
+        // to setLocalPrefixes().
+        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.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv4Gateway = "192.0.2.1";
+        lp.addRoute(new RouteInfo(InetAddress.getByName(ipv4Gateway)));
+        offload.setUpstreamLinkProperties(lp);
+        // No change in local addresses means no call to setLocalPrefixes().
+        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.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv6Gw1 = "fe80::cafe";
+        lp.addRoute(new RouteInfo(InetAddress.getByName(ipv6Gw1)));
+        offload.setUpstreamLinkProperties(lp);
+        // No change in local addresses means no call to setLocalPrefixes().
+        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, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        final String ipv6Gw2 = "fe80::d00d";
+        lp.addRoute(new RouteInfo(InetAddress.getByName(ipv6Gw2)));
+        offload.setUpstreamLinkProperties(lp);
+        // No change in local addresses means no call to setLocalPrefixes().
+        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));
+        assertTrue(v6gws.contains(ipv6Gw2));
+        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        final LinkProperties stacked = new LinkProperties();
+        stacked.setInterfaceName("stacked");
+        stacked.addLinkAddress(new LinkAddress("192.0.2.129/25"));
+        stacked.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+        stacked.addRoute(new RouteInfo(InetAddress.getByName("fe80::bad:f00")));
+        assertTrue(lp.addStackedLink(stacked));
+        offload.setUpstreamLinkProperties(lp);
+        // No change in local addresses means no call to setLocalPrefixes().
+        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));
+        assertTrue(v6gws.contains(ipv6Gw2));
+        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        // Add in some IPv6 upstream info. When there is a tethered downstream
+        // making use of the IPv6 prefix we would expect to see the /64 route
+        // removed from "local prefixes" and /128s added for the upstream IPv6
+        // addresses.  This is not yet implemented, and for now we simply
+        // expect to see these /128s.
+        lp.addRoute(new RouteInfo(new IpPrefix("2001:db8::/64")));
+        // "2001:db8::/64" plus "assigned" ASCII in hex
+        lp.addLinkAddress(new LinkAddress("2001:db8::6173:7369:676e:6564/64"));
+        // "2001:db8::/64" plus "random" ASCII in hex
+        lp.addLinkAddress(new LinkAddress("2001:db8::7261:6e64:6f6d/64"));
+        offload.setUpstreamLinkProperties(lp);
+        inOrder.verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
+        localPrefixes = mStringArrayCaptor.getValue();
+        assertEquals(6, localPrefixes.size());
+        assertContainsAll(localPrefixes,
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64",
+                "2001:db8::6173:7369:676e:6564/128", "2001:db8::7261:6e64:6f6d/128");
+        // The relevant parts of the LinkProperties have not changed, but at the
+        // moment we do not de-dup upstream LinkProperties this carefully.
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(testIfName), eq(ipv4Addr), eq(ipv4Gateway), mStringArrayCaptor.capture());
+        v6gws = mStringArrayCaptor.getValue();
+        assertEquals(2, v6gws.size());
+        assertTrue(v6gws.contains(ipv6Gw1));
+        assertTrue(v6gws.contains(ipv6Gw2));
+        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(testIfName));
+        inOrder.verify(mHardware, times(1)).setDataLimit(eq(testIfName), eq(Long.MAX_VALUE));
+        inOrder.verifyNoMoreInteractions();
+
+        // Completely identical LinkProperties updates are de-duped.
+        offload.setUpstreamLinkProperties(lp);
+        // This LinkProperties value does not differ from the default upstream.
+        // There should be no extraneous call to setUpstreamParameters().
+        inOrder.verify(mHardware, never()).setUpstreamParameters(
+                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);
+    }
+
+    @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);
+
+        InOrder inOrder = inOrder(mHardware);
+
+        final LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(ethernetIface);
+        offload.setUpstreamLinkProperties(lp);
+        // Previous upstream was null, so no stats are fetched.
+        inOrder.verify(mHardware, never()).getForwardedStats(any());
+
+        lp.setInterfaceName(mobileIface);
+        offload.setUpstreamLinkProperties(lp);
+        // Expect that we fetch stats from the previous upstream.
+        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface));
+
+        lp.setInterfaceName(ethernetIface);
+        offload.setUpstreamLinkProperties(lp);
+        // Expect that we fetch stats from the previous upstream.
+        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(mobileIface));
+
+        ethernetStats = new ForwardedStats();
+        ethernetStats.rxBytes = 100000;
+        ethernetStats.txBytes = 100000;
+        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(ethernetStats);
+        offload.setUpstreamLinkProperties(null);
+        // Expect that we first clear the HAL's upstream parameters.
+        inOrder.verify(mHardware, times(1)).setUpstreamParameters(
+                eq(""), eq("0.0.0.0"), eq("0.0.0.0"), eq(null));
+        // Expect that we fetch stats from the previous upstream.
+        inOrder.verify(mHardware, times(1)).getForwardedStats(eq(ethernetIface));
+
+        ITetheringStatsProvider provider = mTetherStatsProviderCaptor.getValue();
+        NetworkStats stats = provider.getTetherStats(STATS_PER_IFACE);
+        NetworkStats perUidStats = provider.getTetherStats(STATS_PER_UID);
+        waitForIdle();
+        // There is no current upstream, so no stats are fetched.
+        inOrder.verify(mHardware, never()).getForwardedStats(any());
+        inOrder.verifyNoMoreInteractions();
+
+        assertEquals(2, stats.size());
+        assertEquals(2, perUidStats.size());
+
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < stats.size(); i++) {
+            assertEquals(UID_ALL, stats.getValues(i, entry).uid);
+            assertEquals(UID_TETHERING, perUidStats.getValues(i, entry).uid);
+        }
+
+        int ethernetPosition = ethernetIface.equals(stats.getValues(0, entry).iface) ? 0 : 1;
+        int mobilePosition = 1 - ethernetPosition;
+
+        entry = stats.getValues(mobilePosition, entry);
+        assertNetworkStats(mobileIface, mobileStats, entry);
+        entry = perUidStats.getValues(mobilePosition, entry);
+        assertNetworkStats(mobileIface, mobileStats, entry);
+
+        ethernetStats.rxBytes = 12345 + 100000;
+        ethernetStats.txBytes = 54321 + 100000;
+        entry = stats.getValues(ethernetPosition, entry);
+        assertNetworkStats(ethernetIface, ethernetStats, entry);
+        entry = perUidStats.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).getForwardedStats(ethernetIface);
+        inOrder.verify(mHardware).stopOffloadControl();
+    }
+
+    @Test
+    public void testDataLimitCallback() throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+        callback.onStoppedLimitReached();
+        verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue());
+    }
+
+    @Test
+    public void testAddRemoveDownstreams() throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        final InOrder inOrder = inOrder(mHardware);
+        inOrder.verify(mHardware, times(1)).initOffloadConfig();
+        inOrder.verify(mHardware, times(1)).initOffloadControl(
+                any(OffloadHardwareInterface.ControlCallback.class));
+        inOrder.verifyNoMoreInteractions();
+
+        // Tethering makes several calls to setLocalPrefixes() before add/remove
+        // downstream calls are made. This is not tested here; only the behavior
+        // of notifyDownstreamLinkProperties() and removeDownstreamInterface()
+        // are tested.
+
+        // [1] USB tethering is started.
+        final LinkProperties usbLinkProperties = new LinkProperties();
+        usbLinkProperties.setInterfaceName(RNDIS0);
+        usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(USB_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, USB_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+
+        // [2] Routes for IPv6 link-local prefixes should never be added.
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_LINKLOCAL)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
+        inOrder.verifyNoMoreInteractions();
+
+        // [3] Add an IPv6 prefix for good measure. Only new offload-able
+        // prefixes should be passed to the HAL.
+        usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::1/64"));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+
+        // [4] Adding addresses doesn't affect notifyDownstreamLinkProperties().
+        // The address is passed in by a separate setLocalPrefixes() invocation.
+        usbLinkProperties.addLinkAddress(new LinkAddress("2001:db8::2/64"));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, never()).addDownstreamPrefix(eq(RNDIS0), anyString());
+
+        // [5] Differences in local routes are converted into addDownstream()
+        // and removeDownstream() invocations accordingly.
+        usbLinkProperties.removeRoute(new RouteInfo(new IpPrefix(IPV6_DOC_PREFIX), null, RNDIS0));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_DISCARD_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DOC_PREFIX);
+        inOrder.verify(mHardware, times(1)).addDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+
+        // [6] Removing a downstream interface which was never added causes no
+        // interactions with the HAL.
+        offload.removeDownstreamInterface(WLAN0);
+        inOrder.verifyNoMoreInteractions();
+
+        // [7] Removing an active downstream removes all remaining prefixes.
+        offload.removeDownstreamInterface(RNDIS0);
+        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, USB_PREFIX);
+        inOrder.verify(mHardware, times(1)).removeDownstreamPrefix(RNDIS0, IPV6_DISCARD_PREFIX);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    public void testControlCallbackOnStoppedUnsupportedFetchesAllStats() throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        // Pretend to set a few different upstreams (only the interface name
+        // matters for this test; we're ignoring IP and route information).
+        final LinkProperties upstreamLp = new LinkProperties();
+        for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) {
+            upstreamLp.setInterfaceName(ifname);
+            offload.setUpstreamLinkProperties(upstreamLp);
+        }
+
+        // Clear invocation history, especially the getForwardedStats() calls
+        // that happen with setUpstreamParameters().
+        clearInvocations(mHardware);
+
+        OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+        callback.onStoppedUnsupported();
+
+        // Verify forwarded stats behaviour.
+        verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
+        verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
+        verifyNoMoreInteractions(mHardware);
+        verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue());
+        verifyNoMoreInteractions(mNMService);
+    }
+
+    @Test
+    public void testControlCallbackOnSupportAvailableFetchesAllStatsAndPushesAllParameters()
+            throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        // Pretend to set a few different upstreams (only the interface name
+        // matters for this test; we're ignoring IP and route information).
+        final LinkProperties upstreamLp = new LinkProperties();
+        for (String ifname : new String[]{RMNET0, WLAN0, RMNET0}) {
+            upstreamLp.setInterfaceName(ifname);
+            offload.setUpstreamLinkProperties(upstreamLp);
+        }
+
+        // Pretend that some local prefixes and downstreams have been added
+        // (and removed, for good measure).
+        final Set<IpPrefix> minimumLocalPrefixes = new HashSet<>();
+        for (String s : new String[]{
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64"}) {
+            minimumLocalPrefixes.add(new IpPrefix(s));
+        }
+        offload.setLocalPrefixes(minimumLocalPrefixes);
+
+        final LinkProperties usbLinkProperties = new LinkProperties();
+        usbLinkProperties.setInterfaceName(RNDIS0);
+        usbLinkProperties.addLinkAddress(new LinkAddress("192.168.42.1/24"));
+        usbLinkProperties.addRoute(new RouteInfo(new IpPrefix(USB_PREFIX)));
+        offload.notifyDownstreamLinkProperties(usbLinkProperties);
+
+        final LinkProperties wifiLinkProperties = new LinkProperties();
+        wifiLinkProperties.setInterfaceName(WLAN0);
+        wifiLinkProperties.addLinkAddress(new LinkAddress("192.168.43.1/24"));
+        wifiLinkProperties.addRoute(new RouteInfo(new IpPrefix(WIFI_PREFIX)));
+        wifiLinkProperties.addRoute(new RouteInfo(new IpPrefix(IPV6_LINKLOCAL)));
+        // Use a benchmark prefix (RFC 5180 + erratum), since the documentation
+        // prefix is included in the excluded prefix list.
+        wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::1/64"));
+        wifiLinkProperties.addLinkAddress(new LinkAddress("2001:2::2/64"));
+        wifiLinkProperties.addRoute(new RouteInfo(new IpPrefix("2001:2::/64")));
+        offload.notifyDownstreamLinkProperties(wifiLinkProperties);
+
+        offload.removeDownstreamInterface(RNDIS0);
+
+        // Clear invocation history, especially the getForwardedStats() calls
+        // that happen with setUpstreamParameters().
+        clearInvocations(mHardware);
+
+        OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
+        callback.onSupportAvailable();
+
+        // Verify forwarded stats behaviour.
+        verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
+        verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
+        verify(mNMService, times(1)).tetherLimitReached(mTetherStatsProviderCaptor.getValue());
+        verifyNoMoreInteractions(mNMService);
+
+        // TODO: verify local prefixes and downstreams are also pushed to the HAL.
+        verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
+        ArrayList<String> localPrefixes = mStringArrayCaptor.getValue();
+        assertEquals(4, localPrefixes.size());
+        assertContainsAll(localPrefixes,
+                // TODO: The logic to find and exclude downstream IP prefixes
+                // is currently in Tethering's OffloadWrapper but must be moved
+                // into OffloadController proper. After this, also check for:
+                //     "192.168.43.1/32", "2001:2::1/128", "2001:2::2/128"
+                "127.0.0.0/8", "192.0.2.0/24", "fe80::/64", "2001:db8::/64");
+        verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "192.168.43.0/24");
+        verify(mHardware, times(1)).addDownstreamPrefix(WLAN0, "2001:2::/64");
+        verify(mHardware, times(1)).setUpstreamParameters(eq(RMNET0), any(), any(), any());
+        verify(mHardware, times(1)).setDataLimit(eq(RMNET0), anyLong());
+        verifyNoMoreInteractions(mHardware);
+    }
+
+}