Merge "Override tethering module APK-in-APEX for Go variant" into rvc-dev
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index 83f761f..cbc5e14 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -27,7 +27,7 @@
         "androidx.annotation_annotation",
         "netd_aidl_interface-V3-java",
         "netlink-client",
-        "networkstack-aidl-interfaces-unstable-java",
+        "networkstack-aidl-interfaces-java",
         "android.hardware.tetheroffload.config-V1.0-java",
         "android.hardware.tetheroffload.control-V1.0-java",
         "net-utils-framework-common",
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index ee6b9f1..d029d2b 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -67,6 +67,7 @@
 stubs_defaults {
     name: "framework-tethering-stubs-defaults",
     srcs: [":framework-tethering-srcs"],
+    dist: { dest: "framework-tethering.txt" },
 }
 
 filegroup {
@@ -93,6 +94,15 @@
         "framework-module-stubs-defaults-publicapi",
         "framework-tethering-stubs-defaults",
     ],
+    check_api: {
+        last_released: {
+            api_file: ":framework-tethering.api.public.latest",
+            removed_api_file: ":framework-tethering-removed.api.public.latest",
+        },
+        api_lint: {
+            new_since: ":framework-tethering.api.public.latest",
+        },
+    },
 }
 
 droidstubs {
@@ -101,6 +111,15 @@
         "framework-module-stubs-defaults-systemapi",
         "framework-tethering-stubs-defaults",
     ],
+    check_api: {
+        last_released: {
+            api_file: ":framework-tethering.api.system.latest",
+            removed_api_file: ":framework-tethering-removed.api.system.latest",
+        },
+        api_lint: {
+            new_since: ":framework-tethering.api.system.latest",
+        },
+    },
 }
 
 droidstubs {
@@ -109,6 +128,15 @@
         "framework-module-api-defaults-module_libs_api",
         "framework-tethering-stubs-defaults",
     ],
+    check_api: {
+        last_released: {
+            api_file: ":framework-tethering.api.module-lib.latest",
+            removed_api_file: ":framework-tethering-removed.api.module-lib.latest",
+        },
+        api_lint: {
+            new_since: ":framework-tethering.api.module-lib.latest",
+        },
+    },
 }
 
 droidstubs {
@@ -123,16 +151,19 @@
     name: "framework-tethering-stubs-publicapi",
     srcs: [":framework-tethering-stubs-srcs-publicapi"],
     defaults: ["framework-module-stubs-lib-defaults-publicapi"],
+    dist: { dest: "framework-tethering.jar" },
 }
 
 java_library {
     name: "framework-tethering-stubs-systemapi",
     srcs: [":framework-tethering-stubs-srcs-systemapi"],
     defaults: ["framework-module-stubs-lib-defaults-systemapi"],
+    dist: { dest: "framework-tethering.jar" },
 }
 
 java_library {
     name: "framework-tethering-stubs-module_libs_api",
     srcs: [":framework-tethering-stubs-srcs-module_libs_api"],
-    defaults: ["framework-module-stubs-lib-defaults-systemapi"],
+    defaults: ["framework-module-stubs-lib-defaults-module_libs_api"],
+    dist: { dest: "framework-tethering.jar" },
 }
diff --git a/Tethering/jarjar-rules.txt b/Tethering/jarjar-rules.txt
index c6efa41..e90a2cc 100644
--- a/Tethering/jarjar-rules.txt
+++ b/Tethering/jarjar-rules.txt
@@ -8,7 +8,6 @@
 rule com.android.internal.util.IndentingPrintWriter.java* com.android.networkstack.tethering.util.IndentingPrintWriter.java@1
 rule com.android.internal.util.IState.java* com.android.networkstack.tethering.util.IState.java@1
 rule com.android.internal.util.MessageUtils* com.android.networkstack.tethering.util.MessageUtils@1
-rule com.android.internal.util.Preconditions* com.android.networkstack.tethering.util.Preconditions@1
 rule com.android.internal.util.State* com.android.networkstack.tethering.util.State@1
 rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1
 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1
diff --git a/Tethering/res/values-mcc310-mnc004-ne/strings.xml b/Tethering/res/values-mcc310-mnc004-ne/strings.xml
index 2a73300..d074f15 100644
--- a/Tethering/res/values-mcc310-mnc004-ne/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ne/strings.xml
@@ -16,13 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- no translation found for no_upstream_notification_title (5030042590486713460) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_message (3843613362272973447) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_disable_button (6385491461813507624) -->
-    <skip />
-    <!-- no translation found for upstream_roaming_notification_title (3015912166812283303) -->
-    <skip />
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"यन्त्रहरू कनेक्ट गर्न सकिएन"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"टेदरिङ निष्क्रिय पार्नुहोस्"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"हटस्पट वा टेदरिङ सक्रिय छ"</string>
     <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-ta/strings.xml b/Tethering/res/values-mcc310-mnc004-ta/strings.xml
index ea04821..f4b15aa 100644
--- a/Tethering/res/values-mcc310-mnc004-ta/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-ta/strings.xml
@@ -16,13 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- no translation found for no_upstream_notification_title (5030042590486713460) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_message (3843613362272973447) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_disable_button (6385491461813507624) -->
-    <skip />
-    <!-- no translation found for upstream_roaming_notification_title (3015912166812283303) -->
-    <skip />
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
+    <string name="no_upstream_notification_message" msgid="3843613362272973447">"சாதனங்களால் இணைய முடியவில்லை"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"இணைப்பு முறையை ஆஃப் செய்"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது"</string>
     <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
 </resources>
diff --git a/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
index 05b9069..528a1e5 100644
--- a/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
+++ b/Tethering/res/values-mcc310-mnc004-zh-rTW/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="5030042590486713460">"無法透過數據連線連上網際網路"</string>
+    <string name="no_upstream_notification_title" msgid="5030042590486713460">"無法透過網路共用連上網際網路"</string>
     <string name="no_upstream_notification_message" msgid="3843613362272973447">"裝置無法連線"</string>
-    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"關閉數據連線"</string>
-    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"無線基地台或數據連線已開啟"</string>
+    <string name="no_upstream_notification_disable_button" msgid="6385491461813507624">"關閉網路共用"</string>
+    <string name="upstream_roaming_notification_title" msgid="3015912166812283303">"無線基地台或網路共用已開啟"</string>
     <string name="upstream_roaming_notification_message" msgid="6724434706748439902">"使用漫遊服務可能須支付額外費用"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ne/strings.xml b/Tethering/res/values-mcc311-mnc480-ne/strings.xml
index 617c50d..1503244 100644
--- a/Tethering/res/values-mcc311-mnc480-ne/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ne/strings.xml
@@ -16,13 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- no translation found for no_upstream_notification_title (611650570559011140) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_message (6508394877641864863) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_disable_button (7609346639290990508) -->
-    <skip />
-    <!-- no translation found for upstream_roaming_notification_title (6032901176124830787) -->
-    <skip />
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"टेदरिङमार्फत इन्टरनेट कनेक्सन प्राप्त हुन सकेन"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"यन्त्रहरू कनेक्ट गर्न सकिएन"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"टेदरिङ निष्क्रिय पार्नुहोस्"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"हटस्पट वा टेदरिङ सक्रिय छ"</string>
     <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"रोमिङ सेवा प्रयोग गर्दा अतिरिक्त शुल्क लाग्न सक्छ"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-ta/strings.xml b/Tethering/res/values-mcc311-mnc480-ta/strings.xml
index 0e43759..2ea2467 100644
--- a/Tethering/res/values-mcc311-mnc480-ta/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-ta/strings.xml
@@ -16,13 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <!-- no translation found for no_upstream_notification_title (611650570559011140) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_message (6508394877641864863) -->
-    <skip />
-    <!-- no translation found for no_upstream_notification_disable_button (7609346639290990508) -->
-    <skip />
-    <!-- no translation found for upstream_roaming_notification_title (6032901176124830787) -->
-    <skip />
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"இணைப்பு முறைக்கு இணைய இணைப்பு இல்லை"</string>
+    <string name="no_upstream_notification_message" msgid="6508394877641864863">"சாதனங்களால் இணைய முடியவில்லை"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"இணைப்பு முறையை ஆஃப் செய்"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"ஹாட்ஸ்பாட் அல்லது இணைப்பு முறை ஆன் செய்யப்பட்டுள்ளது"</string>
     <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"ரோமிங்கின்போது கூடுதல் கட்டணங்கள் விதிக்கப்படக்கூடும்"</string>
 </resources>
diff --git a/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
index ea01b94..cd653df 100644
--- a/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
+++ b/Tethering/res/values-mcc311-mnc480-zh-rTW/strings.xml
@@ -16,9 +16,9 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="no_upstream_notification_title" msgid="611650570559011140">"無法透過數據連線連上網際網路"</string>
+    <string name="no_upstream_notification_title" msgid="611650570559011140">"無法透過網路共用連上網際網路"</string>
     <string name="no_upstream_notification_message" msgid="6508394877641864863">"裝置無法連線"</string>
-    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"關閉數據連線"</string>
-    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"無線基地台或數據連線已開啟"</string>
+    <string name="no_upstream_notification_disable_button" msgid="7609346639290990508">"關閉網路共用"</string>
+    <string name="upstream_roaming_notification_title" msgid="6032901176124830787">"無線基地台或網路共用已開啟"</string>
     <string name="upstream_roaming_notification_message" msgid="7599056263326217523">"使用漫遊服務可能須支付額外費用"</string>
 </resources>
diff --git a/Tethering/res/values-zh-rTW/strings.xml b/Tethering/res/values-zh-rTW/strings.xml
index 9d738a7..50a50bf 100644
--- a/Tethering/res/values-zh-rTW/strings.xml
+++ b/Tethering/res/values-zh-rTW/strings.xml
@@ -16,11 +16,11 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="tethered_notification_title" msgid="6426563586025792944">"數據連線或無線基地台已啟用"</string>
+    <string name="tethered_notification_title" msgid="6426563586025792944">"網路共用或無線基地台已啟用"</string>
     <string name="tethered_notification_message" msgid="64800879503420696">"輕觸即可進行設定。"</string>
-    <string name="disable_tether_notification_title" msgid="3004509127903564191">"數據連線已停用"</string>
+    <string name="disable_tether_notification_title" msgid="3004509127903564191">"網路共用已停用"</string>
     <string name="disable_tether_notification_message" msgid="6717523799293901476">"詳情請洽你的管理員"</string>
-    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"無線基地台與數據連線狀態"</string>
+    <string name="notification_channel_tethering_status" msgid="2663463891530932727">"無線基地台與網路共用狀態"</string>
     <string name="no_upstream_notification_title" msgid="1204601824631788482"></string>
     <string name="no_upstream_notification_message" msgid="8586582938243032621"></string>
     <string name="no_upstream_notification_disable_button" msgid="8800919436924640822"></string>
diff --git a/Tethering/res/values/config.xml b/Tethering/res/values/config.xml
index 83c99d2..9dda716 100644
--- a/Tethering/res/values/config.xml
+++ b/Tethering/res/values/config.xml
@@ -64,6 +64,13 @@
     <string-array translatable="false" name="config_tether_dhcp_range">
     </string-array>
 
+    <!-- Used to config periodic polls tether offload stats from tethering offload HAL to make the
+     data warnings work. 5000(ms) by default. If the device doesn't want to poll tether
+     offload stats, this should be -1. Note that this setting could be override by
+     runtime resource overlays.
+    -->
+    <integer translatable="false" name="config_tether_offload_poll_interval">5000</integer>
+
     <!-- Array of ConnectivityManager.TYPE_{BLUETOOTH, ETHERNET, MOBILE, MOBILE_DUN, MOBILE_HIPRI,
          WIFI} values allowable for tethering.
 
diff --git a/Tethering/res/values/overlayable.xml b/Tethering/res/values/overlayable.xml
index 16ae8ad..4c78a74 100644
--- a/Tethering/res/values/overlayable.xml
+++ b/Tethering/res/values/overlayable.xml
@@ -24,6 +24,7 @@
             <item type="array" name="config_tether_bluetooth_regexs"/>
             <item type="array" name="config_tether_dhcp_range"/>
             <item type="bool" name="config_tether_enable_legacy_dhcp_server"/>
+            <item type="integer" name="config_tether_offload_poll_interval"/>
             <item type="array" name="config_tether_upstream_types"/>
             <item type="bool" name="config_tether_upstream_automatic"/>
             <!-- Configuration values for tethering entitlement check -->
diff --git a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
index 82a26be..4f8ad8a 100644
--- a/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
+++ b/Tethering/src/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -169,7 +169,7 @@
      * <p>If not set, the default value is null.
      */
     public DhcpServingParamsParcelExt setSingleClientAddr(@Nullable Inet4Address clientAddr) {
-        this.clientAddr = clientAddr == null ? 0 : inet4AddressToIntHTH(clientAddr);
+        this.singleClientAddr = clientAddr == null ? 0 : inet4AddressToIntHTH(clientAddr);
         return this;
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadController.java b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
index c007c17..88c77b0 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadController.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadController.java
@@ -23,8 +23,11 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
 import static android.provider.Settings.Global.TETHER_OFFLOAD_DISABLED;
 
+import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.usage.NetworkStatsManager;
@@ -115,11 +118,31 @@
     // includes upstream interfaces that have a quota set.
     private HashMap<String, Long> mInterfaceQuotas = new HashMap<>();
 
+    // Tracking remaining alert quota. Unlike limit quota is subject to interface, the alert
+    // quota is interface independent and global for tether offload. Note that this is only
+    // accessed on the handler thread and in the constructor.
+    private long mRemainingAlertQuota = QUOTA_UNLIMITED;
+    // Runnable that used to schedule the next stats poll.
+    private final Runnable mScheduledPollingTask = () -> {
+        updateStatsForCurrentUpstream();
+        maybeSchedulePollingStats();
+    };
+
     private int mNatUpdateCallbacksReceived;
     private int mNatUpdateNetlinkErrors;
 
+    @NonNull
+    private final Dependencies mDeps;
+
+    // TODO: Put more parameters in constructor into dependency object.
+    interface Dependencies {
+        @NonNull
+        TetheringConfiguration getTetherConfig();
+    }
+
     public OffloadController(Handler h, OffloadHardwareInterface hwi,
-            ContentResolver contentResolver, NetworkStatsManager nsm, SharedLog log) {
+            ContentResolver contentResolver, NetworkStatsManager nsm, SharedLog log,
+            @NonNull Dependencies deps) {
         mHandler = h;
         mHwInterface = hwi;
         mContentResolver = contentResolver;
@@ -135,6 +158,7 @@
             provider = null;
         }
         mStatsProvider = provider;
+        mDeps = deps;
     }
 
     /** Start hardware offload. */
@@ -240,6 +264,7 @@
             mLog.log("tethering offload started");
             mNatUpdateCallbacksReceived = 0;
             mNatUpdateNetlinkErrors = 0;
+            maybeSchedulePollingStats();
         }
         return isStarted;
     }
@@ -255,6 +280,9 @@
         mHwInterface.stopOffloadControl();
         mControlInitialized = false;
         mConfigInitialized = false;
+        if (mHandler.hasCallbacks(mScheduledPollingTask)) {
+            mHandler.removeCallbacks(mScheduledPollingTask);
+        }
         if (wasStarted) mLog.log("tethering offload stopped");
     }
 
@@ -345,6 +373,11 @@
         @Override
         public void onSetAlert(long quotaBytes) {
             // TODO: Ask offload HAL to notify alert without stopping traffic.
+            // Post it to handler thread since it access remaining quota bytes.
+            mHandler.post(() -> {
+                updateAlertQuota(quotaBytes);
+                maybeSchedulePollingStats();
+            });
         }
     }
 
@@ -366,15 +399,70 @@
         // the stats for each interface, and does not observe partial writes where rxBytes is
         // updated and txBytes is not.
         ForwardedStats diff = mHwInterface.getForwardedStats(iface);
+        final long usedAlertQuota = diff.rxBytes + diff.txBytes;
         ForwardedStats base = mForwardedStats.get(iface);
         if (base != null) {
             diff.add(base);
         }
+
+        // Update remaining alert quota if it is still positive.
+        if (mRemainingAlertQuota > 0 && usedAlertQuota > 0) {
+            // Trim to zero if overshoot.
+            final long newQuota = Math.max(mRemainingAlertQuota - usedAlertQuota, 0);
+            updateAlertQuota(newQuota);
+        }
+
         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.
     }
 
+    /**
+     * Update remaining alert quota, fire the {@link NetworkStatsProvider#notifyAlertReached()}
+     * callback when it reaches zero. This can be invoked either from service setting the alert, or
+     * {@code maybeUpdateStats} when updating stats. Note that this can be only called on
+     * handler thread.
+     *
+     * @param newQuota non-negative value to indicate the new quota, or
+     *                 {@link NetworkStatsProvider#QUOTA_UNLIMITED} to indicate there is no
+     *                 quota.
+     */
+    private void updateAlertQuota(long newQuota) {
+        if (newQuota < QUOTA_UNLIMITED) {
+            throw new IllegalArgumentException("invalid quota value " + newQuota);
+        }
+        if (mRemainingAlertQuota == newQuota) return;
+
+        mRemainingAlertQuota = newQuota;
+        if (mRemainingAlertQuota == 0) {
+            mLog.i("notifyAlertReached");
+            if (mStatsProvider != null) mStatsProvider.notifyAlertReached();
+        }
+    }
+
+    /**
+     * Schedule polling if needed, this will be stopped if offload has been
+     * stopped or remaining quota reaches zero or upstream is empty.
+     * Note that this can be only called on handler thread.
+     */
+    private void maybeSchedulePollingStats() {
+        if (!isPollingStatsNeeded()) return;
+
+        if (mHandler.hasCallbacks(mScheduledPollingTask)) {
+            mHandler.removeCallbacks(mScheduledPollingTask);
+        }
+        mHandler.postDelayed(mScheduledPollingTask,
+                mDeps.getTetherConfig().getOffloadPollInterval());
+    }
+
+    private boolean isPollingStatsNeeded() {
+        return started() && mRemainingAlertQuota > 0
+                && !TextUtils.isEmpty(currentUpstreamInterface())
+                && mDeps.getTetherConfig() != null
+                && mDeps.getTetherConfig().getOffloadPollInterval()
+                >= DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
+    }
+
     private boolean maybeUpdateDataLimit(String iface) {
         // setDataLimit may only be called while offload is occurring on this upstream.
         if (!started() || !TextUtils.equals(iface, currentUpstreamInterface())) {
@@ -414,6 +502,8 @@
         final String iface = currentUpstreamInterface();
         if (!TextUtils.isEmpty(iface)) mForwardedStats.putIfAbsent(iface, EMPTY_STATS);
 
+        maybeSchedulePollingStats();
+
         // 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
diff --git a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
index 55344fc..293f8ea 100644
--- a/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
+++ b/Tethering/src/com/android/networkstack/tethering/OffloadHardwareInterface.java
@@ -41,6 +41,7 @@
 import java.net.SocketAddress;
 import java.net.SocketException;
 import java.util.ArrayList;
+import java.util.NoSuchElementException;
 
 
 /**
@@ -143,7 +144,7 @@
         IOffloadConfig offloadConfig;
         try {
             offloadConfig = IOffloadConfig.getService(true /*retry*/);
-        } catch (RemoteException e) {
+        } catch (RemoteException | NoSuchElementException e) {
             mLog.e("getIOffloadConfig error " + e);
             return false;
         }
@@ -239,8 +240,8 @@
 
         if (mOffloadControl == null) {
             try {
-                mOffloadControl = IOffloadControl.getService();
-            } catch (RemoteException e) {
+                mOffloadControl = IOffloadControl.getService(true /*retry*/);
+            } catch (RemoteException | NoSuchElementException e) {
                 mLog.e("tethering offload control not supported: " + e);
                 return false;
             }
@@ -307,7 +308,6 @@
             return stats;
         }
 
-        mLog.log(logmsg + YIELDS + stats);
         return stats;
     }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 4e16c49..b2a43c4 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -62,7 +62,6 @@
 
 import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE;
 
-import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothPan;
 import android.bluetooth.BluetoothProfile;
@@ -268,12 +267,15 @@
         mTetherMasterSM = new TetherMasterSM("TetherMaster", mLooper, deps);
         mTetherMasterSM.start();
 
-        final NetworkStatsManager statsManager =
-                (NetworkStatsManager) mContext.getSystemService(Context.NETWORK_STATS_SERVICE);
         mHandler = mTetherMasterSM.getHandler();
-        mOffloadController = new OffloadController(mHandler,
-                mDeps.getOffloadHardwareInterface(mHandler, mLog), mContext.getContentResolver(),
-                statsManager, mLog);
+        mOffloadController = mDeps.getOffloadController(mHandler, mLog,
+                new OffloadController.Dependencies() {
+
+                    @Override
+                    public TetheringConfiguration getTetherConfig() {
+                        return mConfig;
+                    }
+                });
         mUpstreamNetworkMonitor = mDeps.getUpstreamNetworkMonitor(mContext, mTetherMasterSM, mLog,
                 TetherMasterSM.EVENT_UPSTREAM_CALLBACK);
         mForwardedDownstreams = new LinkedHashSet<>();
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
index aeac437..9d4e747 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringConfiguration.java
@@ -78,6 +78,12 @@
     public static final String TETHER_ENABLE_LEGACY_DHCP_SERVER =
             "tether_enable_legacy_dhcp_server";
 
+    /**
+     * Default value that used to periodic polls tether offload stats from tethering offload HAL
+     * to make the data warnings work.
+     */
+    public static final int DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS = 5000;
+
     public final String[] tetherableUsbRegexs;
     public final String[] tetherableWifiRegexs;
     public final String[] tetherableWifiP2pRegexs;
@@ -96,6 +102,8 @@
 
     public final int activeDataSubId;
 
+    private final int mOffloadPollInterval;
+
     public TetheringConfiguration(Context ctx, SharedLog log, int id) {
         final SharedLog configLog = log.forSubComponent("config");
 
@@ -129,6 +137,10 @@
                 R.integer.config_mobile_hotspot_provision_check_period,
                 0 /* No periodic re-check */);
 
+        mOffloadPollInterval = getResourceInteger(res,
+                R.integer.config_tether_offload_poll_interval,
+                DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+
         configLog.log(toString());
     }
 
@@ -189,6 +201,9 @@
         dumpStringArray(pw, "legacyDhcpRanges", legacyDhcpRanges);
         dumpStringArray(pw, "defaultIPv4DNS", defaultIPv4DNS);
 
+        pw.print("offloadPollInterval: ");
+        pw.println(mOffloadPollInterval);
+
         dumpStringArray(pw, "provisioningApp", provisioningApp);
         pw.print("provisioningAppNoUi: ");
         pw.println(provisioningAppNoUi);
@@ -208,6 +223,7 @@
                 makeString(tetherableBluetoothRegexs)));
         sj.add(String.format("isDunRequired:%s", isDunRequired));
         sj.add(String.format("chooseUpstreamAutomatically:%s", chooseUpstreamAutomatically));
+        sj.add(String.format("offloadPollInterval:%d", mOffloadPollInterval));
         sj.add(String.format("preferredUpstreamIfaceTypes:%s",
                 toIntArray(preferredUpstreamIfaceTypes)));
         sj.add(String.format("provisioningApp:%s", makeString(provisioningApp)));
@@ -246,6 +262,10 @@
         return (tm != null) ? tm.isTetheringApnRequired() : false;
     }
 
+    public int getOffloadPollInterval() {
+        return mOffloadPollInterval;
+    }
+
     private static Collection<Integer> getUpstreamIfaceTypes(Resources res, boolean dunRequired) {
         final int[] ifaceTypes = res.getIntArray(R.array.config_tether_upstream_types);
         final ArrayList<Integer> upstreamIfaceTypes = new ArrayList<>(ifaceTypes.length);
diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
index 9b54b5f..802f2ac 100644
--- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
+++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java
@@ -16,6 +16,7 @@
 
 package com.android.networkstack.tethering;
 
+import android.app.usage.NetworkStatsManager;
 import android.bluetooth.BluetoothAdapter;
 import android.content.Context;
 import android.net.INetd;
@@ -47,6 +48,19 @@
     }
 
     /**
+     * Get a reference to the offload controller to be used by tethering.
+     */
+    @NonNull
+    public OffloadController getOffloadController(@NonNull Handler h,
+            @NonNull SharedLog log, @NonNull OffloadController.Dependencies deps) {
+        final NetworkStatsManager statsManager =
+                (NetworkStatsManager) getContext().getSystemService(Context.NETWORK_STATS_SERVICE);
+        return new OffloadController(h, getOffloadHardwareInterface(h, log),
+                getContext().getContentResolver(), statsManager, log, deps);
+    }
+
+
+    /**
      * Get a reference to the UpstreamNetworkMonitor to be used by tethering.
      */
     public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx, StateMachine target,
diff --git a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
index 25ddce4..320427c 100644
--- a/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
+++ b/Tethering/src/com/android/networkstack/tethering/UpstreamNetworkMonitor.java
@@ -43,7 +43,6 @@
 import android.util.SparseIntArray;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.util.Preconditions;
 import com.android.internal.util.StateMachine;
 
 import java.util.HashMap;
@@ -591,7 +590,9 @@
         // Map from type to transports.
         final int notFound = -1;
         final int transport = sLegacyTypeToTransport.get(type, notFound);
-        Preconditions.checkArgument(transport != notFound, "unknown legacy type: " + type);
+        if (transport == notFound) {
+            throw new IllegalArgumentException("unknown legacy type: " + type);
+        }
         builder.addTransportType(transport);
 
         if (type == TYPE_MOBILE_DUN) {
diff --git a/Tethering/tests/unit/jarjar-rules.txt b/Tethering/tests/unit/jarjar-rules.txt
index 921fbed..1ea56cd 100644
--- a/Tethering/tests/unit/jarjar-rules.txt
+++ b/Tethering/tests/unit/jarjar-rules.txt
@@ -4,7 +4,6 @@
 rule com.android.internal.util.IndentingPrintWriter.java* com.android.networkstack.tethering.util.IndentingPrintWriter.java@1
 rule com.android.internal.util.IState.java* com.android.networkstack.tethering.util.IState.java@1
 rule com.android.internal.util.MessageUtils* com.android.networkstack.tethering.util.MessageUtils@1
-rule com.android.internal.util.Preconditions* com.android.networkstack.tethering.util.Preconditions@1
 rule com.android.internal.util.State* com.android.networkstack.tethering.util.State@1
 rule com.android.internal.util.StateMachine* com.android.networkstack.tethering.util.StateMachine@1
 rule com.android.internal.util.TrafficStatsConstants* com.android.networkstack.tethering.util.TrafficStatsConstants@1
diff --git a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
index f8eb147..a8857b2 100644
--- a/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
+++ b/Tethering/tests/unit/src/android/net/dhcp/DhcpServingParamsParcelExtTest.java
@@ -110,7 +110,7 @@
     @Test
     public void testSetClientAddr() {
         mParcel.setSingleClientAddr(TEST_CLIENT_ADDRESS);
-        assertEquals(TEST_CLIENT_ADDRESS_PARCELED, mParcel.clientAddr);
+        assertEquals(TEST_CLIENT_ADDRESS_PARCELED, mParcel.singleClientAddr);
     }
 
     private static Inet4Address inet4Addr(String addr) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
index 6579720..b291438 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/OffloadControllerTest.java
@@ -29,15 +29,15 @@
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_IFACE;
 import static com.android.networkstack.tethering.OffloadController.StatsType.STATS_PER_UID;
 import static com.android.networkstack.tethering.OffloadHardwareInterface.ForwardedStats;
+import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
 import static com.android.testutils.MiscAssertsKt.assertContainsAll;
 import static com.android.testutils.MiscAssertsKt.assertThrows;
-import static com.android.testutils.NetworkStatsUtilsKt.orderInsensitiveEquals;
+import static com.android.testutils.NetworkStatsUtilsKt.assertNetworkStatsEquals;
 
 import static junit.framework.Assert.assertNotNull;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.anyObject;
@@ -63,10 +63,10 @@
 import android.net.NetworkStats;
 import android.net.NetworkStats.Entry;
 import android.net.RouteInfo;
-import android.net.netstats.provider.INetworkStatsProviderCallback;
+import android.net.netstats.provider.NetworkStatsProvider;
 import android.net.util.SharedLog;
 import android.os.Handler;
-import android.os.Looper;
+import android.os.test.TestLooper;
 import android.provider.Settings;
 import android.provider.Settings.SettingNotFoundException;
 import android.test.mock.MockContentResolver;
@@ -75,7 +75,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.util.test.FakeSettingsProvider;
-import com.android.testutils.HandlerUtilsKt;
+import com.android.testutils.TestableNetworkStatsProviderCbBinder;
 
 import org.junit.After;
 import org.junit.Before;
@@ -109,13 +109,22 @@
     @Mock private ApplicationInfo mApplicationInfo;
     @Mock private Context mContext;
     @Mock private NetworkStatsManager mStatsManager;
-    @Mock private INetworkStatsProviderCallback mTetherStatsProviderCb;
+    @Mock private TetheringConfiguration mTetherConfig;
+    // Late init since methods must be called by the thread that created this object.
+    private TestableNetworkStatsProviderCbBinder mTetherStatsProviderCb;
     private OffloadController.OffloadTetheringStatsProvider mTetherStatsProvider;
     private final ArgumentCaptor<ArrayList> mStringArrayCaptor =
             ArgumentCaptor.forClass(ArrayList.class);
     private final ArgumentCaptor<OffloadHardwareInterface.ControlCallback> mControlCallbackCaptor =
             ArgumentCaptor.forClass(OffloadHardwareInterface.ControlCallback.class);
     private MockContentResolver mContentResolver;
+    private final TestLooper mTestLooper = new TestLooper();
+    private OffloadController.Dependencies mDeps = new OffloadController.Dependencies() {
+        @Override
+        public TetheringConfiguration getTetherConfig() {
+            return mTetherConfig;
+        }
+    };
 
     @Before public void setUp() {
         MockitoAnnotations.initMocks(this);
@@ -125,6 +134,7 @@
         mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
         when(mContext.getContentResolver()).thenReturn(mContentResolver);
         FakeSettingsProvider.clearSettingsProvider();
+        when(mTetherConfig.getOffloadPollInterval()).thenReturn(-1); // Disabled.
     }
 
     @After public void tearDown() throws Exception {
@@ -144,13 +154,17 @@
         Settings.Global.putInt(mContentResolver, TETHER_OFFLOAD_DISABLED, 0);
     }
 
+    private void setOffloadPollInterval(int interval) {
+        when(mTetherConfig.getOffloadPollInterval()).thenReturn(interval);
+    }
+
     private void waitForIdle() {
-        HandlerUtilsKt.waitForIdle(new Handler(Looper.getMainLooper()), WAIT_FOR_IDLE_TIMEOUT);
+        mTestLooper.dispatchAll();
     }
 
     private OffloadController makeOffloadController() throws Exception {
-        OffloadController offload = new OffloadController(new Handler(Looper.getMainLooper()),
-                mHardware, mContentResolver, mStatsManager, new SharedLog("test"));
+        OffloadController offload = new OffloadController(new Handler(mTestLooper.getLooper()),
+                mHardware, mContentResolver, mStatsManager, new SharedLog("test"), mDeps);
         final ArgumentCaptor<OffloadController.OffloadTetheringStatsProvider>
                 tetherStatsProviderCaptor =
                 ArgumentCaptor.forClass(OffloadController.OffloadTetheringStatsProvider.class);
@@ -158,6 +172,7 @@
                 tetherStatsProviderCaptor.capture());
         mTetherStatsProvider = tetherStatsProviderCaptor.getValue();
         assertNotNull(mTetherStatsProvider);
+        mTetherStatsProviderCb = new TestableNetworkStatsProviderCbBinder();
         mTetherStatsProvider.setProviderCallbackBinder(mTetherStatsProviderCb);
         return offload;
     }
@@ -346,9 +361,9 @@
         stacked.setInterfaceName("stacked");
         stacked.addLinkAddress(new LinkAddress("192.0.2.129/25"));
         stacked.addRoute(new RouteInfo(null, InetAddress.getByName("192.0.2.254"), null,
-                  RTN_UNICAST));
+                RTN_UNICAST));
         stacked.addRoute(new RouteInfo(null, InetAddress.getByName("fe80::bad:f00"), null,
-                  RTN_UNICAST));
+                RTN_UNICAST));
         assertTrue(lp.addStackedLink(stacked));
         offload.setUpstreamLinkProperties(lp);
         // No change in local addresses means no call to setLocalPrefixes().
@@ -453,20 +468,12 @@
                 .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999))
                 .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 12345, 54321));
 
-        assertTrue(orderInsensitiveEquals(expectedIfaceStats, ifaceStats));
-        assertTrue(orderInsensitiveEquals(expectedUidStats, uidStats));
-
-        final ArgumentCaptor<NetworkStats> ifaceStatsCaptor = ArgumentCaptor.forClass(
-                NetworkStats.class);
-        final ArgumentCaptor<NetworkStats> uidStatsCaptor = ArgumentCaptor.forClass(
-                NetworkStats.class);
+        assertNetworkStatsEquals(expectedIfaceStats, ifaceStats);
+        assertNetworkStatsEquals(expectedUidStats, uidStats);
 
         // Force pushing stats update to verify the stats reported.
         mTetherStatsProvider.pushTetherStats();
-        verify(mTetherStatsProviderCb, times(1))
-                .notifyStatsUpdated(anyInt(), ifaceStatsCaptor.capture(), uidStatsCaptor.capture());
-        assertTrue(orderInsensitiveEquals(expectedIfaceStats, ifaceStatsCaptor.getValue()));
-        assertTrue(orderInsensitiveEquals(expectedUidStats, uidStatsCaptor.getValue()));
+        mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStats, expectedUidStats);
 
         when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
                 new ForwardedStats(100000, 100000));
@@ -492,11 +499,10 @@
                 .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 999, 99999))
                 .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 112345, 154321));
 
-        assertTrue(orderInsensitiveEquals(expectedIfaceStatsAccu, ifaceStatsAccu));
-        assertTrue(orderInsensitiveEquals(expectedUidStatsAccu, uidStatsAccu));
+        assertNetworkStatsEquals(expectedIfaceStatsAccu, ifaceStatsAccu);
+        assertNetworkStatsEquals(expectedUidStatsAccu, uidStatsAccu);
 
         // Verify that only diff of stats is reported.
-        reset(mTetherStatsProviderCb);
         mTetherStatsProvider.pushTetherStats();
         final NetworkStats expectedIfaceStatsDiff = new NetworkStats(0L, 2)
                 .addEntry(buildTestEntry(STATS_PER_IFACE, mobileIface, 0, 0))
@@ -505,10 +511,8 @@
         final NetworkStats expectedUidStatsDiff = new NetworkStats(0L, 2)
                 .addEntry(buildTestEntry(STATS_PER_UID, mobileIface, 0, 0))
                 .addEntry(buildTestEntry(STATS_PER_UID, ethernetIface, 100000, 100000));
-        verify(mTetherStatsProviderCb, times(1))
-                .notifyStatsUpdated(anyInt(), ifaceStatsCaptor.capture(), uidStatsCaptor.capture());
-        assertTrue(orderInsensitiveEquals(expectedIfaceStatsDiff, ifaceStatsCaptor.getValue()));
-        assertTrue(orderInsensitiveEquals(expectedUidStatsDiff, uidStatsCaptor.getValue()));
+        mTetherStatsProviderCb.expectNotifyStatsUpdated(expectedIfaceStatsDiff,
+                expectedUidStatsDiff);
     }
 
     @Test
@@ -585,7 +589,7 @@
 
         OffloadHardwareInterface.ControlCallback callback = mControlCallbackCaptor.getValue();
         callback.onStoppedLimitReached();
-        verify(mTetherStatsProviderCb, times(1)).notifyStatsUpdated(anyInt(), any(), any());
+        mTetherStatsProviderCb.expectNotifyStatsUpdated();
     }
 
     @Test
@@ -689,8 +693,8 @@
         verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
         verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
         // TODO: verify the exact stats reported.
-        verify(mTetherStatsProviderCb, times(1)).notifyStatsUpdated(anyInt(), any(), any());
-        verifyNoMoreInteractions(mTetherStatsProviderCb);
+        mTetherStatsProviderCb.expectNotifyStatsUpdated();
+        mTetherStatsProviderCb.assertNoCallback();
         verifyNoMoreInteractions(mHardware);
     }
 
@@ -754,8 +758,8 @@
         // Verify forwarded stats behaviour.
         verify(mHardware, times(1)).getForwardedStats(eq(RMNET0));
         verify(mHardware, times(1)).getForwardedStats(eq(WLAN0));
-        verify(mTetherStatsProviderCb, times(1)).notifyStatsUpdated(anyInt(), any(), any());
-        verifyNoMoreInteractions(mTetherStatsProviderCb);
+        mTetherStatsProviderCb.expectNotifyStatsUpdated();
+        mTetherStatsProviderCb.assertNoCallback();
 
         // TODO: verify local prefixes and downstreams are also pushed to the HAL.
         verify(mHardware, times(1)).setLocalPrefixes(mStringArrayCaptor.capture());
@@ -774,4 +778,50 @@
         verifyNoMoreInteractions(mHardware);
     }
 
+    @Test
+    public void testOnSetAlert() throws Exception {
+        setupFunctioningHardwareInterface();
+        enableOffload();
+        setOffloadPollInterval(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+        final OffloadController offload = makeOffloadController();
+        offload.start();
+
+        // Initialize with fake eth upstream.
+        final String ethernetIface = "eth1";
+        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());
+
+        // Verify that set quota to 0 will immediately triggers an callback.
+        mTetherStatsProvider.onSetAlert(0);
+        waitForIdle();
+        mTetherStatsProviderCb.expectNotifyAlertReached();
+
+        // Verify that notifyAlertReached never fired if quota is not yet reached.
+        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
+                new ForwardedStats(0, 0));
+        mTetherStatsProvider.onSetAlert(100);
+        mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+        waitForIdle();
+        mTetherStatsProviderCb.assertNoCallback();
+
+        // Verify that notifyAlertReached fired when quota is reached.
+        when(mHardware.getForwardedStats(eq(ethernetIface))).thenReturn(
+                new ForwardedStats(50, 50));
+        mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+        waitForIdle();
+        mTetherStatsProviderCb.expectNotifyAlertReached();
+
+        // Verify that set quota with UNLIMITED won't trigger any callback, and won't fetch
+        // any stats since the polling is stopped.
+        reset(mHardware);
+        mTetherStatsProvider.onSetAlert(NetworkStatsProvider.QUOTA_UNLIMITED);
+        mTestLooper.moveTimeForward(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
+        waitForIdle();
+        mTetherStatsProviderCb.assertNoCallback();
+        verify(mHardware, never()).getForwardedStats(any());
+    }
 }
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
index 07ddea4..e8ba5b8 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringConfigurationTest.java
@@ -115,6 +115,8 @@
 
         when(mResources.getStringArray(R.array.config_tether_dhcp_range)).thenReturn(
                 new String[0]);
+        when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn(
+                TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS);
         when(mResources.getStringArray(R.array.config_tether_usb_regexs)).thenReturn(new String[0]);
         when(mResources.getStringArray(R.array.config_tether_wifi_regexs))
                 .thenReturn(new String[]{ "test_wlan\\d" });
@@ -314,6 +316,23 @@
     }
 
     @Test
+    public void testOffloadIntervalByResource() {
+        final TetheringConfiguration intervalByDefault =
+                new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+        assertEquals(TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS,
+                intervalByDefault.getOffloadPollInterval());
+
+        final int[] testOverrides = {0, 3000, -1};
+        for (final int override : testOverrides) {
+            when(mResources.getInteger(R.integer.config_tether_offload_poll_interval)).thenReturn(
+                    override);
+            final TetheringConfiguration overrideByRes =
+                    new TetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID);
+            assertEquals(override, overrideByRes.getOffloadPollInterval());
+        }
+    }
+
+    @Test
     public void testGetResourcesBySubId() {
         setUpResourceForSubId();
         final TetheringConfiguration cfg = new TetheringConfiguration(
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 0c86eeb..fff7a70 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -150,6 +150,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.util.ArrayList;
@@ -212,6 +214,9 @@
     private Tethering mTethering;
     private PhoneStateListener mPhoneStateListener;
     private InterfaceConfigurationParcel mInterfaceConfiguration;
+    private TetheringConfiguration mConfig;
+    private EntitlementManager mEntitleMgr;
+    private OffloadController mOffloadCtrl;
 
     private class TestContext extends BroadcastInterceptingContext {
         TestContext(Context base) {
@@ -297,8 +302,9 @@
         }
     }
 
-    private class MockTetheringConfiguration extends TetheringConfiguration {
-        MockTetheringConfiguration(Context ctx, SharedLog log, int id) {
+    // MyTetheringConfiguration is used to override static method for testing.
+    private class MyTetheringConfiguration extends TetheringConfiguration {
+        MyTetheringConfiguration(Context ctx, SharedLog log, int id) {
             super(ctx, log, id);
         }
 
@@ -328,6 +334,15 @@
         }
 
         @Override
+        public OffloadController getOffloadController(Handler h, SharedLog log,
+                OffloadController.Dependencies deps) {
+            mOffloadCtrl = spy(super.getOffloadController(h, log, deps));
+            // Return real object here instead of mock because
+            // testReportFailCallbackIfOffloadNotSupported depend on real OffloadController object.
+            return mOffloadCtrl;
+        }
+
+        @Override
         public UpstreamNetworkMonitor getUpstreamNetworkMonitor(Context ctx,
                 StateMachine target, SharedLog log, int what) {
             mUpstreamNetworkMonitorMasterSM = target;
@@ -352,6 +367,13 @@
         }
 
         @Override
+        public EntitlementManager getEntitlementManager(Context ctx, StateMachine target,
+                SharedLog log, int what) {
+            mEntitleMgr = spy(super.getEntitlementManager(ctx, target, log, what));
+            return mEntitleMgr;
+        }
+
+        @Override
         public boolean isTetheringSupported() {
             return true;
         }
@@ -359,7 +381,8 @@
         @Override
         public TetheringConfiguration generateTetheringConfiguration(Context ctx, SharedLog log,
                 int subId) {
-            return new MockTetheringConfiguration(ctx, log, subId);
+            mConfig = spy(new MyTetheringConfiguration(ctx, log, subId));
+            return mConfig;
         }
 
         @Override
@@ -1689,7 +1712,7 @@
         final DhcpServingParamsParcel params = dhcpParamsCaptor.getValue();
         assertEquals(serverAddr, intToInet4AddressHTH(params.serverAddr).getHostAddress());
         assertEquals(24, params.serverAddrPrefixLength);
-        assertEquals(clientAddrParceled, params.clientAddr);
+        assertEquals(clientAddrParceled, params.singleClientAddr);
     }
 
     @Test
@@ -1726,6 +1749,17 @@
         verify(mNotificationUpdater, never()).onUpstreamCapabilitiesChanged(any());
     }
 
+    @Test
+    public void testDumpTetheringLog() throws Exception {
+        final FileDescriptor mockFd = mock(FileDescriptor.class);
+        final PrintWriter mockPw = mock(PrintWriter.class);
+        runUsbTethering(null);
+        mTethering.dump(mockFd, mockPw, new String[0]);
+        verify(mConfig).dump(any());
+        verify(mEntitleMgr).dump(any());
+        verify(mOffloadCtrl).dump(any());
+    }
+
     // TODO: Test that a request for hotspot mode doesn't interfere with an
     // already operating tethering mode interface.
 }