Merge "[BR06] Support check whether network is blocked by data saver" into main
diff --git a/DnsResolver/Android.bp b/DnsResolver/Android.bp
new file mode 100644
index 0000000..d133034
--- /dev/null
+++ b/DnsResolver/Android.bp
@@ -0,0 +1,83 @@
+//
+// Copyright (C) 2023 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library {
+    name: "libcom.android.tethering.dns_helper",
+    version_script: "libcom.android.tethering.dns_helper.map.txt",
+    stubs: {
+        versions: [
+            "1",
+        ],
+        symbol_file: "libcom.android.tethering.dns_helper.map.txt",
+    },
+    defaults: ["netd_defaults"],
+    header_libs: [
+        "bpf_connectivity_headers",
+        "libcutils_headers",
+    ],
+    srcs: [
+        "DnsBpfHelper.cpp",
+        "DnsHelper.cpp",
+    ],
+    static_libs: [
+        "libmodules-utils-build",
+    ],
+    shared_libs: [
+        "libbase",
+    ],
+    export_include_dirs: ["include"],
+    header_abi_checker: {
+        enabled: true,
+        symbol_file: "libcom.android.tethering.dns_helper.map.txt",
+    },
+    sanitize: {
+        cfi: true,
+    },
+    apex_available: ["com.android.tethering"],
+    min_sdk_version: "30",
+}
+
+cc_test {
+    name: "dns_helper_unit_test",
+    defaults: ["netd_defaults"],
+    test_suites: ["general-tests", "mts-tethering"],
+    test_config_template: ":net_native_test_config_template",
+    header_libs: [
+        "bpf_connectivity_headers",
+    ],
+    srcs: [
+        "DnsBpfHelperTest.cpp",
+    ],
+    static_libs: [
+        "libcom.android.tethering.dns_helper",
+    ],
+    shared_libs: [
+       "libbase",
+       "libcutils",
+    ],
+    compile_multilib: "both",
+    multilib: {
+        lib32: {
+            suffix: "32",
+        },
+        lib64: {
+            suffix: "64",
+        },
+    },
+}
diff --git a/DnsResolver/DnsBpfHelper.cpp b/DnsResolver/DnsBpfHelper.cpp
new file mode 100644
index 0000000..37c46ca
--- /dev/null
+++ b/DnsResolver/DnsBpfHelper.cpp
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#define LOG_TAG "DnsBpfHelper"
+
+#include "DnsBpfHelper.h"
+
+#include <android-base/logging.h>
+#include <android-modules-utils/sdk_level.h>
+
+namespace android {
+namespace net {
+
+#define RETURN_IF_RESULT_NOT_OK(result)                                                            \
+  do {                                                                                             \
+    if (!result.ok()) {                                                                            \
+      LOG(ERROR) << "L" << __LINE__ << " " << __func__ << ": " << strerror(result.error().code()); \
+      return result.error();                                                                       \
+    }                                                                                              \
+  } while (0)
+
+base::Result<void> DnsBpfHelper::init() {
+  if (!android::modules::sdklevel::IsAtLeastT()) {
+    LOG(ERROR) << __func__ << ": Unsupported before Android T.";
+    return base::Error(EOPNOTSUPP);
+  }
+
+  RETURN_IF_RESULT_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
+  RETURN_IF_RESULT_NOT_OK(mUidOwnerMap.init(UID_OWNER_MAP_PATH));
+  RETURN_IF_RESULT_NOT_OK(mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH));
+  return {};
+}
+
+base::Result<bool> DnsBpfHelper::isUidNetworkingBlocked(uid_t uid, bool metered) {
+  if (is_system_uid(uid)) return false;
+  if (!mConfigurationMap.isValid() || !mUidOwnerMap.isValid()) {
+    LOG(ERROR) << __func__
+               << ": BPF maps are not ready. Forgot to call ADnsHelper_init?";
+    return base::Error(EUNATCH);
+  }
+
+  auto enabledRules = mConfigurationMap.readValue(UID_RULES_CONFIGURATION_KEY);
+  RETURN_IF_RESULT_NOT_OK(enabledRules);
+
+  auto value = mUidOwnerMap.readValue(uid);
+  uint32_t uidRules = value.ok() ? value.value().rule : 0;
+
+  // For doze mode, battery saver, low power standby.
+  if (isBlockedByUidRules(enabledRules.value(), uidRules)) return true;
+
+  // For data saver.
+  if (!metered) return false;
+
+  // The background data setting (PENALTY_BOX_MATCH) and unrestricted data usage setting
+  // (HAPPY_BOX_MATCH) for individual apps override the system wide Data Saver setting.
+  if (uidRules & PENALTY_BOX_MATCH) return true;
+  if (uidRules & HAPPY_BOX_MATCH) return false;
+
+  auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
+  RETURN_IF_RESULT_NOT_OK(dataSaverSetting);
+  return dataSaverSetting.value();
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/DnsResolver/DnsBpfHelper.h b/DnsResolver/DnsBpfHelper.h
new file mode 100644
index 0000000..f1c3992
--- /dev/null
+++ b/DnsResolver/DnsBpfHelper.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#pragma once
+
+#include <android-base/result.h>
+
+#include "bpf/BpfMap.h"
+#include "netd.h"
+
+namespace android {
+namespace net {
+
+class DnsBpfHelper {
+ public:
+  DnsBpfHelper() = default;
+  DnsBpfHelper(const DnsBpfHelper&) = delete;
+  DnsBpfHelper& operator=(const DnsBpfHelper&) = delete;
+
+  base::Result<void> init();
+  base::Result<bool> isUidNetworkingBlocked(uid_t uid, bool metered);
+
+ private:
+  android::bpf::BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
+  android::bpf::BpfMapRO<uint32_t, UidOwnerValue> mUidOwnerMap;
+  android::bpf::BpfMapRO<uint32_t, bool> mDataSaverEnabledMap;
+
+  // For testing
+  friend class DnsBpfHelperTest;
+};
+
+}  // namespace net
+}  // namespace android
diff --git a/DnsResolver/DnsBpfHelperTest.cpp b/DnsResolver/DnsBpfHelperTest.cpp
new file mode 100644
index 0000000..67b5b95
--- /dev/null
+++ b/DnsResolver/DnsBpfHelperTest.cpp
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2023 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 <gtest/gtest.h>
+#include <private/android_filesystem_config.h>
+
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
+#include "DnsBpfHelper.h"
+
+using namespace android::bpf;  // NOLINT(google-build-using-namespace): exempted
+
+namespace android {
+namespace net {
+
+constexpr int TEST_MAP_SIZE = 2;
+
+#define ASSERT_VALID(x) ASSERT_TRUE((x).isValid())
+
+class DnsBpfHelperTest : public ::testing::Test {
+ protected:
+  DnsBpfHelper mDnsBpfHelper;
+  BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
+  BpfMap<uint32_t, UidOwnerValue> mFakeUidOwnerMap;
+  BpfMap<uint32_t, bool> mFakeDataSaverEnabledMap;
+
+  void SetUp() {
+    mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
+    ASSERT_VALID(mFakeConfigurationMap);
+
+    mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
+    ASSERT_VALID(mFakeUidOwnerMap);
+
+    mFakeDataSaverEnabledMap.resetMap(BPF_MAP_TYPE_ARRAY, DATA_SAVER_ENABLED_MAP_SIZE);
+    ASSERT_VALID(mFakeDataSaverEnabledMap);
+
+    mDnsBpfHelper.mConfigurationMap = mFakeConfigurationMap;
+    ASSERT_VALID(mDnsBpfHelper.mConfigurationMap);
+    mDnsBpfHelper.mUidOwnerMap = mFakeUidOwnerMap;
+    ASSERT_VALID(mDnsBpfHelper.mUidOwnerMap);
+    mDnsBpfHelper.mDataSaverEnabledMap = mFakeDataSaverEnabledMap;
+    ASSERT_VALID(mDnsBpfHelper.mDataSaverEnabledMap);
+  }
+
+  void ResetAllMaps() {
+    mDnsBpfHelper.mConfigurationMap.reset();
+    mDnsBpfHelper.mUidOwnerMap.reset();
+    mDnsBpfHelper.mDataSaverEnabledMap.reset();
+  }
+};
+
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked) {
+  struct TestConfig {
+    const uid_t uid;
+    const uint32_t enabledRules;
+    const uint32_t uidRules;
+    const int expectedResult;
+    std::string toString() const {
+      return fmt::format(
+          "uid: {}, enabledRules: {}, uidRules: {}, expectedResult: {}",
+          uid, enabledRules, uidRules, expectedResult);
+    }
+  } testConfigs[] = {
+    // clang-format off
+    //   No rule enabled:
+    // uid,         enabledRules,                  uidRules,                      expectedResult
+    {AID_APP_START, NO_MATCH,                      NO_MATCH,                      false},
+
+    //   An allowlist rule:
+    {AID_APP_START, NO_MATCH,                      DOZABLE_MATCH,                 false},
+    {AID_APP_START, DOZABLE_MATCH,                 NO_MATCH,                      true},
+    {AID_APP_START, DOZABLE_MATCH,                 DOZABLE_MATCH,                 false},
+    //   A denylist rule
+    {AID_APP_START, NO_MATCH,                      STANDBY_MATCH,                 false},
+    {AID_APP_START, STANDBY_MATCH,                 NO_MATCH,                      false},
+    {AID_APP_START, STANDBY_MATCH,                 STANDBY_MATCH,                 true},
+
+    //   Multiple rules enabled:
+    //     Match only part of the enabled allowlist rules.
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH,                 true},
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, POWERSAVE_MATCH,               true},
+    //     Match all of the enabled allowlist rules.
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH|POWERSAVE_MATCH, false},
+    //     Match allowlist.
+    {AID_APP_START, DOZABLE_MATCH|STANDBY_MATCH,   DOZABLE_MATCH,                 false},
+    //     Match no rule.
+    {AID_APP_START, DOZABLE_MATCH|STANDBY_MATCH,   NO_MATCH,                      true},
+    {AID_APP_START, DOZABLE_MATCH|POWERSAVE_MATCH, NO_MATCH,                      true},
+
+    // System UID: always unblocked.
+    {AID_SYSTEM,    NO_MATCH,                      NO_MATCH,                      false},
+    {AID_SYSTEM,    NO_MATCH,                      DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH,                 NO_MATCH,                      false},
+    {AID_SYSTEM,    DOZABLE_MATCH,                 DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    NO_MATCH,                      STANDBY_MATCH,                 false},
+    {AID_SYSTEM,    STANDBY_MATCH,                 NO_MATCH,                      false},
+    {AID_SYSTEM,    STANDBY_MATCH,                 STANDBY_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, POWERSAVE_MATCH,               false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, DOZABLE_MATCH|POWERSAVE_MATCH, false},
+    {AID_SYSTEM,    DOZABLE_MATCH|STANDBY_MATCH,   DOZABLE_MATCH,                 false},
+    {AID_SYSTEM,    DOZABLE_MATCH|STANDBY_MATCH,   NO_MATCH,                      false},
+    {AID_SYSTEM,    DOZABLE_MATCH|POWERSAVE_MATCH, NO_MATCH,                      false},
+    // clang-format on
+  };
+
+  for (const auto& config : testConfigs) {
+    SCOPED_TRACE(config.toString());
+
+    // Setup maps.
+    EXPECT_RESULT_OK(mFakeConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY,
+                                                      config.enabledRules, BPF_EXIST));
+    EXPECT_RESULT_OK(mFakeUidOwnerMap.writeValue(config.uid, {.iif = 0, .rule = config.uidRules},
+                                                 BPF_ANY));
+
+    // Verify the function.
+    auto result = mDnsBpfHelper.isUidNetworkingBlocked(config.uid, /*metered=*/false);
+    EXPECT_TRUE(result.ok());
+    EXPECT_EQ(config.expectedResult, result.value());
+  }
+}
+
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked_uninitialized) {
+  ResetAllMaps();
+
+  auto result = mDnsBpfHelper.isUidNetworkingBlocked(AID_APP_START, /*metered=*/false);
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(EUNATCH, result.error().code());
+
+  result = mDnsBpfHelper.isUidNetworkingBlocked(AID_SYSTEM, /*metered=*/false);
+  EXPECT_TRUE(result.ok());
+  EXPECT_FALSE(result.value());
+}
+
+// Verify DataSaver on metered network.
+TEST_F(DnsBpfHelperTest, IsUidNetworkingBlocked_metered) {
+  struct TestConfig {
+    const uint32_t enabledRules;     // Settings in configuration map.
+    const bool dataSaverEnabled;     // Settings in data saver enabled map.
+    const uint32_t uidRules;         // Settings in uid owner map.
+    const int blocked;               // Whether the UID is expected to be networking blocked or not.
+    std::string toString() const {
+      return fmt::format(
+          ", enabledRules: {}, dataSaverEnabled: {},  uidRules: {}, expect blocked: {}",
+          enabledRules, dataSaverEnabled, uidRules, blocked);
+    }
+  } testConfigs[]{
+    // clang-format off
+    // enabledRules, dataSaverEnabled, uidRules,                                        blocked
+    {NO_MATCH,       false,            NO_MATCH,                                        false},
+    {NO_MATCH,       false,            PENALTY_BOX_MATCH,                               true},
+    {NO_MATCH,       false,            HAPPY_BOX_MATCH,                                 false},
+    {NO_MATCH,       false,            PENALTY_BOX_MATCH|HAPPY_BOX_MATCH,               true},
+    {NO_MATCH,       true,             NO_MATCH,                                        true},
+    {NO_MATCH,       true,             PENALTY_BOX_MATCH,                               true},
+    {NO_MATCH,       true,             HAPPY_BOX_MATCH,                                 false},
+    {NO_MATCH,       true,             PENALTY_BOX_MATCH|HAPPY_BOX_MATCH,               true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH,                                   true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_MATCH,                 true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|HAPPY_BOX_MATCH,                   true},
+    {STANDBY_MATCH,  false,            STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH,                                   true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_MATCH,                 true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|HAPPY_BOX_MATCH,                   true},
+    {STANDBY_MATCH,  true,             STANDBY_MATCH|PENALTY_BOX_MATCH|HAPPY_BOX_MATCH, true},
+    // clang-format on
+  };
+
+  for (const auto& config : testConfigs) {
+    SCOPED_TRACE(config.toString());
+
+    // Setup maps.
+    EXPECT_RESULT_OK(mFakeConfigurationMap.writeValue(UID_RULES_CONFIGURATION_KEY,
+                                                      config.enabledRules, BPF_EXIST));
+    EXPECT_RESULT_OK(mFakeDataSaverEnabledMap.writeValue(DATA_SAVER_ENABLED_KEY,
+                                                      config.dataSaverEnabled, BPF_EXIST));
+    EXPECT_RESULT_OK(mFakeUidOwnerMap.writeValue(AID_APP_START, {.iif = 0, .rule = config.uidRules},
+                                                 BPF_ANY));
+
+    // Verify the function.
+    auto result = mDnsBpfHelper.isUidNetworkingBlocked(AID_APP_START, /*metered=*/true);
+    EXPECT_RESULT_OK(result);
+    EXPECT_EQ(config.blocked, result.value());
+  }
+}
+
+}  // namespace net
+}  // namespace android
diff --git a/DnsResolver/DnsHelper.cpp b/DnsResolver/DnsHelper.cpp
new file mode 100644
index 0000000..3372908
--- /dev/null
+++ b/DnsResolver/DnsHelper.cpp
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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 "DnsBpfHelper.h"
+#include "DnsHelperPublic.h"
+
+static android::net::DnsBpfHelper sDnsBpfHelper;
+
+int ADnsHelper_init() {
+  auto result = sDnsBpfHelper.init();
+  if (!result.ok()) return -result.error().code();
+
+  return 0;
+}
+
+int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered) {
+  auto result = sDnsBpfHelper.isUidNetworkingBlocked(uid, metered);
+  if (!result.ok()) return -result.error().code();
+
+  // bool -> int conversion.
+  return result.value();
+}
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
new file mode 100644
index 0000000..7c9fc9e
--- /dev/null
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#pragma once
+
+#include <sys/cdefs.h>
+#include <sys/types.h>
+
+__BEGIN_DECLS
+
+/*
+ * Perform any required initialization - including opening any required BPF maps. This function
+ * needs to be called before using other functions of this library.
+ *
+ * Returns 0 on success, a negative POSIX error code (see errno.h) on other failures.
+ */
+int ADnsHelper_init();
+
+/*
+ * The function reads bpf maps and returns whether the given uid has blocked networking or not. The
+ * function is supported starting from Android T.
+ *
+ * |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID.
+ * |metered| indicates whether the uid is currently using a billing network.
+ *
+ * Returns 0(false)/1(true) on success, a negative POSIX error code (see errno.h) on other failures.
+ */
+int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered);
+
+__END_DECLS
diff --git a/DnsResolver/libcom.android.tethering.dns_helper.map.txt b/DnsResolver/libcom.android.tethering.dns_helper.map.txt
new file mode 100644
index 0000000..3c965a2
--- /dev/null
+++ b/DnsResolver/libcom.android.tethering.dns_helper.map.txt
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2023 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.
+#
+
+# This lists the entry points visible to applications that use the
+# libcom.android.tethering.dns_helper library. Other entry points present in
+# the library won't be usable.
+
+LIBCOM_ANDROID_TETHERING_DNS_HELPER {
+  global:
+    ADnsHelper_init; # apex
+    ADnsHelper_isUidNetworkingBlocked; # apex
+  local:
+    *;
+};
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 46308af..520124d 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -131,6 +131,9 @@
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
     {
+      "name": "dns_helper_unit_test"
+    },
+    {
       "name": "traffic_controller_unit_test",
       "keywords": ["netd-device-kernel-4.9", "netd-device-kernel-4.14"]
     },
diff --git a/Tethering/Android.bp b/Tethering/Android.bp
index e69b872..dd60be7 100644
--- a/Tethering/Android.bp
+++ b/Tethering/Android.bp
@@ -226,6 +226,7 @@
         "com.android.tethering",
     ],
     native_shared_libs: [
+        "libcom.android.tethering.dns_helper",
         "libnetd_updatable",
     ],
 }
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index cd8eac8..ee44f3c 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -83,6 +83,7 @@
                 "libandroid_net_connectivity_com_android_net_module_util_jni",
             ],
             native_shared_libs: [
+                "libcom.android.tethering.dns_helper",
                 "libcom.android.tethering.connectivity_native",
                 "libnetd_updatable",
             ],
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index b3f8ed6..cdf47e7 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -45,6 +45,7 @@
         "com.android.tethering",
     ],
     visibility: [
+        "//packages/modules/Connectivity/DnsResolver",
         "//packages/modules/Connectivity/netd",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 5759df7..f223dd1 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -92,7 +92,7 @@
 DEFINE_BPF_MAP_RO_NETD(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
 DEFINE_BPF_MAP_RO_NETD(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE)
-DEFINE_BPF_MAP_NO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
+DEFINE_BPF_MAP_RO_NETD(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE)
 DEFINE_BPF_MAP_RO_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
 DEFINE_BPF_MAP_NO_NETD(ingress_discard_map, HASH, IngressDiscardKey, IngressDiscardValue,
                        INGRESS_DISCARD_MAP_SIZE)
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index f960dcf..d1fc58d 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -236,6 +236,8 @@
 #define UID_RULES_CONFIGURATION_KEY 0
 // Entry in the configuration map that stores which stats map is currently in use.
 #define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
+// Entry in the data saver enabled map that stores whether data saver is enabled or not.
+#define DATA_SAVER_ENABLED_KEY 0
 
 #undef STRUCT_SIZE
 
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index cc3f019..c74f229 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -519,9 +519,9 @@
         }
     }
 
+    // TODO: Use a Handler instead of a StateMachine since there are no state changes.
     private class NsdStateMachine extends StateMachine {
 
-        private final DefaultState mDefaultState = new DefaultState();
         private final EnabledState mEnabledState = new EnabledState();
 
         @Override
@@ -591,124 +591,12 @@
 
         NsdStateMachine(String name, Handler handler) {
             super(name, handler);
-            addState(mDefaultState);
-                addState(mEnabledState, mDefaultState);
+            addState(mEnabledState);
             State initialState = mEnabledState;
             setInitialState(initialState);
             setLogRecSize(25);
         }
 
-        class DefaultState extends State {
-            @Override
-            public boolean processMessage(Message msg) {
-                final ClientInfo cInfo;
-                final int clientRequestId = msg.arg2;
-                switch (msg.what) {
-                    case NsdManager.REGISTER_CLIENT:
-                        final ConnectorArgs arg = (ConnectorArgs) msg.obj;
-                        final INsdManagerCallback cb = arg.callback;
-                        try {
-                            cb.asBinder().linkToDeath(arg.connector, 0);
-                            final String tag = "Client" + arg.uid + "-" + mClientNumberId++;
-                            final NetworkNsdReportedMetrics metrics =
-                                    mDeps.makeNetworkNsdReportedMetrics(
-                                            (int) mClock.elapsedRealtime());
-                            cInfo = new ClientInfo(cb, arg.uid, arg.useJavaBackend,
-                                    mServiceLogs.forSubComponent(tag), metrics);
-                            mClients.put(arg.connector, cInfo);
-                        } catch (RemoteException e) {
-                            Log.w(TAG, "Client request id " + clientRequestId
-                                    + " has already died");
-                        }
-                        break;
-                    case NsdManager.UNREGISTER_CLIENT:
-                        final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
-                        cInfo = mClients.remove(connector);
-                        if (cInfo != null) {
-                            cInfo.expungeAllRequests();
-                            if (cInfo.isPreSClient()) {
-                                mLegacyClientCount -= 1;
-                            }
-                        }
-                        maybeStopMonitoringSocketsIfNoActiveRequest();
-                        maybeScheduleStop();
-                        break;
-                    case NsdManager.DISCOVER_SERVICES:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onDiscoverServicesFailedImmediately(clientRequestId,
-                                    NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
-                        }
-                       break;
-                    case NsdManager.STOP_DISCOVERY:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onStopDiscoveryFailed(
-                                    clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                        break;
-                    case NsdManager.REGISTER_SERVICE:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onRegisterServiceFailedImmediately(clientRequestId,
-                                    NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
-                        }
-                        break;
-                    case NsdManager.UNREGISTER_SERVICE:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onUnregisterServiceFailed(
-                                    clientRequestId, NsdManager.FAILURE_INTERNAL_ERROR);
-                        }
-                        break;
-                    case NsdManager.RESOLVE_SERVICE:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onResolveServiceFailedImmediately(clientRequestId,
-                                    NsdManager.FAILURE_INTERNAL_ERROR, true /* isLegacy */);
-                        }
-                        break;
-                    case NsdManager.STOP_RESOLUTION:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onStopResolutionFailed(
-                                    clientRequestId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
-                        }
-                        break;
-                    case NsdManager.REGISTER_SERVICE_CALLBACK:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cInfo.onServiceInfoCallbackRegistrationFailed(
-                                    clientRequestId, NsdManager.FAILURE_BAD_PARAMETERS);
-                        }
-                        break;
-                    case NsdManager.DAEMON_CLEANUP:
-                        maybeStopDaemon();
-                        break;
-                    // This event should be only sent by the legacy (target SDK < S) clients.
-                    // Mark the sending client as legacy.
-                    case NsdManager.DAEMON_STARTUP:
-                        cInfo = getClientInfoForReply(msg);
-                        if (cInfo != null) {
-                            cancelStop();
-                            cInfo.setPreSClient();
-                            mLegacyClientCount += 1;
-                            maybeStartDaemon();
-                        }
-                        break;
-                    default:
-                        Log.e(TAG, "Unhandled " + msg);
-                        return NOT_HANDLED;
-                }
-                return HANDLED;
-            }
-
-            private ClientInfo getClientInfoForReply(Message msg) {
-                final ListenerArgs args = (ListenerArgs) msg.obj;
-                return mClients.get(args.connector);
-            }
-        }
-
         class EnabledState extends State {
             @Override
             public void enter() {
@@ -793,6 +681,11 @@
                 removeRequestMap(clientRequestId, transactionId, clientInfo);
             }
 
+            private ClientInfo getClientInfoForReply(Message msg) {
+                final ListenerArgs args = (ListenerArgs) msg.obj;
+                return mClients.get(args.connector);
+            }
+
             @Override
             public boolean processMessage(Message msg) {
                 final ClientInfo clientInfo;
@@ -1214,7 +1107,51 @@
                     case NsdManager.UNREGISTER_OFFLOAD_ENGINE:
                         mOffloadEngines.unregister((IOffloadEngine) msg.obj);
                         break;
+                    case NsdManager.REGISTER_CLIENT:
+                        final ConnectorArgs arg = (ConnectorArgs) msg.obj;
+                        final INsdManagerCallback cb = arg.callback;
+                        try {
+                            cb.asBinder().linkToDeath(arg.connector, 0);
+                            final String tag = "Client" + arg.uid + "-" + mClientNumberId++;
+                            final NetworkNsdReportedMetrics metrics =
+                                    mDeps.makeNetworkNsdReportedMetrics(
+                                            (int) mClock.elapsedRealtime());
+                            clientInfo = new ClientInfo(cb, arg.uid, arg.useJavaBackend,
+                                    mServiceLogs.forSubComponent(tag), metrics);
+                            mClients.put(arg.connector, clientInfo);
+                        } catch (RemoteException e) {
+                            Log.w(TAG, "Client request id " + clientRequestId
+                                    + " has already died");
+                        }
+                        break;
+                    case NsdManager.UNREGISTER_CLIENT:
+                        final NsdServiceConnector connector = (NsdServiceConnector) msg.obj;
+                        clientInfo = mClients.remove(connector);
+                        if (clientInfo != null) {
+                            clientInfo.expungeAllRequests();
+                            if (clientInfo.isPreSClient()) {
+                                mLegacyClientCount -= 1;
+                            }
+                        }
+                        maybeStopMonitoringSocketsIfNoActiveRequest();
+                        maybeScheduleStop();
+                        break;
+                    case NsdManager.DAEMON_CLEANUP:
+                        maybeStopDaemon();
+                        break;
+                    // This event should be only sent by the legacy (target SDK < S) clients.
+                    // Mark the sending client as legacy.
+                    case NsdManager.DAEMON_STARTUP:
+                        clientInfo = getClientInfoForReply(msg);
+                        if (clientInfo != null) {
+                            cancelStop();
+                            clientInfo.setPreSClient();
+                            mLegacyClientCount += 1;
+                            maybeStartDaemon();
+                        }
+                        break;
                     default:
+                        Log.wtf(TAG, "Unhandled " + msg);
                         return NOT_HANDLED;
                 }
                 return HANDLED;
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index e07d380..42a6b0d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -68,6 +68,8 @@
 
     @NonNull
     private final SharedLog mSharedLog;
+    @NonNull
+    private final byte[] mPacketCreationBuffer;
 
     /**
      * Callbacks called by {@link MdnsInterfaceAdvertiser} to report status updates.
@@ -205,6 +207,7 @@
         mCbHandler = new Handler(looper);
         mReplySender = deps.makeReplySender(sharedLog.getTag(), looper, socket,
                 packetCreationBuffer, sharedLog);
+        mPacketCreationBuffer = packetCreationBuffer;
         mAnnouncer = deps.makeMdnsAnnouncer(sharedLog.getTag(), looper, mReplySender,
                 mAnnouncingCallback, sharedLog);
         mProber = deps.makeMdnsProber(sharedLog.getTag(), looper, mReplySender, mProbingCallback,
@@ -390,12 +393,13 @@
      * @param serviceId The serviceId.
      * @return the raw offload payload
      */
+    @NonNull
     public byte[] getRawOffloadPayload(int serviceId) {
         try {
-            return MdnsUtils.createRawDnsPacket(mReplySender.getPacketCreationBuffer(),
+            return MdnsUtils.createRawDnsPacket(mPacketCreationBuffer,
                     mRecordRepository.getOffloadPacket(serviceId));
         } catch (IOException | IllegalArgumentException e) {
-            mSharedLog.wtf("Cannot create rawOffloadPacket: " + e.getMessage());
+            mSharedLog.wtf("Cannot create rawOffloadPacket: ", e);
             return new byte[0];
         }
     }
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
index abf5d99..ea3af5e 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsReplySender.java
@@ -99,11 +99,6 @@
         return PACKET_SENT;
     }
 
-    /** Get the packetCreationBuffer */
-    public byte[] getPacketCreationBuffer() {
-        return mPacketCreationBuffer;
-    }
-
     /**
      * Cancel all pending sends.
      */
diff --git a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
index d0f3d9a..4d79f9d 100644
--- a/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
+++ b/service-t/src/com/android/server/connectivity/mdns/util/MdnsUtils.java
@@ -35,6 +35,7 @@
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetEncoder;
 import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -211,9 +212,7 @@
         }
 
         final int len = writer.getWritePosition();
-        final byte[] outBuffer = new byte[len];
-        System.arraycopy(packetCreationBuffer, 0, outBuffer, 0, len);
-        return outBuffer;
+        return Arrays.copyOfRange(packetCreationBuffer, 0, len);
     }
 
     /**
diff --git a/service-t/src/com/android/server/net/NetworkStatsObservers.java b/service-t/src/com/android/server/net/NetworkStatsObservers.java
index 1cd670a..21cf351 100644
--- a/service-t/src/com/android/server/net/NetworkStatsObservers.java
+++ b/service-t/src/com/android/server/net/NetworkStatsObservers.java
@@ -142,6 +142,11 @@
 
     @VisibleForTesting
     protected Looper getHandlerLooperLocked() {
+        // TODO: Currently, callbacks are dispatched on this thread if the caller register
+        //  callback without supplying a Handler. To ensure that the service handler thread
+        //  is not blocked by client code, the observers must create their own thread. Once
+        //  all callbacks are dispatched outside of the handler thread, the service handler
+        //  thread can be used here.
         HandlerThread handlerThread = new HandlerThread(TAG);
         handlerThread.start();
         return handlerThread.getLooper();
diff --git a/service/Android.bp b/service/Android.bp
index 250693f..7def200 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -185,7 +185,7 @@
         "androidx.annotation_annotation",
         "connectivity-net-module-utils-bpf",
         "connectivity_native_aidl_interface-lateststable-java",
-        "dnsresolver_aidl_interface-V11-java",
+        "dnsresolver_aidl_interface-V12-java",
         "modules-utils-shell-command-handler",
         "net-utils-device-common",
         "net-utils-device-common-ip",
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index aed7875..f52a1a2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -5274,7 +5274,14 @@
 
     private boolean isNetworkPotentialSatisfier(
             @NonNull final NetworkAgentInfo candidate, @NonNull final NetworkRequestInfo nri) {
-        // listen requests won't keep up a network satisfying it. If this is not a multilayer
+        // While destroyed network sometimes satisfy requests (including occasionally newly
+        // satisfying requests), *potential* satisfiers are networks that might beat a current
+        // champion if they validate. As such, a destroyed network is never a potential satisfier,
+        // because it's never a good idea to keep a destroyed network in case it validates.
+        // For example, declaring it a potential satisfier would keep an unvalidated destroyed
+        // candidate after it's been replaced by another unvalidated network.
+        if (candidate.isDestroyed()) return false;
+        // Listen requests won't keep up a network satisfying it. If this is not a multilayer
         // request, return immediately. For multilayer requests, check to see if any of the
         // multilayer requests may have a potential satisfier.
         if (!nri.isMultilayerRequest() && (nri.mRequests.get(0).isListen()
@@ -5292,8 +5299,12 @@
             if (req.isListen() || req.isListenForBest()) {
                 continue;
             }
-            // If this Network is already the best Network for a request, or if
-            // there is hope for it to become one if it validated, then it is needed.
+            // If there is hope for this network might validate and subsequently become the best
+            // network for that request, then it is needed. Note that this network can't already
+            // be the best for this request, or it would be the current satisfier, and therefore
+            // there would be no need to call this method to find out if it is a *potential*
+            // satisfier ("unneeded", the only caller, only calls this if this network currently
+            // satisfies no request).
             if (candidate.satisfies(req)) {
                 // As soon as a network is found that satisfies a request, return. Specifically for
                 // multilayer requests, returning as soon as a NetworkAgentInfo satisfies a request
@@ -8899,7 +8910,7 @@
         // This network might have been underlying another network. Propagate its capabilities.
         propagateUnderlyingNetworkCapabilities(nai.network);
 
-        if (!newNc.equalsTransportTypes(prevNc)) {
+        if (meteredChanged || !newNc.equalsTransportTypes(prevNc)) {
             mDnsManager.updateCapabilitiesForNetwork(nai.network.getNetId(), newNc);
         }
 
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 11345d3..cda8d06 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -692,8 +692,10 @@
 
     /**
      * Dump AutomaticOnOffKeepaliveTracker state.
+     * This should be only be called in ConnectivityService handler thread.
      */
     public void dump(IndentingPrintWriter pw) {
+        ensureRunningOnHandlerThread();
         mKeepaliveTracker.dump(pw);
         // Reading DeviceConfig will check if the calling uid and calling package name are the same.
         // Clear calling identity to align the calling uid and package so that it won't fail if cts
@@ -712,6 +714,9 @@
         pw.increaseIndent();
         mEventLog.reverseDump(pw);
         pw.decreaseIndent();
+
+        pw.println();
+        mKeepaliveStatsTracker.dump(pw);
     }
 
     /**
diff --git a/service/src/com/android/server/connectivity/DnsManager.java b/service/src/com/android/server/connectivity/DnsManager.java
index 81b2289..894bcc4 100644
--- a/service/src/com/android/server/connectivity/DnsManager.java
+++ b/service/src/com/android/server/connectivity/DnsManager.java
@@ -326,10 +326,14 @@
     }
 
     /**
-     * When creating a new network or transport types are changed in a specific network,
-     * capabilities are always saved to a hashMap before update dns config.
-     * When destroying network, the specific network will be removed from the hashMap.
-     * The hashMap is always accessed on the same thread.
+     * Update {@link NetworkCapabilities} stored in this instance.
+     *
+     * In order to ensure that the resolver has access to necessary information when other events
+     * occur, capabilities are always saved to a hashMap before updating the DNS configuration
+     * whenever a new network is created, transport types are modified, or metered capabilities are
+     * altered for a network. When a network is destroyed, the corresponding entry is removed from
+     * the hashMap. To prevent concurrency issues, the hashMap should always be accessed from the
+     * same thread.
      */
     public void updateCapabilitiesForNetwork(int netId, @NonNull final NetworkCapabilities nc) {
         mNetworkCapabilitiesMap.put(netId, nc);
@@ -385,6 +389,7 @@
                 : useTls ? paramsParcel.servers  // Opportunistic
                 : new String[0];            // Off
         paramsParcel.transportTypes = nc.getTransportTypes();
+        paramsParcel.meteredNetwork = nc.isMetered();
         // Prepare to track the validation status of the DNS servers in the
         // resolver config when private DNS is in opportunistic or strict mode.
         if (useTls) {
@@ -398,12 +403,13 @@
         }
 
         Log.d(TAG, String.format("sendDnsConfigurationForNetwork(%d, %s, %s, %d, %d, %d, %d, "
-                + "%d, %d, %s, %s)", paramsParcel.netId, Arrays.toString(paramsParcel.servers),
-                Arrays.toString(paramsParcel.domains), paramsParcel.sampleValiditySeconds,
-                paramsParcel.successThreshold, paramsParcel.minSamples,
-                paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
+                + "%d, %d, %s, %s, %s, %b)", paramsParcel.netId,
+                Arrays.toString(paramsParcel.servers), Arrays.toString(paramsParcel.domains),
+                paramsParcel.sampleValiditySeconds, paramsParcel.successThreshold,
+                paramsParcel.minSamples, paramsParcel.maxSamples, paramsParcel.baseTimeoutMsec,
                 paramsParcel.retryCount, paramsParcel.tlsName,
-                Arrays.toString(paramsParcel.tlsServers)));
+                Arrays.toString(paramsParcel.tlsServers),
+                Arrays.toString(paramsParcel.transportTypes), paramsParcel.meteredNetwork));
 
         try {
             mDnsResolver.setResolverConfiguration(paramsParcel);
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 0c2ed18..7a8b41b 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -34,6 +34,7 @@
 import android.telephony.SubscriptionInfo;
 import android.telephony.SubscriptionManager;
 import android.telephony.TelephonyManager;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
@@ -73,6 +74,9 @@
 public class KeepaliveStatsTracker {
     private static final String TAG = KeepaliveStatsTracker.class.getSimpleName();
     private static final int INVALID_KEEPALIVE_ID = -1;
+    // 1 hour acceptable deviation in metrics collection duration time.
+    private static final long MAX_EXPECTED_DURATION_MS =
+            AutomaticOnOffKeepaliveTracker.METRICS_COLLECTION_DURATION_MS + 1 * 60 * 60 * 1_000L;
 
     @NonNull private final Handler mConnectivityServiceHandler;
     @NonNull private final Dependencies mDependencies;
@@ -709,6 +713,36 @@
         return mEnabled.get();
     }
 
+    /**
+     * Checks the DailykeepaliveInfoReported for the following:
+     * 1. total active durations/lifetimes <= total registered durations/lifetimes.
+     * 2. Total time in Durations == total time in Carrier lifetime stats
+     * 3. The total elapsed real time spent is within expectations.
+     */
+    @VisibleForTesting
+    public boolean allMetricsExpected(DailykeepaliveInfoReported dailyKeepaliveInfoReported) {
+        int totalRegistered = 0;
+        int totalActiveDurations = 0;
+        int totalTimeSpent = 0;
+        for (DurationForNumOfKeepalive durationForNumOfKeepalive: dailyKeepaliveInfoReported
+                .getDurationPerNumOfKeepalive().getDurationForNumOfKeepaliveList()) {
+            final int n = durationForNumOfKeepalive.getNumOfKeepalive();
+            totalRegistered += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec() * n;
+            totalActiveDurations += durationForNumOfKeepalive.getKeepaliveActiveDurationsMsec() * n;
+            totalTimeSpent += durationForNumOfKeepalive.getKeepaliveRegisteredDurationsMsec();
+        }
+        int totalLifetimes = 0;
+        int totalActiveLifetimes = 0;
+        for (KeepaliveLifetimeForCarrier keepaliveLifetimeForCarrier: dailyKeepaliveInfoReported
+                .getKeepaliveLifetimePerCarrier().getKeepaliveLifetimeForCarrierList()) {
+            totalLifetimes += keepaliveLifetimeForCarrier.getLifetimeMsec();
+            totalActiveLifetimes += keepaliveLifetimeForCarrier.getActiveLifetimeMsec();
+        }
+        return totalActiveDurations <= totalRegistered && totalActiveLifetimes <= totalLifetimes
+                && totalLifetimes == totalRegistered && totalActiveLifetimes == totalActiveDurations
+                && totalTimeSpent <= MAX_EXPECTED_DURATION_MS;
+    }
+
     /** Writes the stored metrics to ConnectivityStatsLog and resets. */
     public void writeAndResetMetrics() {
         ensureRunningOnHandlerThread();
@@ -724,9 +758,21 @@
         }
 
         final DailykeepaliveInfoReported dailyKeepaliveInfoReported = buildAndResetMetrics();
+        if (!allMetricsExpected(dailyKeepaliveInfoReported)) {
+            Log.wtf(TAG, "Unexpected metrics values: " + dailyKeepaliveInfoReported.toString());
+        }
         mDependencies.writeStats(dailyKeepaliveInfoReported);
     }
 
+    /** Dump KeepaliveStatsTracker state. */
+    public void dump(IndentingPrintWriter pw) {
+        ensureRunningOnHandlerThread();
+        pw.println("KeepaliveStatsTracker enabled: " + isEnabled());
+        pw.increaseIndent();
+        pw.println(buildKeepaliveMetrics().toString());
+        pw.decreaseIndent();
+    }
+
     private void ensureRunningOnHandlerThread() {
         if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
             throw new IllegalStateException(
diff --git a/staticlibs/device/com/android/net/module/util/Struct.java b/staticlibs/device/com/android/net/module/util/Struct.java
index dc0d19b..ff7a711 100644
--- a/staticlibs/device/com/android/net/module/util/Struct.java
+++ b/staticlibs/device/com/android/net/module/util/Struct.java
@@ -422,7 +422,14 @@
                 final byte[] address = new byte[isIpv6 ? 16 : 4];
                 buf.get(address);
                 try {
-                    value = InetAddress.getByAddress(address);
+                    if (isIpv6) {
+                        // Using Inet6Address.getByAddress since InetAddress.getByAddress converts
+                        // v4-mapped v6 address to v4 address internally and returns Inet4Address.
+                        value = Inet6Address.getByAddress(
+                                null /* host */, address, -1 /* scope_id */);
+                    } else {
+                        value = InetAddress.getByAddress(address);
+                    }
                 } catch (UnknownHostException e) {
                     throw new IllegalArgumentException("illegal length of IP address", e);
                 }
diff --git a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
index 40fc59f..4b27a97 100644
--- a/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
+++ b/staticlibs/framework/com/android/net/module/util/InetAddressUtils.java
@@ -21,6 +21,7 @@
 import android.util.Log;
 
 
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
@@ -32,6 +33,7 @@
 public class InetAddressUtils {
 
     private static final String TAG = InetAddressUtils.class.getSimpleName();
+    private static final int INET4_ADDR_LENGTH = 4;
     private static final int INET6_ADDR_LENGTH = 16;
 
     /**
@@ -93,5 +95,29 @@
         }
     }
 
+    /**
+     * Create a v4-mapped v6 address from v4 address
+     *
+     * @param v4Addr Inet4Address which is converted to v4-mapped v6 address
+     * @return v4-mapped v6 address
+     */
+    public static Inet6Address v4MappedV6Address(@NonNull final Inet4Address v4Addr) {
+        final byte[] v6AddrBytes = new byte[INET6_ADDR_LENGTH];
+        v6AddrBytes[10] = (byte) 0xFF;
+        v6AddrBytes[11] = (byte) 0xFF;
+        System.arraycopy(v4Addr.getAddress(), 0 /* srcPos */, v6AddrBytes, 12 /* dstPos */,
+                INET4_ADDR_LENGTH);
+        try {
+            // Using Inet6Address.getByAddress since InetAddress.getByAddress converts v4-mapped v6
+            // address to v4 address internally and returns Inet4Address
+            return Inet6Address.getByAddress(null /* host */, v6AddrBytes, -1 /* scope_id */);
+        } catch (UnknownHostException impossible) {
+            // getByAddress throws UnknownHostException when the argument is the invalid length
+            // but INET6_ADDR_LENGTH(16) is the valid length.
+            Log.wtf(TAG, "Failed to generate v4-mapped v6 address from " + v4Addr, impossible);
+            return null;
+        }
+    }
+
     private InetAddressUtils() {}
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
index bb2b933..66427fc 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/InetAddressUtilsTest.java
@@ -18,6 +18,7 @@
 
 import static junit.framework.Assert.assertEquals;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -30,6 +31,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.net.Inet4Address;
 import java.net.Inet6Address;
 import java.net.InetAddress;
 
@@ -92,4 +94,17 @@
         assertEquals(localAddrStr + "%" + scopeId, updatedLocalAddr.getHostAddress());
         assertEquals(scopeId, updatedLocalAddr.getScopeId());
     }
+
+    @Test
+    public void testV4MappedV6Address() throws Exception {
+        final Inet4Address v4Addr = (Inet4Address) InetAddress.getByName("192.0.2.1");
+        final Inet6Address v4MappedV6Address = InetAddressUtils.v4MappedV6Address(v4Addr);
+        final byte[] expectedAddrBytes = new byte[]{
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
+                (byte) 0x00, (byte) 0x00, (byte) 0xff, (byte) 0xff,
+                (byte) 0xc0, (byte) 0x00, (byte) 0x02, (byte) 0x01,
+        };
+        assertArrayEquals(expectedAddrBytes, v4MappedV6Address.getAddress());
+    }
 }
diff --git a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
index b4da043..a39b7a3 100644
--- a/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/StructTest.java
@@ -765,6 +765,14 @@
                 msg.writeToBytes(ByteOrder.BIG_ENDIAN));
     }
 
+    @Test
+    public void testV4MappedV6Address() {
+        final IpAddressMessage msg = doParsingMessageTest("c0a86401"
+                + "00000000000000000000ffffc0a86401", IpAddressMessage.class, ByteOrder.BIG_ENDIAN);
+        assertEquals(TEST_IPV4_ADDRESS, msg.ipv4Address);
+        assertEquals(InetAddressUtils.v4MappedV6Address(TEST_IPV4_ADDRESS), msg.ipv6Address);
+    }
+
     public static class WrongIpAddressType extends Struct {
         @Field(order = 0, type = Type.Ipv4Address) public byte[] ipv4Address;
         @Field(order = 1, type = Type.Ipv6Address) public byte[] ipv6Address;
diff --git a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
index 0f6fa48..440b836 100644
--- a/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/testutils/HandlerUtilsTest.kt
@@ -27,7 +27,7 @@
 import org.junit.runners.JUnit4
 
 private const val ATTEMPTS = 50 // Causes testWaitForIdle to take about 150ms on aosp_crosshatch-eng
-private const val TIMEOUT_MS = 200
+private const val TIMEOUT_MS = 1000
 
 @RunWith(JUnit4::class)
 class HandlerUtilsTest {
diff --git a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
index d92fb01..5893de7 100644
--- a/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
+++ b/tests/cts/hostside/app/src/com/android/cts/net/hostside/AbstractRestrictBackgroundNetworkTestCase.java
@@ -752,27 +752,24 @@
         assertDelayedShellCommand("dumpsys deviceidle get deep", enabled ? "IDLE" : "ACTIVE");
     }
 
-    protected void setAppIdle(boolean enabled) throws Exception {
+    protected void setAppIdle(boolean isIdle) throws Exception {
+        setAppIdleNoAssert(isIdle);
+        assertAppIdle(isIdle);
+    }
+
+    protected void setAppIdleNoAssert(boolean isIdle) throws Exception {
         if (!isAppStandbySupported()) {
             return;
         }
-        Log.i(TAG, "Setting app idle to " + enabled);
-        executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
-        assertAppIdle(enabled);
+        Log.i(TAG, "Setting app idle to " + isIdle);
+        final String bucketName = isIdle ? "rare" : "active";
+        executeSilentShellCommand("am set-standby-bucket " + TEST_APP2_PKG + " " + bucketName);
     }
 
-    protected void setAppIdleNoAssert(boolean enabled) throws Exception {
-        if (!isAppStandbySupported()) {
-            return;
-        }
-        Log.i(TAG, "Setting app idle to " + enabled);
-        executeSilentShellCommand("am set-inactive " + TEST_APP2_PKG + " " + enabled );
-    }
-
-    protected void assertAppIdle(boolean enabled) throws Exception {
+    protected void assertAppIdle(boolean isIdle) throws Exception {
         try {
             assertDelayedShellCommand("am get-inactive " + TEST_APP2_PKG,
-                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + enabled);
+                    30 /* maxTries */, 1 /* napTimeSeconds */, "Idle=" + isIdle);
         } catch (Throwable e) {
             throw e;
         }
diff --git a/tests/native/utilities/firewall.cpp b/tests/native/utilities/firewall.cpp
index e4669cb..6e35d07 100644
--- a/tests/native/utilities/firewall.cpp
+++ b/tests/native/utilities/firewall.cpp
@@ -27,6 +27,9 @@
 
     result = mUidOwnerMap.init(UID_OWNER_MAP_PATH);
     EXPECT_RESULT_OK(result) << "init mUidOwnerMap failed";
+
+    result = mDataSaverEnabledMap.init(DATA_SAVER_ENABLED_MAP_PATH);
+    EXPECT_RESULT_OK(result) << "init mDataSaverEnabledMap failed";
 }
 
 Firewall* Firewall::getInstance() {
@@ -116,3 +119,20 @@
     }
     return {};
 }
+
+Result<bool> Firewall::getDataSaverSetting() {
+    std::lock_guard guard(mMutex);
+    auto dataSaverSetting = mDataSaverEnabledMap.readValue(DATA_SAVER_ENABLED_KEY);
+    if (!dataSaverSetting.ok()) {
+        return Errorf("Cannot read the data saver setting: {}", dataSaverSetting.error().message());
+    }
+    return dataSaverSetting;
+}
+
+Result<void> Firewall::setDataSaver(bool enabled) {
+    std::lock_guard guard(mMutex);
+    auto res = mDataSaverEnabledMap.writeValue(DATA_SAVER_ENABLED_KEY, enabled, BPF_EXIST);
+    if (!res.ok()) return Errorf("Failed to set data saver: {}", res.error().message());
+
+    return {};
+}
diff --git a/tests/native/utilities/firewall.h b/tests/native/utilities/firewall.h
index 1e7e987..b3d69bf 100644
--- a/tests/native/utilities/firewall.h
+++ b/tests/native/utilities/firewall.h
@@ -33,9 +33,11 @@
     Result<void> removeRule(uint32_t uid, UidOwnerMatchType match) EXCLUDES(mMutex);
     Result<void> addUidInterfaceRules(const std::string& ifName, const std::vector<int32_t>& uids);
     Result<void> removeUidInterfaceRules(const std::vector<int32_t>& uids);
-
+    Result<bool> getDataSaverSetting();
+    Result<void> setDataSaver(bool enabled);
   private:
     BpfMap<uint32_t, uint32_t> mConfigurationMap GUARDED_BY(mMutex);
     BpfMap<uint32_t, UidOwnerValue> mUidOwnerMap GUARDED_BY(mMutex);
+    BpfMap<uint32_t, bool> mDataSaverEnabledMap GUARDED_BY(mMutex);
     std::mutex mMutex;
 };
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 97ae3bf..aae37e5 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -417,7 +417,6 @@
 import com.android.server.connectivity.QosCallbackTracker;
 import com.android.server.connectivity.TcpKeepaliveController;
 import com.android.server.connectivity.UidRangeUtils;
-import com.android.server.connectivity.Vpn;
 import com.android.server.connectivity.VpnProfileStore;
 import com.android.server.net.NetworkPinner;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -1497,13 +1496,8 @@
         return uidRangesForUids(CollectionUtils.toIntArray(uids));
     }
 
-    private static Looper startHandlerThreadAndReturnLooper() {
-        final HandlerThread handlerThread = new HandlerThread("MockVpnThread");
-        handlerThread.start();
-        return handlerThread.getLooper();
-    }
-
-    private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork {
+    // Helper class to mock vpn interaction.
+    private class MockVpn implements TestableNetworkCallback.HasNetwork {
         // Note : Please do not add any new instrumentation here. If you need new instrumentation,
         // please add it in CSTest and use subclasses of CSTest instead of adding more
         // tools in ConnectivityServiceTest.
@@ -1511,45 +1505,23 @@
         // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
         // not inherit from NetworkAgent.
         private TestNetworkAgentWrapper mMockNetworkAgent;
+        // Initialize a stored NetworkCapabilities following the defaults of VPN. The TransportInfo
+        // should at least be updated to a valid VPN type before usage, see registerAgent(...).
+        private NetworkCapabilities mNetworkCapabilities = new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
+                .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+                .setTransportInfo(new VpnTransportInfo(
+                        VpnManager.TYPE_VPN_NONE,
+                        null /* sessionId */,
+                        false /* bypassable */,
+                        false /* longLivedTcpConnectionsExpensive */))
+                .build();
         private boolean mAgentRegistered = false;
 
         private int mVpnType = VpnManager.TYPE_VPN_SERVICE;
-        private UnderlyingNetworkInfo mUnderlyingNetworkInfo;
         private String mSessionKey;
 
-        public MockVpn(int userId) {
-            super(startHandlerThreadAndReturnLooper(), mServiceContext,
-                    new Dependencies() {
-                        @Override
-                        public boolean isCallerSystem() {
-                            return true;
-                        }
-
-                        @Override
-                        public DeviceIdleInternal getDeviceIdleInternal() {
-                            return mDeviceIdleInternal;
-                        }
-                    },
-                    mNetworkManagementService, mMockNetd, userId, mVpnProfileStore,
-                    new SystemServices(mServiceContext) {
-                        @Override
-                        public String settingsSecureGetStringForUser(String key, int userId) {
-                            switch (key) {
-                                // Settings keys not marked as @Readable are not readable from
-                                // non-privileged apps, unless marked as testOnly=true
-                                // (atest refuses to install testOnly=true apps), even if mocked
-                                // in the content provider, because
-                                // Settings.Secure.NameValueCache#getStringForUser checks the key
-                                // before querying the mock settings provider.
-                                case Settings.Secure.ALWAYS_ON_VPN_APP:
-                                    return null;
-                                default:
-                                    return super.settingsSecureGetStringForUser(key, userId);
-                            }
-                        }
-                    }, new Ikev2SessionCreator());
-        }
-
         public void setUids(Set<UidRange> uids) {
             mNetworkCapabilities.setUids(UidRange.toIntRanges(uids));
             if (mAgentRegistered) {
@@ -1561,7 +1533,6 @@
             mVpnType = vpnType;
         }
 
-        @Override
         public Network getNetwork() {
             return (mMockNetworkAgent == null) ? null : mMockNetworkAgent.getNetwork();
         }
@@ -1570,7 +1541,6 @@
             return null == mMockNetworkAgent ? null : mMockNetworkAgent.getNetworkAgentConfig();
         }
 
-        @Override
         public int getActiveVpnType() {
             return mVpnType;
         }
@@ -1584,14 +1554,11 @@
         private void registerAgent(boolean isAlwaysMetered, Set<UidRange> uids, LinkProperties lp)
                 throws Exception {
             if (mAgentRegistered) throw new IllegalStateException("already registered");
-            updateState(NetworkInfo.DetailedState.CONNECTING, "registerAgent");
-            mConfig = new VpnConfig();
-            mConfig.session = "MySession12345";
+            final String session = "MySession12345";
             setUids(uids);
             if (!isAlwaysMetered) mNetworkCapabilities.addCapability(NET_CAPABILITY_NOT_METERED);
-            mInterface = VPN_IFNAME;
             mNetworkCapabilities.setTransportInfo(new VpnTransportInfo(getActiveVpnType(),
-                    mConfig.session));
+                    session));
             mMockNetworkAgent = new TestNetworkAgentWrapper(TRANSPORT_VPN, lp,
                     mNetworkCapabilities);
             mMockNetworkAgent.waitForIdle(TIMEOUT_MS);
@@ -1605,9 +1572,7 @@
             mAgentRegistered = true;
             verify(mMockNetd).networkCreate(nativeNetworkConfigVpn(getNetwork().netId,
                     !mMockNetworkAgent.isBypassableVpn(), mVpnType));
-            updateState(NetworkInfo.DetailedState.CONNECTED, "registerAgent");
             mNetworkCapabilities.set(mMockNetworkAgent.getNetworkCapabilities());
-            mNetworkAgent = mMockNetworkAgent.getNetworkAgent();
         }
 
         private void registerAgent(Set<UidRange> uids) throws Exception {
@@ -1667,23 +1632,20 @@
         public void disconnect() {
             if (mMockNetworkAgent != null) {
                 mMockNetworkAgent.disconnect();
-                updateState(NetworkInfo.DetailedState.DISCONNECTED, "disconnect");
             }
             mAgentRegistered = false;
             setUids(null);
             // Remove NET_CAPABILITY_INTERNET or MockNetworkAgent will refuse to connect later on.
             mNetworkCapabilities.removeCapability(NET_CAPABILITY_INTERNET);
-            mInterface = null;
         }
 
-        private synchronized void startLegacyVpn() {
-            updateState(DetailedState.CONNECTING, "startLegacyVpn");
+        private void startLegacyVpn() {
+            // Do nothing.
         }
 
         // Mock the interaction of IkeV2VpnRunner start. In the context of ConnectivityService,
         // setVpnDefaultForUids() is the main interaction and a sessionKey is stored.
-        private synchronized void startPlatformVpn() {
-            updateState(DetailedState.CONNECTING, "startPlatformVpn");
+        private void startPlatformVpn() {
             mSessionKey = UUID.randomUUID().toString();
             // Assuming no disallowed applications
             final Set<Range<Integer>> ranges = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
@@ -1692,7 +1654,6 @@
             waitForIdle();
         }
 
-        @Override
         public void startLegacyVpnPrivileged(VpnProfile profile,
                 @Nullable Network underlying, @NonNull LinkProperties egress) {
             switch (profile.type) {
@@ -1714,8 +1675,7 @@
             }
         }
 
-        @Override
-        public synchronized void stopVpnRunnerPrivileged() {
+        public void stopVpnRunnerPrivileged() {
             if (mSessionKey != null) {
                 // Clear vpn network preference.
                 mCm.setVpnDefaultForUids(mSessionKey, Collections.EMPTY_LIST);
@@ -1724,20 +1684,7 @@
             disconnect();
         }
 
-        @Override
-        public synchronized UnderlyingNetworkInfo getUnderlyingNetworkInfo() {
-            if (mUnderlyingNetworkInfo != null) return mUnderlyingNetworkInfo;
-
-            return super.getUnderlyingNetworkInfo();
-        }
-
-        private synchronized void setUnderlyingNetworkInfo(
-                UnderlyingNetworkInfo underlyingNetworkInfo) {
-            mUnderlyingNetworkInfo = underlyingNetworkInfo;
-        }
-
-        @Override
-        public synchronized boolean setUnderlyingNetworks(@Nullable Network[] networks) {
+        public boolean setUnderlyingNetworks(@Nullable Network[] networks) {
             if (!mAgentRegistered) return false;
             mMockNetworkAgent.setUnderlyingNetworks(
                     (networks == null) ? null : Arrays.asList(networks));
@@ -1774,11 +1721,6 @@
         waitForIdle();
     }
 
-    private void mockVpn(int uid) {
-        int userId = UserHandle.getUserId(uid);
-        mMockVpn = new MockVpn(userId);
-    }
-
     private void mockUidNetworkingBlocked() {
         doAnswer(i -> isUidBlocked(mBlockedReasons, i.getArgument(1))
         ).when(mNetworkPolicyManager).isUidNetworkingBlocked(anyInt(), anyBoolean());
@@ -1990,7 +1932,7 @@
         mService.systemReadyInternal();
         verify(mMockDnsResolver).registerUnsolicitedEventListener(any());
 
-        mockVpn(Process.myUid());
+        mMockVpn = new MockVpn();
         mCm.bindProcessToNetwork(null);
         mQosCallbackTracker = mock(QosCallbackTracker.class);
 
@@ -4346,7 +4288,9 @@
         testFactory.terminate();
         testFactory.assertNoRequestChanged();
         if (networkCallback != null) mCm.unregisterNetworkCallback(networkCallback);
-        handlerThread.quit();
+
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     @Test
@@ -4407,6 +4351,8 @@
         expectNoRequestChanged(testFactoryAll); // still seeing the request
 
         mWiFiAgent.disconnect();
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     @Test
@@ -4440,7 +4386,8 @@
                 }
             }
         }
-        handlerThread.quit();
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     @Test
@@ -6061,7 +6008,8 @@
             testFactory.assertNoRequestChanged();
         } finally {
             mCm.unregisterNetworkCallback(cellNetworkCallback);
-            handlerThread.quit();
+            handlerThread.quitSafely();
+            handlerThread.join();
         }
     }
 
@@ -6651,7 +6599,8 @@
             }
         } finally {
             testFactory.terminate();
-            handlerThread.quit();
+            handlerThread.quitSafely();
+            handlerThread.join();
         }
     }
 
@@ -10268,7 +10217,6 @@
 
         // Init lockdown state to simulate LockdownVpnTracker behavior.
         mCm.setLegacyLockdownVpnEnabled(true);
-        mMockVpn.setEnableTeardown(false);
         final List<Range<Integer>> ranges =
                 intRangesPrimaryExcludingUids(Collections.EMPTY_LIST /* excludedeUids */);
         mCm.setRequireVpnForUids(true /* requireVpn */, ranges);
@@ -12594,9 +12542,6 @@
         mMockVpn.establish(new LinkProperties(), vpnOwnerUid, vpnRange);
         assertVpnUidRangesUpdated(true, vpnRange, vpnOwnerUid);
 
-        final UnderlyingNetworkInfo underlyingNetworkInfo =
-                new UnderlyingNetworkInfo(vpnOwnerUid, VPN_IFNAME, new ArrayList<>());
-        mMockVpn.setUnderlyingNetworkInfo(underlyingNetworkInfo);
         mDeps.setConnectionOwnerUid(42);
     }
 
@@ -15376,6 +15321,8 @@
         expectNoRequestChanged(oemPaidFactory);
         internetFactory.expectRequestAdd();
         mCm.unregisterNetworkCallback(wifiCallback);
+        handlerThread.quitSafely();
+        handlerThread.join();
     }
 
     /**
@@ -15740,6 +15687,8 @@
             assertTrue(testFactory.getMyStartRequested());
         } finally {
             testFactory.terminate();
+            handlerThread.quitSafely();
+            handlerThread.join();
         }
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index 986c389..8e19c01 100644
--- a/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -77,6 +77,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.internal.util.IndentingPrintWriter;
 import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker.AutomaticOnOffKeepalive;
 import com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
 import com.android.testutils.DevSdkIgnoreRule;
@@ -94,6 +95,7 @@
 import org.mockito.MockitoAnnotations;
 
 import java.io.FileDescriptor;
+import java.io.StringWriter;
 import java.net.Inet4Address;
 import java.net.InetAddress;
 import java.net.Socket;
@@ -974,4 +976,19 @@
         // The keepalive should be removed in AutomaticOnOffKeepaliveTracker.
         assertNull(getAutoKiForBinder(testInfo.binder));
     }
+
+    @Test
+    public void testDumpDoesNotCrash() throws Exception {
+        final TestKeepaliveInfo testInfo1 = doStartNattKeepalive();
+        final TestKeepaliveInfo testInfo2 = doStartNattKeepalive();
+        checkAndProcessKeepaliveStart(TEST_SLOT, testInfo1.kpd);
+        checkAndProcessKeepaliveStart(TEST_SLOT + 1, testInfo2.kpd);
+        final AutomaticOnOffKeepalive autoKi1  = getAutoKiForBinder(testInfo1.binder);
+        doPauseKeepalive(autoKi1);
+
+        final StringWriter stringWriter = new StringWriter();
+        final IndentingPrintWriter pw = new IndentingPrintWriter(stringWriter, "   ");
+        visibleOnHandlerThread(mTestHandler, () -> mAOOKeepaliveTracker.dump(pw));
+        assertFalse(stringWriter.toString().isEmpty());
+    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
index 9a6fd38..545ed16 100644
--- a/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/DnsManagerTest.java
@@ -139,7 +139,8 @@
         assertEquals(actual.tlsConnectTimeoutMs, expected.tlsConnectTimeoutMs);
         assertResolverOptionsEquals(actual.resolverOptions, expected.resolverOptions);
         assertContainsExactly(actual.transportTypes, expected.transportTypes);
-        assertFieldCountEquals(16, ResolverParamsParcel.class);
+        assertEquals(actual.meteredNetwork, expected.meteredNetwork);
+        assertFieldCountEquals(17, ResolverParamsParcel.class);
     }
 
     @Before
@@ -379,6 +380,7 @@
         expectedParams.tlsServers = new String[]{"3.3.3.3", "4.4.4.4"};
         expectedParams.transportTypes = TEST_TRANSPORT_TYPES;
         expectedParams.resolverOptions = null;
+        expectedParams.meteredNetwork = true;
         assertResolverParamsEquals(actualParams, expectedParams);
     }
 
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
index 90a0edd..1b964e2 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/KeepaliveStatsTrackerTest.java
@@ -37,6 +37,7 @@
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.content.BroadcastReceiver;
@@ -1293,5 +1294,18 @@
                 expectRegisteredDurations,
                 expectActiveDurations,
                 new KeepaliveCarrierStats[0]);
+
+        assertTrue(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported));
+
+        // Write time after 26 hours.
+        final int writeTime2 = 26 * 60 * 60 * 1000;
+        setElapsedRealtime(writeTime2);
+
+        visibleOnHandlerThread(mTestHandler, () -> mKeepaliveStatsTracker.writeAndResetMetrics());
+        verify(mDependencies, times(2)).writeStats(dailyKeepaliveInfoReportedCaptor.capture());
+        final DailykeepaliveInfoReported dailyKeepaliveInfoReported2 =
+                dailyKeepaliveInfoReportedCaptor.getValue();
+
+        assertFalse(mKeepaliveStatsTracker.allMetricsExpected(dailyKeepaliveInfoReported2));
     }
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
new file mode 100644
index 0000000..d40c035
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSDestroyedNetworkTests.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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
+
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import com.android.testutils.RecorderCallback.CallbackEntry.Lost
+import com.android.testutils.TestableNetworkCallback
+import org.junit.Test
+
+private const val LONG_TIMEOUT_MS = 5_000
+
+class CSDestroyedNetworkTests : CSTest() {
+    @Test
+    fun testDestroyNetworkNotKeptWhenUnvalidated() {
+        val nc = NetworkCapabilities.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build()
+
+        val nr = NetworkRequest.Builder()
+                .clearCapabilities()
+                .addTransportType(TRANSPORT_WIFI)
+                .build()
+        val cbRequest = TestableNetworkCallback()
+        val cbCallback = TestableNetworkCallback()
+        cm.requestNetwork(nr, cbRequest)
+        cm.registerNetworkCallback(nr, cbCallback)
+
+        val firstAgent = Agent(nc = nc)
+        firstAgent.connect()
+        cbCallback.expectAvailableCallbacks(firstAgent.network, validated = false)
+
+        firstAgent.unregisterAfterReplacement(LONG_TIMEOUT_MS)
+
+        val secondAgent = Agent(nc = nc)
+        secondAgent.connect()
+        cbCallback.expectAvailableCallbacks(secondAgent.network, validated = false)
+
+        cbCallback.expect<Lost>(timeoutMs = 500) { it.network == firstAgent.network }
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
index 094ded3..8860895 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -34,7 +34,6 @@
 import android.net.NetworkTestResultParcelable
 import android.net.networkstack.NetworkStackClientBase
 import android.os.HandlerThread
-import com.android.modules.utils.build.SdkLevel
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
 import com.android.testutils.RecorderCallback.CallbackEntry.Lost
 import com.android.testutils.TestableNetworkCallback
@@ -168,5 +167,6 @@
         cb.eventuallyExpect<Lost> { it.network == agent.network }
     }
 
+    fun unregisterAfterReplacement(timeoutMs: Int) = agent.unregisterAfterReplacement(timeoutMs)
     fun sendLocalNetworkConfig(lnc: LocalNetworkConfig) = agent.sendLocalNetworkConfig(lnc)
 }
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
index c477b2c..e62ac74 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsObserversTest.java
@@ -125,7 +125,7 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
 
-        mObserverHandlerThread = new HandlerThread("HandlerThread");
+        mObserverHandlerThread = new HandlerThread("NetworkStatsObserversTest");
         mObserverHandlerThread.start();
         final Looper observerLooper = mObserverHandlerThread.getLooper();
         mStatsObservers = new NetworkStatsObservers() {
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index e8d5c66..92a5b64 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -120,6 +120,7 @@
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.IBinder;
+import android.os.Looper;
 import android.os.PowerManager;
 import android.os.SimpleClock;
 import android.provider.Settings;
@@ -284,6 +285,7 @@
     private @Mock PersistentInt mImportLegacyFallbacksCounter;
     private @Mock Resources mResources;
     private Boolean mIsDebuggable;
+    private HandlerThread mObserverHandlerThread;
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -365,10 +367,23 @@
         PowerManager.WakeLock wakeLock =
                 powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
 
-        mHandlerThread = new HandlerThread("HandlerThread");
+        mHandlerThread = new HandlerThread("NetworkStatsServiceTest-HandlerThread");
         final NetworkStatsService.Dependencies deps = makeDependencies();
+        // Create a separate thread for observers to run on. This thread cannot be the same
+        // as the handler thread, because the observer callback is fired on this thread, and
+        // it should not be blocked by client code. Additionally, creating the observers
+        // object requires a looper, which can only be obtained after a thread has been started.
+        mObserverHandlerThread = new HandlerThread("NetworkStatsServiceTest-ObserversThread");
+        mObserverHandlerThread.start();
+        final Looper observerLooper = mObserverHandlerThread.getLooper();
+        final NetworkStatsObservers statsObservers = new NetworkStatsObservers() {
+            @Override
+            protected Looper getHandlerLooperLocked() {
+                return observerLooper;
+            }
+        };
         mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
-                mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
+                mClock, mSettings, mStatsFactory, statsObservers, deps);
 
         mElapsedRealtime = 0L;
 
@@ -545,8 +560,14 @@
         mSession.close();
         mService = null;
 
-        mHandlerThread.quitSafely();
-        mHandlerThread.join();
+        if (mHandlerThread != null) {
+            mHandlerThread.quitSafely();
+            mHandlerThread.join();
+        }
+        if (mObserverHandlerThread != null) {
+            mObserverHandlerThread.quitSafely();
+            mObserverHandlerThread.join();
+        }
     }
 
     private void initWifiStats(NetworkStateSnapshot snapshot) throws Exception {