Merge "Add DataElementHeader"
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index c3a7a6d..819936d 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -903,10 +903,15 @@
                 dstPort, payload);
     }
 
-    // TODO: remove this verification once upstream connected notification race is fixed.
-    // See #runUdp4Test.
-    private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
-            TetheredDevice tethered) throws Exception {
+    // TODO: remove ipv4 verification (is4To6 = false) once upstream connected notification race is
+    // fixed. See #runUdp4Test.
+    //
+    // This function sends a probe packet to downstream interface and exam the result from upstream
+    // interface to make sure ipv4 tethering is ready. Return the entire packet which received from
+    // upstream interface.
+    @NonNull
+    private byte[] probeV4TetheringConnectivity(TetheringTester tester, TetheredDevice tethered,
+            boolean is4To6) throws Exception {
         final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
                 tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                 REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
@@ -916,12 +921,17 @@
         for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
             byte[] expectedPacket = tester.testUpload(probePacket, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */,
+                // If is4To6 is true, the ipv4 probe packet would be translated to ipv6 by Clat and
+                // would see this translated ipv6 packet in upstream interface.
+                return isExpectedUdpPacket(p, false /* hasEther */, !is4To6 /* isIpv4 */,
                         TEST_REACHABILITY_PAYLOAD);
             });
-            if (expectedPacket != null) return true;
+            if (expectedPacket != null) return expectedPacket;
         }
-        return false;
+
+        fail("Can't verify " + (is4To6 ? "ipv4 to ipv6" : "ipv4") + " tethering connectivity after "
+                + TETHER_REACHABILITY_ATTEMPTS + " attempts");
+        return null;
     }
 
     private void runUdp4Test(TetheringTester tester, boolean usingBpf) throws Exception {
@@ -934,7 +944,7 @@
         // For short term plan, consider using IPv6 RA to get MAC address because the prefix comes
         // from upstream. That can guarantee that the routing is ready. Long term plan is that
         // refactors upstream connected notification from async to sync.
-        assertTrue(isIpv4TetherConnectivityVerified(tester, tethered));
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         // Send a UDP packet in original direction.
         final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
@@ -1179,32 +1189,16 @@
         return null;
     }
 
-    @Nullable
+    @NonNull
     private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
             throws Exception {
-        final ByteBuffer probePacket = buildUdpPacket(tethered.macAddr,
-                tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
-                REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
-                TEST_REACHABILITY_PAYLOAD);
-
         // Send an IPv4 UDP packet from client and check that a CLAT translated IPv6 UDP packet can
         // be found on upstream interface. Get CLAT IPv6 address from the CLAT translated IPv6 UDP
         // packet.
-        byte[] expectedPacket = null;
-        for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
-            expectedPacket = tester.verifyUpload(probePacket, p -> {
-                Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
-                return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */,
-                        TEST_REACHABILITY_PAYLOAD);
-            });
-            if (expectedPacket != null) break;
-        }
-        if (expectedPacket == null) return null;
+        byte[] expectedPacket = probeV4TetheringConnectivity(tester, tethered, true /* is4To6 */);
 
         // Above has guaranteed that the found packet is an IPv6 packet without ether header.
-        final Ipv6Header ipv6Header = Struct.parse(Ipv6Header.class,
-                ByteBuffer.wrap(expectedPacket));
-        return ipv6Header.srcIp;
+        return Struct.parse(Ipv6Header.class, ByteBuffer.wrap(expectedPacket)).srcIp;
     }
 
     // Test network topology:
@@ -1227,7 +1221,6 @@
 
         // Get CLAT IPv6 address.
         final Inet6Address clatAddr6 = getClatIpv6Address(tester, tethered);
-        assertNotNull(clatAddr6);
 
         // Send an IPv4 UDP packet in original direction.
         // IPv4 packet -- CLAT translation --> IPv6 packet
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 23af3e3..78fca29 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -25,8 +25,14 @@
     name: "bpf_connectivity_headers",
     vendor_available: false,
     host_supported: false,
-    header_libs: ["bpf_headers"],
-    export_header_lib_headers: ["bpf_headers"],
+    header_libs: [
+        "bpf_headers",
+        "netd_mainline_headers",
+    ],
+    export_header_lib_headers: [
+        "bpf_headers",
+        "netd_mainline_headers",
+    ],
     export_include_dirs: ["."],
     cflags: [
         "-Wall",
@@ -37,11 +43,8 @@
     apex_available: [
         "//apex_available:platform",
         "com.android.tethering",
-        ],
+    ],
     visibility: [
-        // TODO: remove it when NetworkStatsService is moved into the mainline module and no more
-        // calls to JNI in libservices.core.
-        "//frameworks/base/services/core/jni",
         "//packages/modules/Connectivity/netd",
         "//packages/modules/Connectivity/service",
         "//packages/modules/Connectivity/service/native/libs/libclat",
@@ -50,7 +53,6 @@
         "//packages/modules/Connectivity/tests/native",
         "//packages/modules/Connectivity/service-t/native/libs/libnetworkstats",
         "//packages/modules/Connectivity/tests/unit/jni",
-        "//system/netd/server",
         "//system/netd/tests",
     ],
 }
@@ -106,13 +108,11 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        "frameworks/libs/net/common/netd/libnetdutils/include",
-    ],
     sub_dir: "net_shared",
 }
 
 bpf {
+    // WARNING: Android T's non-updatable netd depends on 'netd' string for xt_bpf programs it loads
     name: "netd.o",
     srcs: ["netd.c"],
     btf: true,
@@ -120,8 +120,6 @@
         "-Wall",
         "-Werror",
     ],
-    include_dirs: [
-        "frameworks/libs/net/common/netd/libnetdutils/include",
-    ],
+    // WARNING: Android T's non-updatable netd depends on 'netd_shared' string for xt_bpf programs
     sub_dir: "netd_shared",
 }
diff --git a/bpf_progs/bpf_shared.h b/bpf_progs/bpf_shared.h
index 706dd1d..fd449a3 100644
--- a/bpf_progs/bpf_shared.h
+++ b/bpf_progs/bpf_shared.h
@@ -21,6 +21,11 @@
 #include <linux/in.h>
 #include <linux/in6.h>
 
+#ifdef __cplusplus
+#include <string_view>
+#include "XtBpfProgLocations.h"
+#endif
+
 // This header file is shared by eBPF kernel programs (C) and netd (C++) and
 // some of the maps are also accessed directly from Java mainline module code.
 //
@@ -98,14 +103,33 @@
 static const int CONFIGURATION_MAP_SIZE = 2;
 static const int UID_OWNER_MAP_SIZE = 2000;
 
+#ifdef __cplusplus
+
 #define BPF_NETD_PATH "/sys/fs/bpf/netd_shared/"
 
 #define BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_egress_stats"
 #define BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupskb_ingress_stats"
-#define XT_BPF_INGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf"
-#define XT_BPF_EGRESS_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf"
-#define XT_BPF_ALLOWLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf"
-#define XT_BPF_DENYLIST_PROG_PATH BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf"
+
+#define ASSERT_STRING_EQUAL(s1, s2) \
+    static_assert(std::string_view(s1) == std::string_view(s2), "mismatch vs Android T netd")
+
+/* -=-=-=-=- WARNING -=-=-=-=-
+ *
+ * These 4 xt_bpf program paths are actually defined by:
+ *   //system/netd/include/mainline/XtBpfProgLocations.h
+ * which is intentionally a non-automerged location.
+ *
+ * They are *UNCHANGEABLE* due to being hard coded in Android T's netd binary
+ * as such we have compile time asserts that things match.
+ * (which will be validated during build on mainline-prod branch against old system/netd)
+ *
+ * If you break this, netd on T will fail to start with your tethering mainline module.
+ */
+ASSERT_STRING_EQUAL(XT_BPF_INGRESS_PROG_PATH,   BPF_NETD_PATH "prog_netd_skfilter_ingress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_EGRESS_PROG_PATH,    BPF_NETD_PATH "prog_netd_skfilter_egress_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_ALLOWLIST_PROG_PATH, BPF_NETD_PATH "prog_netd_skfilter_allowlist_xtbpf");
+ASSERT_STRING_EQUAL(XT_BPF_DENYLIST_PROG_PATH,  BPF_NETD_PATH "prog_netd_skfilter_denylist_xtbpf");
+
 #define CGROUP_SOCKET_PROG_PATH BPF_NETD_PATH "prog_netd_cgroupsock_inet_create"
 
 #define TC_BPF_INGRESS_ACCOUNT_PROG_NAME "prog_netd_schedact_ingress_account"
@@ -122,6 +146,8 @@
 #define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
 #define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
 
+#endif // __cplusplus
+
 enum UidOwnerMatchType {
     NO_MATCH = 0,
     HAPPY_BOX_MATCH = (1 << 0),
@@ -164,19 +190,9 @@
 STRUCT_SIZE(UidOwnerValue, 2 * 4);  // 8
 
 // Entry in the configuration map that stores which UID rules are enabled.
-#define UID_RULES_CONFIGURATION_KEY 1
+#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 2
-
-#define BPF_CLATD_PATH "/sys/fs/bpf/net_shared/"
-
-#define CLAT_INGRESS6_PROG_RAWIP_NAME "prog_clatd_schedcls_ingress6_clat_rawip"
-#define CLAT_INGRESS6_PROG_ETHER_NAME "prog_clatd_schedcls_ingress6_clat_ether"
-
-#define CLAT_INGRESS6_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_RAWIP_NAME
-#define CLAT_INGRESS6_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_INGRESS6_PROG_ETHER_NAME
-
-#define CLAT_INGRESS6_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_ingress6_map"
+#define CURRENT_STATS_MAP_CONFIGURATION_KEY 1
 
 typedef struct {
     uint32_t iif;            // The input interface index
@@ -191,14 +207,6 @@
 } ClatIngress6Value;
 STRUCT_SIZE(ClatIngress6Value, 4 + 4);  // 8
 
-#define CLAT_EGRESS4_PROG_RAWIP_NAME "prog_clatd_schedcls_egress4_clat_rawip"
-#define CLAT_EGRESS4_PROG_ETHER_NAME "prog_clatd_schedcls_egress4_clat_ether"
-
-#define CLAT_EGRESS4_PROG_RAWIP_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_RAWIP_NAME
-#define CLAT_EGRESS4_PROG_ETHER_PATH BPF_CLATD_PATH CLAT_EGRESS4_PROG_ETHER_NAME
-
-#define CLAT_EGRESS4_MAP_PATH BPF_CLATD_PATH "map_clatd_clat_egress4_map"
-
 typedef struct {
     uint32_t iif;           // The input interface index
     struct in_addr local4;  // The source IPv4 address
diff --git a/bpf_progs/clat_mark.h b/bpf_progs/clat_mark.h
new file mode 100644
index 0000000..874d6ae
--- /dev/null
+++ b/bpf_progs/clat_mark.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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
+
+/* -=-=-=-=-= WARNING -=-=-=-=-=-
+ *
+ * DO *NOT* *EVER* CHANGE THIS CONSTANT
+ *
+ * This is aidl::android::net::INetd::CLAT_MARK but we can't use that from
+ * pure C code (ie. the eBPF clat program).
+ *
+ * It must match the iptables rules setup by netd on Android T.
+ *
+ * This mark value is used by the eBPF clatd program to mark ingress non-offloaded clat
+ * packets for later dropping in ip6tables bw_raw_PREROUTING.
+ * They need to be dropped *after* the clat daemon (via receive on an AF_PACKET socket)
+ * sees them and thus cannot be dropped from the bpf program itself.
+ */
+static const uint32_t CLAT_MARK = 0xDEADC1A7;
diff --git a/bpf_progs/clatd.c b/bpf_progs/clatd.c
index c5b8555..66e9616 100644
--- a/bpf_progs/clatd.c
+++ b/bpf_progs/clatd.c
@@ -36,16 +36,11 @@
 #include "bpf_helpers.h"
 #include "bpf_net_helpers.h"
 #include "bpf_shared.h"
+#include "clat_mark.h"
 
 // From kernel:include/net/ip.h
 #define IP_DF 0x4000  // Flag: "Don't Fragment"
 
-// Used for iptables drops ingress clat packet. Beware of clat mark change may break the device
-// which is using the old clat mark in netd platform code. The reason is that the clat mark is a
-// mainline constant since T+ but netd iptable rules (ex: bandwidth control, firewall, and so on)
-// are set in stone.
-#define CLAT_MARK 0xdeadc1a7
-
 DEFINE_BPF_MAP_GRW(clat_ingress6_map, HASH, ClatIngress6Key, ClatIngress6Value, 16, AID_SYSTEM)
 
 static inline __always_inline int nat64(struct __sk_buff* skb, bool is_ethernet) {
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index cb1714c..44f76de 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -28,7 +28,6 @@
 #include <linux/ipv6.h>
 #include <linux/pkt_cls.h>
 #include <linux/tcp.h>
-#include <netdutils/UidConstants.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include "bpf_net_helpers.h"
@@ -52,28 +51,57 @@
 #define TCP_FLAG_OFF 13
 #define RST_OFFSET 2
 
-DEFINE_BPF_MAP_GRW(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_A, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(stats_map_B, HASH, StatsKey, StatsValue, STATS_MAP_SIZE, AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(iface_stats_map, HASH, uint32_t, StatsValue, IFACE_STATS_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(configuration_map, HASH, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_owner_map, HASH, uint32_t, UidOwnerValue, UID_OWNER_MAP_SIZE,
-                   AID_NET_BW_ACCT)
-DEFINE_BPF_MAP_GRW(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE, AID_NET_BW_ACCT)
+// For maps netd does not need to access
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
+
+// For maps netd only needs read only access to
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
+
+// For maps netd needs to be able to read and write
+#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+    DEFINE_BPF_MAP_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+                       AID_ROOT, AID_NET_BW_ACCT, 0660)
+
+// Bpf map arrays on creation are preinitialized to 0 and do not support deletion of a key,
+// see: kernel/bpf/arraymap.c array_map_delete_elem() returns -EINVAL (from both syscall and ebpf)
+// Additionally on newer kernels the bpf jit can optimize out the lookups.
+// only valid indexes are [0..CONFIGURATION_MAP_SIZE-1]
+DEFINE_BPF_MAP_RO_NETD(configuration_map, ARRAY, uint32_t, uint32_t, CONFIGURATION_MAP_SIZE)
+
+DEFINE_BPF_MAP_RW_NETD(cookie_tag_map, HASH, uint64_t, UidTagValue, COOKIE_UID_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(uid_counterset_map, HASH, uint32_t, uint8_t, UID_COUNTERSET_MAP_SIZE)
+DEFINE_BPF_MAP_NO_NETD(app_uid_stats_map, HASH, uint32_t, StatsValue, APP_STATS_MAP_SIZE)
+DEFINE_BPF_MAP_RW_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_RW_NETD(uid_permission_map, HASH, uint32_t, uint8_t, UID_OWNER_MAP_SIZE)
 
 /* never actually used from ebpf */
-DEFINE_BPF_MAP_GRW(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE,
-                   AID_NET_BW_ACCT)
+DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
+
+// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
+#define DEFINE_XTBPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog)
+
+// programs that need to be usable by netd, but not by netutils_wrappers
+#define DEFINE_NETD_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+                        KVER_NONE, KVER_INF, false, "fs_bpf_netd_readonly", "")
+
+// programs that only need to be usable by the system server
+#define DEFINE_SYS_BPF_PROG(SECTION_NAME, prog_uid, prog_gid, the_prog) \
+    DEFINE_BPF_PROG_EXT(SECTION_NAME, prog_uid, prog_gid, the_prog, \
+                        KVER_NONE, KVER_INF, false, "fs_bpf_net_shared", "")
 
 static __always_inline int is_system_uid(uint32_t uid) {
-    return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
+    // MIN_SYSTEM_UID is AID_ROOT == 0, so uint32_t is *always* >= 0
+    // MAX_SYSTEM_UID is AID_NOBODY == 9999, while AID_APP_START == 10000
+    return (uid < AID_APP_START);
 }
 
 /*
@@ -302,17 +330,18 @@
     return match;
 }
 
-DEFINE_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
+DEFINE_NETD_BPF_PROG("cgroupskb/ingress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_ingress)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, BPF_INGRESS);
 }
 
-DEFINE_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
+DEFINE_NETD_BPF_PROG("cgroupskb/egress/stats", AID_ROOT, AID_SYSTEM, bpf_cgroup_egress)
 (struct __sk_buff* skb) {
     return bpf_traffic_account(skb, BPF_EGRESS);
 }
 
-DEFINE_BPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/egress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_egress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon does not generate new traffic, all its traffic is accounted for already
     // on the v4-* interfaces (except for the 20 (or 28) extra bytes of IPv6 vs IPv4 overhead,
@@ -331,7 +360,8 @@
     return BPF_MATCH;
 }
 
-DEFINE_BPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/ingress/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_ingress_prog)
 (struct __sk_buff* skb) {
     // Clat daemon traffic is not accounted by virtue of iptables raw prerouting drop rule
     // (in clat_raw_PREROUTING chain), which triggers before this (in bw_raw_PREROUTING chain).
@@ -343,7 +373,8 @@
     return BPF_MATCH;
 }
 
-DEFINE_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN, tc_bpf_ingress_account_prog)
+DEFINE_SYS_BPF_PROG("schedact/ingress/account", AID_ROOT, AID_NET_ADMIN,
+                    tc_bpf_ingress_account_prog)
 (struct __sk_buff* skb) {
     if (is_received_skb(skb)) {
         // Account for ingress traffic before tc drops it.
@@ -353,7 +384,8 @@
     return TC_ACT_UNSPEC;
 }
 
-DEFINE_BPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/allowlist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_allowlist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     if (is_system_uid(sock_uid)) return BPF_MATCH;
@@ -370,7 +402,8 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
+// WARNING: Android T's non-updatable netd depends on the name of this program.
+DEFINE_XTBPF_PROG("skfilter/denylist/xtbpf", AID_ROOT, AID_NET_ADMIN, xt_bpf_denylist_prog)
 (struct __sk_buff* skb) {
     uint32_t sock_uid = bpf_get_socket_uid(skb);
     UidOwnerValue* denylistMatch = bpf_uid_owner_map_lookup_elem(&sock_uid);
@@ -378,7 +411,7 @@
     return BPF_NOMATCH;
 }
 
-DEFINE_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
+DEFINE_NETD_BPF_PROG("cgroupsock/inet/create", AID_ROOT, AID_ROOT, inet_socket_create)
 (struct bpf_sock* sk) {
     uint64_t gid_uid = bpf_get_current_uid_gid();
     /*
@@ -387,7 +420,7 @@
      * user at install time so we only check the appId part of a request uid at
      * run time. See UserHandle#isSameApp for detail.
      */
-    uint32_t appId = (gid_uid & 0xffffffff) % PER_USER_RANGE;
+    uint32_t appId = (gid_uid & 0xffffffff) % AID_USER_OFFSET;  // == PER_USER_RANGE == 100000
     uint8_t* permissions = bpf_uid_permission_map_lookup_elem(&appId);
     if (!permissions) {
         // UID not in map. Default to just INTERNET permission.
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 4ce6add..a2a1ac0 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -200,6 +200,8 @@
     method public int describeContents();
     method @NonNull public android.os.ParcelFileDescriptor getFileDescriptor();
     method @NonNull public String getInterfaceName();
+    method @Nullable public android.net.MacAddress getMacAddress();
+    method public int getMtu();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.net.TestNetworkInterface> CREATOR;
   }
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index db1d7e9..f1298ce 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -249,10 +249,10 @@
     method public void onValidationStatus(int, @Nullable android.net.Uri);
     method @NonNull public android.net.Network register();
     method public void sendAddDscpPolicy(@NonNull android.net.DscpPolicy);
-    method public final void sendLinkProperties(@NonNull android.net.LinkProperties);
-    method public final void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
-    method public final void sendNetworkScore(@NonNull android.net.NetworkScore);
-    method public final void sendNetworkScore(@IntRange(from=0, to=99) int);
+    method public void sendLinkProperties(@NonNull android.net.LinkProperties);
+    method public void sendNetworkCapabilities(@NonNull android.net.NetworkCapabilities);
+    method public void sendNetworkScore(@NonNull android.net.NetworkScore);
+    method public void sendNetworkScore(@IntRange(from=0, to=99) int);
     method public final void sendQosCallbackError(int, int);
     method public final void sendQosSessionAvailable(int, int, @NonNull android.net.QosSessionAttributes);
     method public final void sendQosSessionLost(int, int, int);
@@ -262,7 +262,7 @@
     method @Deprecated public void setLegacySubtype(int, @NonNull String);
     method public void setLingerDuration(@NonNull java.time.Duration);
     method public void setTeardownDelayMillis(@IntRange(from=0, to=0x1388) int);
-    method public final void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
+    method public void setUnderlyingNetworks(@Nullable java.util.List<android.net.Network>);
     method public void unregister();
     method public void unregisterAfterReplacement(@IntRange(from=0, to=0x1388) int);
     field public static final int DSCP_POLICY_STATUS_DELETED = 4; // 0x4
diff --git a/framework/jarjar-excludes.txt b/framework/jarjar-excludes.txt
new file mode 100644
index 0000000..1311765
--- /dev/null
+++ b/framework/jarjar-excludes.txt
@@ -0,0 +1,25 @@
+# INetworkStatsProvider / INetworkStatsProviderCallback are referenced from net-tests-utils, which
+# may be used by tests that do not apply connectivity jarjar rules.
+# TODO: move files to a known internal package (like android.net.connectivity.visiblefortesting)
+# so that they do not need jarjar
+android\.net\.netstats\.provider\.INetworkStatsProvider(\$.+)?
+android\.net\.netstats\.provider\.INetworkStatsProviderCallback(\$.+)?
+
+# INetworkAgent / INetworkAgentRegistry are used in NetworkAgentTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.INetworkAgent(\$.+)?
+android\.net\.INetworkAgentRegistry(\$.+)?
+
+# IConnectivityDiagnosticsCallback used in ConnectivityDiagnosticsManagerTest
+# TODO: move files to android.net.connectivity.visiblefortesting
+android\.net\.IConnectivityDiagnosticsCallback(\$.+)?
+
+
+# KeepaliveUtils is used by ConnectivityManager CTS
+# TODO: move into service-connectivity so framework-connectivity stops using
+# ServiceConnectivityResources (callers need high permissions to find/query the resource apk anyway)
+# and have a ConnectivityManager test API instead
+android\.net\.util\.KeepaliveUtils(\$.+)?
+
+# TODO (b/217115866): add jarjar rules for Nearby
+android\.nearby\..+
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 1b0578f..39cd7f3 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -983,16 +983,6 @@
     public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5;
 
     /**
-     * Firewall chain used for lockdown VPN.
-     * Denylist of apps that cannot receive incoming packets except on loopback because they are
-     * subject to an always-on VPN which is not currently connected.
-     *
-     * @see #BLOCKED_REASON_LOCKDOWN_VPN
-     * @hide
-     */
-    public static final int FIREWALL_CHAIN_LOCKDOWN_VPN = 6;
-
-    /**
      * Firewall chain used for OEM-specific application restrictions.
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
@@ -1024,7 +1014,6 @@
         FIREWALL_CHAIN_POWERSAVE,
         FIREWALL_CHAIN_RESTRICTED,
         FIREWALL_CHAIN_LOW_POWER_STANDBY,
-        FIREWALL_CHAIN_LOCKDOWN_VPN,
         FIREWALL_CHAIN_OEM_DENY_1,
         FIREWALL_CHAIN_OEM_DENY_2,
         FIREWALL_CHAIN_OEM_DENY_3
diff --git a/framework/src/android/net/ITestNetworkManager.aidl b/framework/src/android/net/ITestNetworkManager.aidl
index 27d13c1..d18b931 100644
--- a/framework/src/android/net/ITestNetworkManager.aidl
+++ b/framework/src/android/net/ITestNetworkManager.aidl
@@ -29,8 +29,10 @@
  */
 interface ITestNetworkManager
 {
-    TestNetworkInterface createInterface(boolean isTun, boolean bringUp, in LinkAddress[] addrs,
-            in @nullable String iface);
+    TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
+            in LinkAddress[] addrs, in @nullable String iface);
+
+    void setCarrierEnabled(in TestNetworkInterface iface, boolean enabled);
 
     void setupTestNetwork(in String iface, in LinkProperties lp, in boolean isMetered,
             in int[] administratorUids, in IBinder binder);
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 2c50c73..5659a35 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -913,7 +913,7 @@
      * Must be called by the agent when the network's {@link LinkProperties} change.
      * @param linkProperties the new LinkProperties.
      */
-    public final void sendLinkProperties(@NonNull LinkProperties linkProperties) {
+    public void sendLinkProperties(@NonNull LinkProperties linkProperties) {
         Objects.requireNonNull(linkProperties);
         final LinkProperties lp = new LinkProperties(linkProperties);
         queueOrSendMessage(reg -> reg.sendLinkProperties(lp));
@@ -938,7 +938,7 @@
      * @param underlyingNetworks the new list of underlying networks.
      * @see {@link VpnService.Builder#setUnderlyingNetworks(Network[])}
      */
-    public final void setUnderlyingNetworks(
+    public void setUnderlyingNetworks(
             @SuppressLint("NullableCollection") @Nullable List<Network> underlyingNetworks) {
         final ArrayList<Network> underlyingArray = (underlyingNetworks != null)
                 ? new ArrayList<>(underlyingNetworks) : null;
@@ -1088,7 +1088,7 @@
      * Must be called by the agent when the network's {@link NetworkCapabilities} change.
      * @param networkCapabilities the new NetworkCapabilities.
      */
-    public final void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
+    public void sendNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) {
         Objects.requireNonNull(networkCapabilities);
         mBandwidthUpdatePending.set(false);
         mLastBwRefreshTime = System.currentTimeMillis();
@@ -1102,7 +1102,7 @@
      *
      * @param score the new score.
      */
-    public final void sendNetworkScore(@NonNull NetworkScore score) {
+    public void sendNetworkScore(@NonNull NetworkScore score) {
         Objects.requireNonNull(score);
         queueOrSendMessage(reg -> reg.sendScore(score));
     }
@@ -1113,7 +1113,7 @@
      * @param score the new score, between 0 and 99.
      * deprecated use sendNetworkScore(NetworkScore) TODO : remove in S.
      */
-    public final void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
+    public void sendNetworkScore(@IntRange(from = 0, to = 99) int score) {
         sendNetworkScore(new NetworkScore.Builder().setLegacyInt(score).build());
     }
 
diff --git a/framework/src/android/net/TestNetworkInterface.java b/framework/src/android/net/TestNetworkInterface.java
index 4449ff8..26200e1 100644
--- a/framework/src/android/net/TestNetworkInterface.java
+++ b/framework/src/android/net/TestNetworkInterface.java
@@ -16,22 +16,32 @@
 package android.net;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SystemApi;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
 import android.os.Parcelable;
+import android.util.Log;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
 
 /**
- * This class is used to return the interface name and fd of the test interface
+ * This class is used to return the interface name, fd, MAC, and MTU of the test interface
  *
  * @hide
  */
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
 public final class TestNetworkInterface implements Parcelable {
+    private static final String TAG = "TestNetworkInterface";
+
     @NonNull
     private final ParcelFileDescriptor mFileDescriptor;
     @NonNull
     private final String mInterfaceName;
+    @Nullable
+    private final MacAddress mMacAddress;
+    private final int mMtu;
 
     @Override
     public int describeContents() {
@@ -40,18 +50,41 @@
 
     @Override
     public void writeToParcel(@NonNull Parcel out, int flags) {
-        out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+        out.writeParcelable(mFileDescriptor, flags);
         out.writeString(mInterfaceName);
+        out.writeParcelable(mMacAddress, flags);
+        out.writeInt(mMtu);
     }
 
     public TestNetworkInterface(@NonNull ParcelFileDescriptor pfd, @NonNull String intf) {
         mFileDescriptor = pfd;
         mInterfaceName = intf;
+
+        MacAddress macAddress = null;
+        int mtu = 1500;
+        try {
+            // This constructor is called by TestNetworkManager which runs inside the system server,
+            // which has permission to read the MacAddress.
+            NetworkInterface nif = NetworkInterface.getByName(mInterfaceName);
+
+            // getHardwareAddress() returns null for tun interfaces.
+            byte[] hardwareAddress = nif.getHardwareAddress();
+            if (hardwareAddress != null) {
+                macAddress = MacAddress.fromBytes(nif.getHardwareAddress());
+            }
+            mtu = nif.getMTU();
+        } catch (SocketException e) {
+            Log.e(TAG, "Failed to fetch MacAddress or MTU size from NetworkInterface", e);
+        }
+        mMacAddress = macAddress;
+        mMtu = mtu;
     }
 
     private TestNetworkInterface(@NonNull Parcel in) {
         mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
         mInterfaceName = in.readString();
+        mMacAddress = in.readParcelable(MacAddress.class.getClassLoader());
+        mMtu = in.readInt();
     }
 
     @NonNull
@@ -64,6 +97,15 @@
         return mInterfaceName;
     }
 
+    @Nullable
+    public MacAddress getMacAddress() {
+        return mMacAddress;
+    }
+
+    public int getMtu() {
+        return mMtu;
+    }
+
     @NonNull
     public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
             new Parcelable.Creator<TestNetworkInterface>() {
diff --git a/framework/src/android/net/TestNetworkManager.java b/framework/src/android/net/TestNetworkManager.java
index 4e78823..7b18765 100644
--- a/framework/src/android/net/TestNetworkManager.java
+++ b/framework/src/android/net/TestNetworkManager.java
@@ -58,6 +58,7 @@
     private static final boolean TAP = false;
     private static final boolean TUN = true;
     private static final boolean BRING_UP = true;
+    private static final boolean CARRIER_UP = true;
     private static final LinkAddress[] NO_ADDRS = new LinkAddress[0];
 
     /** @hide */
@@ -166,7 +167,7 @@
     public TestNetworkInterface createTunInterface(@NonNull Collection<LinkAddress> linkAddrs) {
         try {
             final LinkAddress[] arr = new LinkAddress[linkAddrs.size()];
-            return mService.createInterface(TUN, BRING_UP, linkAddrs.toArray(arr),
+            return mService.createInterface(TUN, CARRIER_UP, BRING_UP, linkAddrs.toArray(arr),
                     null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
@@ -185,7 +186,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface() {
         try {
-            return mService.createInterface(TAP, BRING_UP, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, BRING_UP, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -204,7 +205,7 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS, null /* iface */);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, null /* iface */);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -226,7 +227,43 @@
     @NonNull
     public TestNetworkInterface createTapInterface(boolean bringUp, @NonNull String iface) {
         try {
-            return mService.createInterface(TAP, bringUp, NO_ADDRS, iface);
+            return mService.createInterface(TAP, CARRIER_UP, bringUp, NO_ADDRS, iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface with or without carrier for testing purposes.
+     *
+     * @param carrierUp whether the created interface has a carrier or not.
+     * @param bringUp whether to bring up the interface before returning it.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    @NonNull
+    public TestNetworkInterface createTapInterface(boolean carrierUp, boolean bringUp) {
+        try {
+            return mService.createInterface(TAP, carrierUp, bringUp, NO_ADDRS, null /* iface */);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable / disable carrier on TestNetworkInterface
+     *
+     * Note: TUNSETCARRIER is not supported until kernel version 5.0.
+     * TODO: add RequiresApi annotation.
+     *
+     * @param iface the interface to configure.
+     * @param enabled true to turn carrier on, false to turn carrier off.
+     * @hide
+     */
+    @RequiresPermission(Manifest.permission.MANAGE_TEST_NETWORKS)
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        try {
+            mService.setCarrierEnabled(iface, enabled);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
index 23d5170..dc4e11e 100644
--- a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
@@ -500,16 +500,16 @@
             return false;
         }
         BleFilter other = (BleFilter) obj;
-        return mDeviceName.equals(other.mDeviceName)
-                && mDeviceAddress.equals(other.mDeviceAddress)
+        return equal(mDeviceName, other.mDeviceName)
+                && equal(mDeviceAddress, other.mDeviceAddress)
                 && mManufacturerId == other.mManufacturerId
                 && Arrays.equals(mManufacturerData, other.mManufacturerData)
                 && Arrays.equals(mManufacturerDataMask, other.mManufacturerDataMask)
-                && mServiceDataUuid.equals(other.mServiceDataUuid)
+                && equal(mServiceDataUuid, other.mServiceDataUuid)
                 && Arrays.equals(mServiceData, other.mServiceData)
                 && Arrays.equals(mServiceDataMask, other.mServiceDataMask)
-                && mServiceUuid.equals(other.mServiceUuid)
-                && mServiceUuidMask.equals(other.mServiceUuidMask);
+                && equal(mServiceUuid, other.mServiceUuid)
+                && equal(mServiceUuidMask, other.mServiceUuidMask);
     }
 
     /** Builder class for {@link BleFilter}. */
@@ -743,4 +743,11 @@
         }
         return osFilterBuilder.build();
     }
+
+    /**
+     * equal() method for two possibly-null objects
+     */
+    private static boolean equal(@Nullable Object obj1, @Nullable Object obj2) {
+        return obj1 == obj2 || (obj1 != null && obj1.equals(obj2));
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
index f27899f..b4f46f8 100644
--- a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
+++ b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
@@ -46,6 +46,12 @@
     /** Model ID in {@link #getFastPairRecord()}. */
     public static final byte[] FAST_PAIR_MODEL_ID = Hex.stringToBytes("AABBCC");
 
+    /** An arbitrary BLE device address. */
+    public static final String DEVICE_ADDRESS = "00:00:00:00:00:01";
+
+    /** Arbitrary RSSI (Received Signal Strength Indicator). */
+    public static final int RSSI = -72;
+
     /** @see #getFastPairRecord() */
     public static byte[] newFastPairRecord(byte header, byte[] modelId) {
         return newFastPairRecord(
@@ -61,6 +67,45 @@
                         Hex.bytesToStringUppercase(serviceData)));
     }
 
+    // This is an example extended inquiry response for a phone with PANU
+    // and Hands-free Audio Gateway
+    public static byte[] eir_1 = {
+            0x06, // Length of this Data
+            0x09, // <<Complete Local Name>>
+            'P',
+            'h',
+            'o',
+            'n',
+            'e',
+            0x05, // Length of this Data
+            0x03, // <<Complete list of 16-bit Service UUIDs>>
+            0x15,
+            0x11, // PANU service class UUID
+            0x1F,
+            0x11, // Hands-free Audio Gateway service class UUID
+            0x01, // Length of this data
+            0x05, // <<Complete list of 32-bit Service UUIDs>>
+            0x11, // Length of this data
+            0x07, // <<Complete list of 128-bit Service UUIDs>>
+            0x01,
+            0x02,
+            0x03,
+            0x04,
+            0x05,
+            0x06,
+            0x07,
+            0x08, // Made up UUID
+            0x11,
+            0x12,
+            0x13,
+            0x14,
+            0x15,
+            0x16,
+            0x17,
+            0x18, //
+            0x00 // End of Data (Not transmitted over the air
+    };
+
     // This is an example of advertising data with AD types
     public static byte[] adv_1 = {
             0x02, // Length of this Data
@@ -138,4 +183,46 @@
             0x00
     };
 
+    // An Eddystone UID frame. go/eddystone for more info
+    public static byte[] eddystone_header_and_uuid = {
+            // BLE Flags
+            (byte) 0x02,
+            (byte) 0x01,
+            (byte) 0x06,
+            // Service UUID
+            (byte) 0x03,
+            (byte) 0x03,
+            (byte) 0xaa,
+            (byte) 0xfe,
+            // Service data header
+            (byte) 0x17,
+            (byte) 0x16,
+            (byte) 0xaa,
+            (byte) 0xfe,
+            // Eddystone frame type
+            (byte) 0x00,
+            // Ranging data
+            (byte) 0xb3,
+            // Eddystone ID namespace
+            (byte) 0x0a,
+            (byte) 0x09,
+            (byte) 0x08,
+            (byte) 0x07,
+            (byte) 0x06,
+            (byte) 0x05,
+            (byte) 0x04,
+            (byte) 0x03,
+            (byte) 0x02,
+            (byte) 0x01,
+            // Eddystone ID instance
+            (byte) 0x16,
+            (byte) 0x15,
+            (byte) 0x14,
+            (byte) 0x13,
+            (byte) 0x12,
+            (byte) 0x11,
+            // RFU
+            (byte) 0x00,
+            (byte) 0x00
+    };
 }
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
index 0b50dfd..7a0548b 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
@@ -133,14 +133,11 @@
             Event that = (Event) o;
             return this.mEventCode == that.getEventCode()
                     && this.mTimestamp == that.getTimestamp()
-                    && (this.mProfile == null
-                        ? that.getProfile() == null : this.mProfile.equals(that.getProfile()))
                     && (this.mBluetoothDevice == null
-                        ? that.getBluetoothDevice() == null :
-                            this.mBluetoothDevice.equals(that.getBluetoothDevice()))
-                    && (this.mException == null
-                        ?  that.getException() == null :
-                            this.mException.equals(that.getException()));
+                    ? that.getBluetoothDevice() == null :
+                    this.mBluetoothDevice.equals(that.getBluetoothDevice()))
+                    && (this.mProfile == null
+                    ? that.getProfile() == null : this.mProfile.equals(that.getProfile()));
         }
         return false;
     }
@@ -150,7 +147,6 @@
         return Objects.hash(mEventCode, mTimestamp, mProfile, mBluetoothDevice, mException);
     }
 
-
     /**
      * Builder
      */
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
index 5b45f61..b2002c5 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
@@ -187,12 +187,6 @@
         return mWrappedBluetoothDevice.createInsecureRfcommSocketToServiceRecord(uuid);
     }
 
-    /** See {@link android.bluetooth.BluetoothDevice#setPin(byte[])}. */
-    @TargetApi(19)
-    public boolean setPairingConfirmation(byte[] pin) {
-        return mWrappedBluetoothDevice.setPin(pin);
-    }
-
     /** See {@link android.bluetooth.BluetoothDevice#setPairingConfirmation(boolean)}. */
     public boolean setPairingConfirmation(boolean confirm) {
         return mWrappedBluetoothDevice.setPairingConfirmation(confirm);
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
index 3f6f361..d4873fd 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
@@ -51,6 +51,11 @@
         return new BluetoothGattServer(instance);
     }
 
+    /** Unwraps a Bluetooth Gatt server. */
+    public android.bluetooth.BluetoothGattServer unwrap() {
+        return mWrappedInstance;
+    }
+
     /**
      * See {@link android.bluetooth.BluetoothGattServer#connect(
      * android.bluetooth.BluetoothDevice, boolean)}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
index 6fe4432..b2c61ab 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
@@ -71,4 +71,10 @@
         }
         return new BluetoothLeAdvertiser(bluetoothLeAdvertiser);
     }
+
+    /** Unwraps a Bluetooth LE advertiser. */
+    @Nullable
+    public android.bluetooth.le.BluetoothLeAdvertiser unwrap() {
+        return mWrappedInstance;
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
index 8a13abe..9b3447e 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
@@ -77,6 +77,12 @@
         mWrappedBluetoothLeScanner.stopScan(callbackIntent);
     }
 
+    /** Unwraps a Bluetooth LE scanner. */
+    @Nullable
+    public android.bluetooth.le.BluetoothLeScanner unwrap() {
+        return mWrappedBluetoothLeScanner;
+    }
+
     /** Wraps a Bluetooth LE scanner. */
     @Nullable
     public static BluetoothLeScanner wrap(
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
index f8b43a6..2003335 100644
--- a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
+++ b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
@@ -110,7 +110,8 @@
         throw new IllegalStateException(errorMessage);
     }
 
-    private String getUnboundErrorMessage(Class<?> type) {
+    @VisibleForTesting
+    String getUnboundErrorMessage(Class<?> type) {
         StringBuilder sb = new StringBuilder();
         sb.append("Unbound type: ").append(type.getName()).append("\n").append(
                 "Searched locators:\n");
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
index 2ecce47..d459329 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -31,10 +31,7 @@
 import com.android.server.nearby.common.ble.decode.FastPairDecoder;
 import com.android.server.nearby.common.ble.util.RangingUtils;
 import com.android.server.nearby.common.bloomfilter.BloomFilter;
-import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
 import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 import com.android.server.nearby.util.DataUtils;
@@ -120,7 +117,7 @@
                 Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
             }
         } else {
-            // Start to process bloom filter
+            // Start to process bloom filter. Yet to finish.
             try {
                 List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
                 byte[] bloomFilterByteArray = FastPairDecoder
@@ -130,73 +127,9 @@
                 if (bloomFilterByteArray == null || bloomFilterByteArray.length == 0) {
                     return;
                 }
-                for (Account account : accountList) {
-                    List<Data.FastPairDeviceWithAccountKey> listDevices =
-                            mPairDataProvider.loadFastPairDeviceWithAccountKey(account);
-                    Data.FastPairDeviceWithAccountKey recognizedDevice =
-                            findRecognizedDevice(listDevices,
-                                    new BloomFilter(bloomFilterByteArray,
-                                            new FastPairBloomFilterHasher()), bloomFilterSalt);
-
-                    if (recognizedDevice != null) {
-                        Log.d(TAG, "find matched device show notification to remind"
-                                + " user to pair");
-                        // Check the distance of the device if the distance is larger than the
-                        // threshold
-                        // do not show half sheet.
-                        if (!isNearby(fastPairDevice.getRssi(),
-                                recognizedDevice.getDiscoveryItem().getTxPower() == 0
-                                        ? fastPairDevice.getTxPower()
-                                        : recognizedDevice.getDiscoveryItem().getTxPower())) {
-                            return;
-                        }
-                        // Check if the device is already paired
-                        List<Cache.StoredFastPairItem> storedFastPairItemList =
-                                Locator.get(mContext, FastPairCacheManager.class)
-                                        .getAllSavedStoredFastPairItem();
-                        Cache.StoredFastPairItem recognizedStoredFastPairItem =
-                                findRecognizedDeviceFromCachedItem(storedFastPairItemList,
-                                        new BloomFilter(bloomFilterByteArray,
-                                                new FastPairBloomFilterHasher()), bloomFilterSalt);
-                        if (recognizedStoredFastPairItem != null) {
-                            // The bloomfilter is recognized in the cache so the device is paired
-                            // before
-                            Log.d(TAG, "bloom filter is recognized in the cache");
-                            continue;
-                        } else {
-                            Log.d(TAG, "bloom filter is recognized not paired before should"
-                                    + "show subsequent pairing notification");
-                            if (mIsFirst) {
-                                mIsFirst = false;
-                                // Get full info from api the initial request will only return
-                                // part of the info due to size limit.
-                                List<Data.FastPairDeviceWithAccountKey> resList =
-                                        mPairDataProvider.loadFastPairDeviceWithAccountKey(account,
-                                                List.of(recognizedDevice.getAccountKey()
-                                                        .toByteArray()));
-                                if (resList != null && resList.size() > 0) {
-                                    //Saved device from footprint does not have ble address so
-                                    // fill ble address with current scan result.
-                                    Cache.StoredDiscoveryItem storedDiscoveryItem =
-                                            resList.get(0).getDiscoveryItem().toBuilder()
-                                                    .setMacAddress(
-                                                            fastPairDevice.getBluetoothAddress())
-                                                    .build();
-                                    Locator.get(mContext, FastPairController.class).pair(
-                                            new DiscoveryItem(mContext, storedDiscoveryItem),
-                                            resList.get(0).getAccountKey().toByteArray(),
-                                            /** companionApp=*/null);
-                                }
-                            }
-                        }
-
-                        return;
-                    }
-                }
             } catch (IllegalStateException e) {
                 Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
             }
-
         }
     }
 
@@ -249,5 +182,4 @@
     boolean isNearby(int rssi, int txPower) {
         return RangingUtils.distanceFromRssiAndTxPower(rssi, txPower) < NEARBY_DISTANCE_THRESHOLD;
     }
-
 }
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
index 6065f99..5ce4488 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
@@ -28,6 +28,7 @@
 import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.common.ble.util.RangingUtils;
 import com.android.server.nearby.common.fastpair.IconUtils;
 import com.android.server.nearby.common.locator.Locator;
@@ -106,15 +107,6 @@
     }
 
     /**
-     * Sets the store discovery item mac address.
-     */
-    public void setMacAddress(String address) {
-        mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().setMacAddress(address).build();
-
-        mFastPairCacheManager.saveDiscoveryItem(this);
-    }
-
-    /**
      * Checks if the item is expired. Expired items are those over getItemExpirationMillis() eg. 2
      * minutes
      */
@@ -295,7 +287,8 @@
      * Returns the app name of discovery item.
      */
     @Nullable
-    private String getAppName() {
+    @VisibleForTesting
+    protected String getAppName() {
         return mStoredDiscoveryItem.getAppName();
     }
 
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
index b840091..c6134f5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
@@ -64,16 +64,6 @@
     }
 
     /**
-     * Checks if the entry can be auto deleted from the cache
-     */
-    public boolean isDeletable(Cache.ServerResponseDbItem entry) {
-        if (!entry.getExpirable()) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
      * Save discovery item into database. Discovery item is item that discovered through Ble before
      * pairing success.
      */
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
index ccd7e5e..5fb05d5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
@@ -24,6 +24,7 @@
 import android.content.Context;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
 import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -184,7 +185,8 @@
                 + maskBluetoothAddress(address));
     }
 
-    private static void optInFootprintsForInitialPairing(
+    @VisibleForTesting
+    static void optInFootprintsForInitialPairing(
             FootprintsDeviceManager footprints,
             DiscoveryItem item,
             byte[] accountKey,
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
index 8493e0c..4fe11c7 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -27,6 +27,7 @@
 import android.hardware.location.NanoAppState;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.injector.ContextHubManagerAdapter;
 import com.android.server.nearby.injector.Injector;
 
@@ -172,7 +173,8 @@
         mCallback.onNanoAppRestart(nanoAppId);
     }
 
-    private static String contextHubTransactionResultToString(int result) {
+    @VisibleForTesting
+    static String contextHubTransactionResultToString(int result) {
         switch (result) {
             case ContextHubTransaction.RESULT_SUCCESS:
                 return "RESULT_SUCCESS";
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
index 0fed8db..c564f0d 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -33,6 +33,7 @@
 import android.util.Log;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.injector.Injector;
 import com.android.server.nearby.metrics.NearbyMetrics;
 import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -50,7 +51,6 @@
 
 /** Manages all aspects of discovery providers. */
 public class DiscoveryProviderManager implements AbstractDiscoveryProvider.Listener {
-
     protected final Object mLock = new Object();
     private final Context mContext;
     private final BleDiscoveryProvider mBleDiscoveryProvider;
@@ -233,7 +233,8 @@
         }
     }
 
-    private void startChreProvider() {
+    @VisibleForTesting
+    void startChreProvider() {
         Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
         synchronized (mLock) {
             List<ScanFilter> scanFilters = new ArrayList();
@@ -266,7 +267,8 @@
         mChreDiscoveryProvider.getController().stop();
     }
 
-    private void invalidateProviderScanMode() {
+    @VisibleForTesting
+    void invalidateProviderScanMode() {
         if (mBleDiscoveryProvider.getController().isStarted()) {
             mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
         } else {
@@ -277,7 +279,8 @@
         }
     }
 
-    private static boolean presenceFilterMatches(
+    @VisibleForTesting
+    static boolean presenceFilterMatches(
             NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
         if (scanFilters.isEmpty()) {
             return true;
diff --git a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
index 0f99a2f..d925f07 100644
--- a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
@@ -30,7 +30,7 @@
 
 import androidx.annotation.WorkerThread;
 
-import com.android.server.nearby.common.bloomfilter.BloomFilter;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
 
 import java.util.ArrayList;
@@ -80,6 +80,11 @@
         }
     }
 
+    @VisibleForTesting
+    void setProxyDataProvider(ProxyFastPairDataProvider proxyFastPairDataProvider) {
+        this.mProxyFastPairDataProvider = proxyFastPairDataProvider;
+    }
+
     /**
      * Loads FastPairAntispoofKeyDeviceMetadata.
      *
@@ -136,14 +141,6 @@
     }
 
     /**
-     * Get recognized device from bloom filter.
-     */
-    public Data.FastPairDeviceWithAccountKey getRecognizedDevice(BloomFilter bloomFilter,
-            byte[] salt) {
-        return Data.FastPairDeviceWithAccountKey.newBuilder().build();
-    }
-
-    /**
      * Loads FastPair device accountKeys for a given account, but not other detailed fields.
      *
      * @throws IllegalStateException If ProxyFastPairDataProvider is not available.
diff --git a/nearby/service/java/com/android/server/nearby/util/DataUtils.java b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
index 8bb83e9..c3bae08 100644
--- a/nearby/service/java/com/android/server/nearby/util/DataUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
@@ -57,11 +57,9 @@
      */
     public static String toString(ScanFastPairStoreItem item) {
         return "ScanFastPairStoreItem=[address:" + item.getAddress()
-                + ", actionUr:" + item.getActionUrl()
+                + ", actionUrl:" + item.getActionUrl()
                 + ", deviceName:" + item.getDeviceName()
-                + ", iconPng:" + item.getIconPng()
                 + ", iconFifeUrl:" + item.getIconFifeUrl()
-                + ", antiSpoofingKeyPair:" + item.getAntiSpoofingPublicKey()
                 + ", fastPairStrings:" + toString(item.getFastPairStrings())
                 + "]";
     }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index aacb6d8..a2da967 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -42,7 +42,6 @@
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         CredentialElement element = new CredentialElement(KEY, VALUE);
-
         assertThat(element.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
     }
@@ -58,9 +57,31 @@
         CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
                 parcel);
         parcel.recycle();
-
         assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        CredentialElement element = new CredentialElement(KEY, VALUE);
+        assertThat(element.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEqual() {
+        CredentialElement element1 = new CredentialElement(KEY, VALUE);
+        CredentialElement element2 = new CredentialElement(KEY, VALUE);
+        assertThat(element1.equals(element2)).isTrue();
+        assertThat(element1.hashCode()).isEqualTo(element2.hashCode());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        CredentialElement [] elements =
+                CredentialElement.CREATOR.newArray(2);
+        assertThat(elements.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
index 6fb2ec9..cead741 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
@@ -42,8 +42,11 @@
 
     private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
     private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+    private static final byte[] SALT = new byte[] {1, 2, 3, 4};
     private static final String FAST_PAIR_MODEL_ID = "1234";
     private static final int RSSI = -60;
+    private static final int TX_POWER = -10;
+    private static final int ACTION = 1;
 
     private NearbyDeviceParcelable.Builder mBuilder;
 
@@ -66,11 +69,11 @@
     public void testToString() {
         PublicCredential publicCredential =
                 new PublicCredential.Builder(
-                                new byte[] {1},
-                                new byte[] {2},
-                                new byte[] {3},
-                                new byte[] {4},
-                                new byte[] {5})
+                        new byte[] {1},
+                        new byte[] {2},
+                        new byte[] {3},
+                        new byte[] {4},
+                        new byte[] {5})
                         .build();
         NearbyDeviceParcelable nearbyDeviceParcelable =
                 mBuilder.setFastPairModelId(null)
@@ -89,20 +92,36 @@
 
     @Test
     @SdkSuppress(minSdkVersion = 33, codeName = "T")
-    public void test_defaultNullFields() {
+    public void testNullFields() {
+        PublicCredential publicCredential =
+                new PublicCredential.Builder(
+                        new byte[] {1},
+                        new byte[] {2},
+                        new byte[] {3},
+                        new byte[] {4},
+                        new byte[] {5})
+                        .build();
         NearbyDeviceParcelable nearbyDeviceParcelable =
                 new NearbyDeviceParcelable.Builder()
                         .setMedium(NearbyDevice.Medium.BLE)
+                        .setPublicCredential(publicCredential)
+                        .setAction(ACTION)
                         .setRssi(RSSI)
+                        .setTxPower(TX_POWER)
+                        .setSalt(SALT)
                         .build();
 
         assertThat(nearbyDeviceParcelable.getName()).isNull();
         assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
         assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
         assertThat(nearbyDeviceParcelable.getData()).isNull();
-
         assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
         assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+        assertThat(nearbyDeviceParcelable.getAction()).isEqualTo(ACTION);
+        assertThat(nearbyDeviceParcelable.getPublicCredential()).isEqualTo(publicCredential);
+        assertThat(nearbyDeviceParcelable.getSalt()).isEqualTo(SALT);
+        assertThat(nearbyDeviceParcelable.getScanType()).isEqualTo(SCAN_TYPE_NEARBY_PRESENCE);
+        assertThat(nearbyDeviceParcelable.getTxPower()).isEqualTo(TX_POWER);
     }
 
     @Test
@@ -142,7 +161,6 @@
     @SdkSuppress(minSdkVersion = 33, codeName = "T")
     public void testWriteParcel_nullBluetoothAddress() {
         NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
-
         Parcel parcel = Parcel.obtain();
         nearbyDeviceParcelable.writeToParcel(parcel, 0);
         parcel.setDataPosition(0);
@@ -152,4 +170,28 @@
 
         assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void describeContents() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+        assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testEqual() {
+        NearbyDeviceParcelable nearbyDeviceParcelable1 = mBuilder.setBluetoothAddress(null).build();
+        NearbyDeviceParcelable nearbyDeviceParcelable2 = mBuilder.setBluetoothAddress(null).build();
+        assertThat(nearbyDeviceParcelable1.equals(nearbyDeviceParcelable2)).isTrue();
+        assertThat(nearbyDeviceParcelable1.hashCode())
+                .isEqualTo(nearbyDeviceParcelable2.hashCode());
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        NearbyDeviceParcelable[] nearbyDeviceParcelables =
+                NearbyDeviceParcelable.CREATOR.newArray(2);
+        assertThat(nearbyDeviceParcelables.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
index f37800a..675969a 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -56,4 +56,30 @@
         assertThat(fastPairDevice.getMediums()).contains(1);
         assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEqual() {
+        FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+                .addMedium(NearbyDevice.Medium.BLE)
+                .setRssi(-60)
+                .build();
+        FastPairDevice fastPairDevice2 = new FastPairDevice.Builder()
+                .addMedium(NearbyDevice.Medium.BLE)
+                .setRssi(-60)
+                .build();
+        assertThat(fastPairDevice1.equals(fastPairDevice2)).isTrue();
+        assertThat(fastPairDevice1.hashCode()).isEqualTo(fastPairDevice2.hashCode());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testToString() {
+        FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+                .addMedium(NearbyDevice.Medium.BLE)
+                .setRssi(-60)
+                .build();
+
+        assertThat(fastPairDevice1.toString()).isEqualTo("");
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 7696a61..462d05a 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -158,6 +158,15 @@
         mNearbyManager.stopBroadcast(callback);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void setFastPairScanEnabled() {
+        mNearbyManager.setFastPairScanEnabled(mContext, true);
+        assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isTrue();
+        mNearbyManager.setFastPairScanEnabled(mContext, false);
+        assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isFalse();
+    }
+
     private void enableBluetooth() {
         BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
         BluetoothAdapter bluetoothAdapter = manager.getAdapter();
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index 1daa410..be0c4b7 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -114,4 +114,18 @@
         assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
 
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+        assertThat(broadcastRequest.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PresenceBroadcastRequest[] presenceBroadcastRequests =
+                PresenceBroadcastRequest.CREATOR.newArray(2);
+        assertThat(presenceBroadcastRequests.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index 5fefc68..ab0b7b4 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -104,4 +104,24 @@
         assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
         assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceDevice device =
+                new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+                        .addExtendedProperty(new DataElement(KEY, VALUE))
+                        .setRssi(RSSI)
+                        .addMedium(MEDIUM)
+                        .setName(DEVICE_NAME)
+                        .build();
+        assertThat(device.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PresenceDevice[] devices =
+                PresenceDevice.CREATOR.newArray(2);
+        assertThat(devices.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index b7fe40a..806e7c0 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -91,4 +91,19 @@
         assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
         assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PresenceScanFilter filter = mBuilder.build();
+        assertThat(filter.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        PresenceScanFilter[] filters =
+                PresenceScanFilter.CREATOR.newArray(2);
+        assertThat(filters.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index f05f65f..fa8c954 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -99,4 +99,19 @@
         assertThat(credentialElement.getKey()).isEqualTo(KEY);
         assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void describeContents() {
+        PrivateCredential credential = mBuilder.build();
+        assertThat(credential.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 33, codeName = "T")
+    public void testCreatorNewArray() {
+        PrivateCredential[]  credentials =
+                PrivateCredential.CREATOR.newArray(2);
+        assertThat(credentials.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index 11bbacc..05a4598 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -161,4 +161,19 @@
                         .build();
         assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
     }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        PublicCredential credential = mBuilder.build();
+        assertThat(credential.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        PublicCredential[] credentials  =
+        PublicCredential.CREATOR.newArray(2);
+        assertThat(credentials.length).isEqualTo(2);
+    }
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index 21f3d28..de4b1c3 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -171,6 +171,23 @@
         assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(RSSI);
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void describeContents() {
+        ScanRequest request = new ScanRequest.Builder()
+                .setScanType(SCAN_TYPE_FAST_PAIR)
+                .build();
+        assertThat(request.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreatorNewArray() {
+        ScanRequest[] requests =
+                ScanRequest.CREATOR.newArray(2);
+        assertThat(requests.length).isEqualTo(2);
+    }
+
     private static PresenceScanFilter getPresenceScanFilter() {
         final byte[] secretId = new byte[]{1, 2, 3, 4};
         final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index 9f58baf..7dcb263 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -23,6 +23,7 @@
     <uses-permission android:name="android.permission.BLUETOOTH" />
     <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
     <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
 
     <application android:debuggable="true">
         <uses-library android:name="android.test.runner" />
diff --git a/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java b/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java
new file mode 100644
index 0000000..d095529
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairAntispoofKeyDeviceMetadataTest {
+
+    private static final int BLE_TX_POWER  = 5;
+    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+    private static final float DELTA = 0.001f;
+    private static final int DEVICE_TYPE = 7;
+    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+    private static final byte[] IMAGE = new byte[] {7, 9};
+    private static final String IMAGE_URL = "IMAGE_URL";
+    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+            "INITIAL_NOTIFICATION_DESCRIPTION";
+    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+    private static final String INTENT_URI = "INTENT_URI";
+    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+    private static final float TRIGGER_DISTANCE = 111;
+    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+            "UPDATE_COMPANION_APP_DESCRIPTION";
+    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+    private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
+    private static final String NAME = "NAME";
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetGetFastPairAntispoofKeyDeviceMetadataNotNull() {
+        FastPairDeviceMetadata fastPairDeviceMetadata = genFastPairDeviceMetadata();
+        FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
+                genFastPairAntispoofKeyDeviceMetadata(ANTI_SPOOFING_KEY, fastPairDeviceMetadata);
+
+        assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
+                ANTI_SPOOFING_KEY);
+        ensureFastPairDeviceMetadataAsExpected(
+                fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetGetFastPairAntispoofKeyDeviceMetadataNull() {
+        FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
+                genFastPairAntispoofKeyDeviceMetadata(null, null);
+        assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
+                null);
+        assertThat(fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata()).isEqualTo(
+                null);
+    }
+
+    /* Verifies DeviceMetadata. */
+    private static void ensureFastPairDeviceMetadataAsExpected(FastPairDeviceMetadata metadata) {
+        assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
+        assertThat(metadata.getConnectSuccessCompanionAppInstalled())
+                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+        assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
+                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+        assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
+        assertThat(metadata.getDownloadCompanionAppDescription())
+                .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+        assertThat(metadata.getFailConnectGoToSettingsDescription())
+                .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+        assertThat(metadata.getImage()).isEqualTo(IMAGE);
+        assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
+        assertThat(metadata.getInitialNotificationDescription())
+                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
+        assertThat(metadata.getInitialNotificationDescriptionNoAccount())
+                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+        assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+        assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
+        assertThat(metadata.getName()).isEqualTo(NAME);
+        assertThat(metadata.getOpenCompanionAppDescription())
+                .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
+        assertThat(metadata.getRetroactivePairingDescription())
+                .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+        assertThat(metadata.getSubsequentPairingDescription())
+                .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
+        assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
+        assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+        assertThat(metadata.getTrueWirelessImageUrlLeftBud())
+                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+        assertThat(metadata.getTrueWirelessImageUrlRightBud())
+                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+        assertThat(metadata.getUnableToConnectDescription())
+                .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
+        assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+        assertThat(metadata.getUpdateCompanionAppDescription())
+                .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
+        assertThat(metadata.getWaitLaunchCompanionAppDescription())
+                .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+    }
+
+    /* Generates FastPairAntispoofKeyDeviceMetadata. */
+    private static FastPairAntispoofKeyDeviceMetadata genFastPairAntispoofKeyDeviceMetadata(
+            byte[] antispoofPublicKey, FastPairDeviceMetadata deviceMetadata) {
+        FastPairAntispoofKeyDeviceMetadata.Builder builder =
+                new FastPairAntispoofKeyDeviceMetadata.Builder();
+        builder.setAntispoofPublicKey(antispoofPublicKey);
+        builder.setFastPairDeviceMetadata(deviceMetadata);
+
+        return builder.build();
+    }
+
+    /* Generates FastPairDeviceMetadata. */
+    private static FastPairDeviceMetadata genFastPairDeviceMetadata() {
+        FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
+        builder.setBleTxPower(BLE_TX_POWER);
+        builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+        builder.setConnectSuccessCompanionAppNotInstalled(
+                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+        builder.setDeviceType(DEVICE_TYPE);
+        builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+        builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+        builder.setImage(IMAGE);
+        builder.setImageUrl(IMAGE_URL);
+        builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
+        builder.setInitialNotificationDescriptionNoAccount(
+                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+        builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+        builder.setIntentUri(INTENT_URI);
+        builder.setName(NAME);
+        builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
+        builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+        builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+        builder.setTriggerDistance(TRIGGER_DISTANCE);
+        builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
+        builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+        builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+        builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
+        builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
+        builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
+        builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+
+        return builder.build();
+    }
+}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java
new file mode 100644
index 0000000..b3f2442
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java
@@ -0,0 +1,966 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.accounts.Account;
+import android.content.Intent;
+import android.nearby.aidl.ByteArrayParcel;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDiscoveryItemParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
+import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
+import android.nearby.aidl.IFastPairDataProvider;
+import android.nearby.aidl.IFastPairEligibleAccountsCallback;
+import android.nearby.aidl.IFastPairManageAccountCallback;
+import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairDataProviderServiceTest {
+
+    private static final String TAG = "FastPairDataProviderServiceTest";
+
+    private static final int BLE_TX_POWER  = 5;
+    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+    private static final float DELTA = 0.001f;
+    private static final int DEVICE_TYPE = 7;
+    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+    private static final Account ELIGIBLE_ACCOUNT_1 = new Account("abc@google.com", "type1");
+    private static final boolean ELIGIBLE_ACCOUNT_1_OPT_IN = true;
+    private static final Account ELIGIBLE_ACCOUNT_2 = new Account("def@gmail.com", "type2");
+    private static final boolean ELIGIBLE_ACCOUNT_2_OPT_IN = false;
+    private static final Account MANAGE_ACCOUNT = new Account("ghi@gmail.com", "type3");
+    private static final Account ACCOUNTDEVICES_METADATA_ACCOUNT =
+            new Account("jk@gmail.com", "type4");
+    private static final int NUM_ACCOUNT_DEVICES = 2;
+
+    private static final int ERROR_CODE_BAD_REQUEST =
+            FastPairDataProviderService.ERROR_CODE_BAD_REQUEST;
+    private static final int MANAGE_ACCOUNT_REQUEST_TYPE =
+            FastPairDataProviderService.MANAGE_REQUEST_ADD;
+    private static final String ERROR_STRING = "ERROR_STRING";
+    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+    private static final byte[] IMAGE = new byte[] {7, 9};
+    private static final String IMAGE_URL = "IMAGE_URL";
+    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+            "INITIAL_NOTIFICATION_DESCRIPTION";
+    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+    private static final String INTENT_URI = "INTENT_URI";
+    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+    private static final float TRIGGER_DISTANCE = 111;
+    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+            "UPDATE_COMPANION_APP_DESCRIPTION";
+    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+    private static final byte[] ACCOUNT_KEY = new byte[] {3};
+    private static final byte[] ACCOUNT_KEY_2 = new byte[] {9, 3};
+    private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[] {2, 8};
+    private static final byte[] REQUEST_MODEL_ID = new byte[] {1, 2, 3};
+    private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
+    private static final String ACTION_URL = "ACTION_URL";
+    private static final int ACTION_URL_TYPE = 5;
+    private static final String APP_NAME = "APP_NAME";
+    private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[] {5, 7};
+    private static final String DESCRIPTION = "DESCRIPTION";
+    private static final String DEVICE_NAME = "DEVICE_NAME";
+    private static final String DISPLAY_URL = "DISPLAY_URL";
+    private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
+    private static final String  ICON_FIFE_URL = "ICON_FIFE_URL";
+    private static final byte[]  ICON_PNG = new byte[]{2, 5};
+    private static final String ID = "ID";
+    private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
+    private static final String MAC_ADDRESS = "MAC_ADDRESS";
+    private static final String NAME = "NAME";
+    private static final String PACKAGE_NAME = "PACKAGE_NAME";
+    private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
+    private static final int RSSI = 9;
+    private static final int STATE = 63;
+    private static final String TITLE = "TITLE";
+    private static final String TRIGGER_ID = "TRIGGER_ID";
+    private static final int TX_POWER = 62;
+
+    private static final int ELIGIBLE_ACCOUNTS_NUM = 2;
+    private static final ImmutableList<FastPairEligibleAccount> ELIGIBLE_ACCOUNTS =
+            ImmutableList.of(
+                    genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_1,
+                            ELIGIBLE_ACCOUNT_1_OPT_IN),
+                    genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_2,
+                            ELIGIBLE_ACCOUNT_2_OPT_IN));
+    private static final int ACCOUNTKEY_DEVICE_NUM = 2;
+    private static final ImmutableList<FastPairAccountKeyDeviceMetadata>
+            FAST_PAIR_ACCOUNT_DEVICES_METADATA =
+            ImmutableList.of(
+                    genHappyPathFastPairAccountkeyDeviceMetadata(),
+                    genHappyPathFastPairAccountkeyDeviceMetadata());
+
+    private static final FastPairAntispoofKeyDeviceMetadataRequestParcel
+            FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL =
+            genFastPairAntispoofKeyDeviceMetadataRequestParcel();
+    private static final FastPairAccountDevicesMetadataRequestParcel
+            FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL =
+            genFastPairAccountDevicesMetadataRequestParcel();
+    private static final FastPairEligibleAccountsRequestParcel
+            FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL =
+            genFastPairEligibleAccountsRequestParcel();
+    private static final FastPairManageAccountRequestParcel
+            FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL =
+            genFastPairManageAccountRequestParcel();
+    private static final FastPairManageAccountDeviceRequestParcel
+            FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL =
+            genFastPairManageAccountDeviceRequestParcel();
+    private static final FastPairAntispoofKeyDeviceMetadata
+            HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA =
+            genHappyPathFastPairAntispoofKeyDeviceMetadata();
+
+    @Captor private ArgumentCaptor<FastPairEligibleAccountParcel[]>
+            mFastPairEligibleAccountParcelsArgumentCaptor;
+    @Captor private ArgumentCaptor<FastPairAccountKeyDeviceMetadataParcel[]>
+            mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor;
+
+    @Mock private FastPairDataProviderService mMockFastPairDataProviderService;
+    @Mock private IFastPairAntispoofKeyDeviceMetadataCallback.Stub
+            mAntispoofKeyDeviceMetadataCallback;
+    @Mock private IFastPairAccountDevicesMetadataCallback.Stub mAccountDevicesMetadataCallback;
+    @Mock private IFastPairEligibleAccountsCallback.Stub mEligibleAccountsCallback;
+    @Mock private IFastPairManageAccountCallback.Stub mManageAccountCallback;
+    @Mock private IFastPairManageAccountDeviceCallback.Stub mManageAccountDeviceCallback;
+
+    private MyHappyPathProvider mHappyPathFastPairDataProvider;
+    private MyErrorPathProvider mErrorPathFastPairDataProvider;
+
+    @Before
+    public void setUp() throws Exception {
+        initMocks(this);
+
+        mHappyPathFastPairDataProvider =
+                new MyHappyPathProvider(TAG, mMockFastPairDataProviderService);
+        mErrorPathFastPairDataProvider =
+                new MyErrorPathProvider(TAG, mMockFastPairDataProviderService);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testHappyPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
+        // AOSP sends calls to OEM via Parcelable.
+        mHappyPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
+                FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
+                mAntispoofKeyDeviceMetadataCallback);
+
+        // OEM receives request and verifies that it is as expected.
+        final ArgumentCaptor<FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest>
+                fastPairAntispoofKeyDeviceMetadataRequestCaptor =
+                ArgumentCaptor.forClass(
+                        FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class
+                );
+        verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
+                fastPairAntispoofKeyDeviceMetadataRequestCaptor.capture(),
+                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
+        ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataRequestCaptor.getValue());
+
+        // AOSP receives responses and verifies that it is as expected.
+        final ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataParcel>
+                fastPairAntispoofKeyDeviceMetadataParcelCaptor =
+                ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataParcel.class);
+        verify(mAntispoofKeyDeviceMetadataCallback).onFastPairAntispoofKeyDeviceMetadataReceived(
+                fastPairAntispoofKeyDeviceMetadataParcelCaptor.capture());
+        ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataParcelCaptor.getValue());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testHappyPathLoadFastPairAccountDevicesMetadata() throws Exception {
+        // AOSP sends calls to OEM via Parcelable.
+        mHappyPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
+                FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
+                mAccountDevicesMetadataCallback);
+
+        // OEM receives request and verifies that it is as expected.
+        final ArgumentCaptor<FastPairDataProviderService.FastPairAccountDevicesMetadataRequest>
+                fastPairAccountDevicesMetadataRequestCaptor =
+                ArgumentCaptor.forClass(
+                        FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class);
+        verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
+                fastPairAccountDevicesMetadataRequestCaptor.capture(),
+                any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
+        ensureHappyPathAsExpected(fastPairAccountDevicesMetadataRequestCaptor.getValue());
+
+        // AOSP receives responses and verifies that it is as expected.
+        verify(mAccountDevicesMetadataCallback).onFastPairAccountDevicesMetadataReceived(
+                mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.capture());
+        ensureHappyPathAsExpected(
+                mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.getValue());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testHappyPathLoadFastPairEligibleAccounts() throws Exception {
+        // AOSP sends calls to OEM via Parcelable.
+        mHappyPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
+                FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
+                mEligibleAccountsCallback);
+
+        // OEM receives request and verifies that it is as expected.
+        final ArgumentCaptor<FastPairDataProviderService.FastPairEligibleAccountsRequest>
+                fastPairEligibleAccountsRequestCaptor =
+                ArgumentCaptor.forClass(
+                        FastPairDataProviderService.FastPairEligibleAccountsRequest.class);
+        verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
+                fastPairEligibleAccountsRequestCaptor.capture(),
+                any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
+        ensureHappyPathAsExpected(fastPairEligibleAccountsRequestCaptor.getValue());
+
+        // AOSP receives responses and verifies that it is as expected.
+        verify(mEligibleAccountsCallback).onFastPairEligibleAccountsReceived(
+                mFastPairEligibleAccountParcelsArgumentCaptor.capture());
+        ensureHappyPathAsExpected(mFastPairEligibleAccountParcelsArgumentCaptor.getValue());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testHappyPathManageFastPairAccount() throws Exception {
+        // AOSP sends calls to OEM via Parcelable.
+        mHappyPathFastPairDataProvider.asProvider().manageFastPairAccount(
+                FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
+                mManageAccountCallback);
+
+        // OEM receives request and verifies that it is as expected.
+        final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountRequest>
+                fastPairManageAccountRequestCaptor =
+                ArgumentCaptor.forClass(
+                        FastPairDataProviderService.FastPairManageAccountRequest.class);
+        verify(mMockFastPairDataProviderService).onManageFastPairAccount(
+                fastPairManageAccountRequestCaptor.capture(),
+                any(FastPairDataProviderService.FastPairManageActionCallback.class));
+        ensureHappyPathAsExpected(fastPairManageAccountRequestCaptor.getValue());
+
+        // AOSP receives SUCCESS response.
+        verify(mManageAccountCallback).onSuccess();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testHappyPathManageFastPairAccountDevice() throws Exception {
+        // AOSP sends calls to OEM via Parcelable.
+        mHappyPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
+                FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
+                mManageAccountDeviceCallback);
+
+        // OEM receives request and verifies that it is as expected.
+        final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountDeviceRequest>
+                fastPairManageAccountDeviceRequestCaptor =
+                ArgumentCaptor.forClass(
+                        FastPairDataProviderService.FastPairManageAccountDeviceRequest.class);
+        verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
+                fastPairManageAccountDeviceRequestCaptor.capture(),
+                any(FastPairDataProviderService.FastPairManageActionCallback.class));
+        ensureHappyPathAsExpected(fastPairManageAccountDeviceRequestCaptor.getValue());
+
+        // AOSP receives SUCCESS response.
+        verify(mManageAccountDeviceCallback).onSuccess();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testErrorPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
+        mErrorPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
+                FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
+                mAntispoofKeyDeviceMetadataCallback);
+        verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
+                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class),
+                any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
+        verify(mAntispoofKeyDeviceMetadataCallback).onError(
+                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testErrorPathLoadFastPairAccountDevicesMetadata() throws Exception {
+        mErrorPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
+                FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
+                mAccountDevicesMetadataCallback);
+        verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
+                any(FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class),
+                any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
+        verify(mAccountDevicesMetadataCallback).onError(
+                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testErrorPathLoadFastPairEligibleAccounts() throws Exception {
+        mErrorPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
+                FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
+                mEligibleAccountsCallback);
+        verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
+                any(FastPairDataProviderService.FastPairEligibleAccountsRequest.class),
+                any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
+        verify(mEligibleAccountsCallback).onError(
+                eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testErrorPathManageFastPairAccount() throws Exception {
+        mErrorPathFastPairDataProvider.asProvider().manageFastPairAccount(
+                FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
+                mManageAccountCallback);
+        verify(mMockFastPairDataProviderService).onManageFastPairAccount(
+                any(FastPairDataProviderService.FastPairManageAccountRequest.class),
+                any(FastPairDataProviderService.FastPairManageActionCallback.class));
+        verify(mManageAccountCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testErrorPathManageFastPairAccountDevice() throws Exception {
+        mErrorPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
+                FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
+                mManageAccountDeviceCallback);
+        verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
+                any(FastPairDataProviderService.FastPairManageAccountDeviceRequest.class),
+                any(FastPairDataProviderService.FastPairManageActionCallback.class));
+        verify(mManageAccountDeviceCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+    }
+
+    public static class MyHappyPathProvider extends FastPairDataProviderService {
+
+        private final FastPairDataProviderService mMockFastPairDataProviderService;
+
+        public MyHappyPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
+            super(tag);
+            mMockFastPairDataProviderService = mock;
+        }
+
+        public IFastPairDataProvider asProvider() {
+            Intent intent = new Intent();
+            return IFastPairDataProvider.Stub.asInterface(onBind(intent));
+        }
+
+        @Override
+        public void onLoadFastPairAntispoofKeyDeviceMetadata(
+                @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
+                @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
+            mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
+                    request, callback);
+            callback.onFastPairAntispoofKeyDeviceMetadataReceived(
+                    HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA);
+        }
+
+        @Override
+        public void onLoadFastPairAccountDevicesMetadata(
+                @NonNull FastPairAccountDevicesMetadataRequest request,
+                @NonNull FastPairAccountDevicesMetadataCallback callback) {
+            mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
+                    request, callback);
+            callback.onFastPairAccountDevicesMetadataReceived(FAST_PAIR_ACCOUNT_DEVICES_METADATA);
+        }
+
+        @Override
+        public void onLoadFastPairEligibleAccounts(
+                @NonNull FastPairEligibleAccountsRequest request,
+                @NonNull FastPairEligibleAccountsCallback callback) {
+            mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(
+                    request, callback);
+            callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS);
+        }
+
+        @Override
+        public void onManageFastPairAccount(
+                @NonNull FastPairManageAccountRequest request,
+                @NonNull FastPairManageActionCallback callback) {
+            mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
+            callback.onSuccess();
+        }
+
+        @Override
+        public void onManageFastPairAccountDevice(
+                @NonNull FastPairManageAccountDeviceRequest request,
+                @NonNull FastPairManageActionCallback callback) {
+            mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
+            callback.onSuccess();
+        }
+    }
+
+    public static class MyErrorPathProvider extends FastPairDataProviderService {
+
+        private final FastPairDataProviderService mMockFastPairDataProviderService;
+
+        public MyErrorPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
+            super(tag);
+            mMockFastPairDataProviderService = mock;
+        }
+
+        public IFastPairDataProvider asProvider() {
+            Intent intent = new Intent();
+            return IFastPairDataProvider.Stub.asInterface(onBind(intent));
+        }
+
+        @Override
+        public void onLoadFastPairAntispoofKeyDeviceMetadata(
+                @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
+                @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
+            mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
+                    request, callback);
+            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+        }
+
+        @Override
+        public void onLoadFastPairAccountDevicesMetadata(
+                @NonNull FastPairAccountDevicesMetadataRequest request,
+                @NonNull FastPairAccountDevicesMetadataCallback callback) {
+            mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
+                    request, callback);
+            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+        }
+
+        @Override
+        public void onLoadFastPairEligibleAccounts(
+                @NonNull FastPairEligibleAccountsRequest request,
+                @NonNull FastPairEligibleAccountsCallback callback) {
+            mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(request, callback);
+            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+        }
+
+        @Override
+        public void onManageFastPairAccount(
+                @NonNull FastPairManageAccountRequest request,
+                @NonNull FastPairManageActionCallback callback) {
+            mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
+            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+        }
+
+        @Override
+        public void onManageFastPairAccountDevice(
+                @NonNull FastPairManageAccountDeviceRequest request,
+                @NonNull FastPairManageActionCallback callback) {
+            mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
+            callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+        }
+    }
+
+    /* Generates AntispoofKeyDeviceMetadataRequestParcel. */
+    private static FastPairAntispoofKeyDeviceMetadataRequestParcel
+            genFastPairAntispoofKeyDeviceMetadataRequestParcel() {
+        FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel =
+                new FastPairAntispoofKeyDeviceMetadataRequestParcel();
+        requestParcel.modelId = REQUEST_MODEL_ID;
+
+        return requestParcel;
+    }
+
+    /* Generates AccountDevicesMetadataRequestParcel. */
+    private static FastPairAccountDevicesMetadataRequestParcel
+            genFastPairAccountDevicesMetadataRequestParcel() {
+        FastPairAccountDevicesMetadataRequestParcel requestParcel =
+                new FastPairAccountDevicesMetadataRequestParcel();
+
+        requestParcel.account = ACCOUNTDEVICES_METADATA_ACCOUNT;
+        requestParcel.deviceAccountKeys = new ByteArrayParcel[NUM_ACCOUNT_DEVICES];
+        requestParcel.deviceAccountKeys[0] = new ByteArrayParcel();
+        requestParcel.deviceAccountKeys[1] = new ByteArrayParcel();
+        requestParcel.deviceAccountKeys[0].byteArray = ACCOUNT_KEY;
+        requestParcel.deviceAccountKeys[1].byteArray = ACCOUNT_KEY_2;
+
+        return requestParcel;
+    }
+
+    /* Generates FastPairEligibleAccountsRequestParcel. */
+    private static FastPairEligibleAccountsRequestParcel
+            genFastPairEligibleAccountsRequestParcel() {
+        FastPairEligibleAccountsRequestParcel requestParcel =
+                new FastPairEligibleAccountsRequestParcel();
+        // No fields since FastPairEligibleAccountsRequestParcel is just a place holder now.
+        return requestParcel;
+    }
+
+    /* Generates FastPairManageAccountRequestParcel. */
+    private static FastPairManageAccountRequestParcel
+            genFastPairManageAccountRequestParcel() {
+        FastPairManageAccountRequestParcel requestParcel =
+                new FastPairManageAccountRequestParcel();
+        requestParcel.account = MANAGE_ACCOUNT;
+        requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
+
+        return requestParcel;
+    }
+
+    /* Generates FastPairManageAccountDeviceRequestParcel. */
+    private static FastPairManageAccountDeviceRequestParcel
+            genFastPairManageAccountDeviceRequestParcel() {
+        FastPairManageAccountDeviceRequestParcel requestParcel =
+                new FastPairManageAccountDeviceRequestParcel();
+        requestParcel.account = MANAGE_ACCOUNT;
+        requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
+        requestParcel.accountKeyDeviceMetadata =
+                genHappyPathFastPairAccountkeyDeviceMetadataParcel();
+
+        return requestParcel;
+    }
+
+    /* Generates Happy Path AntispoofKeyDeviceMetadata. */
+    private static FastPairAntispoofKeyDeviceMetadata
+            genHappyPathFastPairAntispoofKeyDeviceMetadata() {
+        FastPairAntispoofKeyDeviceMetadata.Builder builder =
+                new FastPairAntispoofKeyDeviceMetadata.Builder();
+        builder.setAntispoofPublicKey(ANTI_SPOOFING_KEY);
+        builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
+
+        return builder.build();
+    }
+
+    /* Generates Happy Path FastPairAccountKeyDeviceMetadata. */
+    private static FastPairAccountKeyDeviceMetadata
+            genHappyPathFastPairAccountkeyDeviceMetadata() {
+        FastPairAccountKeyDeviceMetadata.Builder builder =
+                new FastPairAccountKeyDeviceMetadata.Builder();
+        builder.setDeviceAccountKey(ACCOUNT_KEY);
+        builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
+        builder.setSha256DeviceAccountKeyPublicAddress(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+        builder.setFastPairDiscoveryItem(genHappyPathFastPairDiscoveryItem());
+
+        return builder.build();
+    }
+
+    /* Generates Happy Path FastPairAccountKeyDeviceMetadataParcel. */
+    private static FastPairAccountKeyDeviceMetadataParcel
+            genHappyPathFastPairAccountkeyDeviceMetadataParcel() {
+        FastPairAccountKeyDeviceMetadataParcel parcel =
+                new FastPairAccountKeyDeviceMetadataParcel();
+        parcel.deviceAccountKey = ACCOUNT_KEY;
+        parcel.metadata = genHappyPathFastPairDeviceMetadataParcel();
+        parcel.sha256DeviceAccountKeyPublicAddress = SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS;
+        parcel.discoveryItem = genHappyPathFastPairDiscoveryItemParcel();
+
+        return parcel;
+    }
+
+    /* Generates Happy Path DiscoveryItem. */
+    private static FastPairDiscoveryItem genHappyPathFastPairDiscoveryItem() {
+        FastPairDiscoveryItem.Builder builder = new FastPairDiscoveryItem.Builder();
+
+        builder.setActionUrl(ACTION_URL);
+        builder.setActionUrlType(ACTION_URL_TYPE);
+        builder.setAppName(APP_NAME);
+        builder.setAuthenticationPublicKeySecp256r1(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+        builder.setDescription(DESCRIPTION);
+        builder.setDeviceName(DEVICE_NAME);
+        builder.setDisplayUrl(DISPLAY_URL);
+        builder.setFirstObservationTimestampMillis(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+        builder.setIconFfeUrl(ICON_FIFE_URL);
+        builder.setIconPng(ICON_PNG);
+        builder.setId(ID);
+        builder.setLastObservationTimestampMillis(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+        builder.setMacAddress(MAC_ADDRESS);
+        builder.setPackageName(PACKAGE_NAME);
+        builder.setPendingAppInstallTimestampMillis(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+        builder.setRssi(RSSI);
+        builder.setState(STATE);
+        builder.setTitle(TITLE);
+        builder.setTriggerId(TRIGGER_ID);
+        builder.setTxPower(TX_POWER);
+
+        return builder.build();
+    }
+
+    /* Generates Happy Path DiscoveryItemParcel. */
+    private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() {
+        FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel();
+
+        parcel.actionUrl = ACTION_URL;
+        parcel.actionUrlType = ACTION_URL_TYPE;
+        parcel.appName = APP_NAME;
+        parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1;
+        parcel.description = DESCRIPTION;
+        parcel.deviceName = DEVICE_NAME;
+        parcel.displayUrl = DISPLAY_URL;
+        parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS;
+        parcel.iconFifeUrl = ICON_FIFE_URL;
+        parcel.iconPng = ICON_PNG;
+        parcel.id = ID;
+        parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS;
+        parcel.macAddress = MAC_ADDRESS;
+        parcel.packageName = PACKAGE_NAME;
+        parcel.pendingAppInstallTimestampMillis = PENDING_APP_INSTALL_TIMESTAMP_MILLIS;
+        parcel.rssi = RSSI;
+        parcel.state = STATE;
+        parcel.title = TITLE;
+        parcel.triggerId = TRIGGER_ID;
+        parcel.txPower = TX_POWER;
+
+        return parcel;
+    }
+
+    /* Generates Happy Path DeviceMetadata. */
+    private static FastPairDeviceMetadata genHappyPathFastPairDeviceMetadata() {
+        FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
+        builder.setBleTxPower(BLE_TX_POWER);
+        builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+        builder.setConnectSuccessCompanionAppNotInstalled(
+                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+        builder.setDeviceType(DEVICE_TYPE);
+        builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+        builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+        builder.setImage(IMAGE);
+        builder.setImageUrl(IMAGE_URL);
+        builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
+        builder.setInitialNotificationDescriptionNoAccount(
+                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+        builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+        builder.setIntentUri(INTENT_URI);
+        builder.setName(NAME);
+        builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
+        builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+        builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+        builder.setTriggerDistance(TRIGGER_DISTANCE);
+        builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
+        builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+        builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+        builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
+        builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
+        builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
+        builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+
+        return builder.build();
+    }
+
+    /* Generates Happy Path DeviceMetadataParcel. */
+    private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
+        FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
+
+        parcel.bleTxPower = BLE_TX_POWER;
+        parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
+        parcel.connectSuccessCompanionAppNotInstalled =
+                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
+        parcel.deviceType = DEVICE_TYPE;
+        parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
+        parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
+        parcel.image = IMAGE;
+        parcel.imageUrl = IMAGE_URL;
+        parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
+        parcel.initialNotificationDescriptionNoAccount =
+                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
+        parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
+        parcel.intentUri = INTENT_URI;
+        parcel.name = NAME;
+        parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
+        parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
+        parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
+        parcel.triggerDistance = TRIGGER_DISTANCE;
+        parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
+        parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
+        parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
+        parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
+        parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
+        parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
+        parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
+
+        return parcel;
+    }
+
+    /* Generates Happy Path FastPairEligibleAccount. */
+    private static FastPairEligibleAccount genHappyPathFastPairEligibleAccount(
+            Account account, boolean optIn) {
+        FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
+        builder.setAccount(account);
+        builder.setOptIn(optIn);
+
+        return builder.build();
+    }
+
+    /* Verifies Happy Path AntispoofKeyDeviceMetadataRequest. */
+    private static void ensureHappyPathAsExpected(
+            FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest request) {
+        assertThat(request.getModelId()).isEqualTo(REQUEST_MODEL_ID);
+    }
+
+    /* Verifies Happy Path AccountDevicesMetadataRequest. */
+    private static void ensureHappyPathAsExpected(
+            FastPairDataProviderService.FastPairAccountDevicesMetadataRequest request) {
+        assertThat(request.getAccount()).isEqualTo(ACCOUNTDEVICES_METADATA_ACCOUNT);
+        assertThat(request.getDeviceAccountKeys().size()).isEqualTo(ACCOUNTKEY_DEVICE_NUM);
+        assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY);
+        assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY_2);
+    }
+
+    /* Verifies Happy Path FastPairEligibleAccountsRequest. */
+    @SuppressWarnings("UnusedVariable")
+    private static void ensureHappyPathAsExpected(
+            FastPairDataProviderService.FastPairEligibleAccountsRequest request) {
+        // No fields since FastPairEligibleAccountsRequest is just a place holder now.
+    }
+
+    /* Verifies Happy Path FastPairManageAccountRequest. */
+    private static void ensureHappyPathAsExpected(
+            FastPairDataProviderService.FastPairManageAccountRequest request) {
+        assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
+        assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
+    }
+
+    /* Verifies Happy Path FastPairManageAccountDeviceRequest. */
+    private static void ensureHappyPathAsExpected(
+            FastPairDataProviderService.FastPairManageAccountDeviceRequest request) {
+        assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
+        assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
+        ensureHappyPathAsExpected(request.getAccountKeyDeviceMetadata());
+    }
+
+    /* Verifies Happy Path AntispoofKeyDeviceMetadataParcel. */
+    private static void ensureHappyPathAsExpected(
+            FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) {
+        assertThat(metadataParcel).isNotNull();
+        assertThat(metadataParcel.antispoofPublicKey).isEqualTo(ANTI_SPOOFING_KEY);
+        ensureHappyPathAsExpected(metadataParcel.deviceMetadata);
+    }
+
+    /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel[]. */
+    private static void ensureHappyPathAsExpected(
+            FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) {
+        assertThat(metadataParcels).isNotNull();
+        assertThat(metadataParcels).hasLength(ACCOUNTKEY_DEVICE_NUM);
+        for (FastPairAccountKeyDeviceMetadataParcel parcel: metadataParcels) {
+            ensureHappyPathAsExpected(parcel);
+        }
+    }
+
+    /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel. */
+    private static void ensureHappyPathAsExpected(
+            FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
+        assertThat(metadataParcel).isNotNull();
+        assertThat(metadataParcel.deviceAccountKey).isEqualTo(ACCOUNT_KEY);
+        assertThat(metadataParcel.sha256DeviceAccountKeyPublicAddress)
+                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+        ensureHappyPathAsExpected(metadataParcel.metadata);
+        ensureHappyPathAsExpected(metadataParcel.discoveryItem);
+    }
+
+    /* Verifies Happy Path FastPairAccountKeyDeviceMetadata. */
+    private static void ensureHappyPathAsExpected(
+            FastPairAccountKeyDeviceMetadata metadata) {
+        assertThat(metadata.getDeviceAccountKey()).isEqualTo(ACCOUNT_KEY);
+        assertThat(metadata.getSha256DeviceAccountKeyPublicAddress())
+                .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+        ensureHappyPathAsExpected(metadata.getFastPairDeviceMetadata());
+        ensureHappyPathAsExpected(metadata.getFastPairDiscoveryItem());
+    }
+
+    /* Verifies Happy Path DeviceMetadataParcel. */
+    private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) {
+        assertThat(metadataParcel).isNotNull();
+        assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER);
+
+        assertThat(metadataParcel.connectSuccessCompanionAppInstalled).isEqualTo(
+                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+        assertThat(metadataParcel.connectSuccessCompanionAppNotInstalled).isEqualTo(
+                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+
+        assertThat(metadataParcel.deviceType).isEqualTo(DEVICE_TYPE);
+        assertThat(metadataParcel.downloadCompanionAppDescription).isEqualTo(
+                DOWNLOAD_COMPANION_APP_DESCRIPTION);
+
+        assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo(
+                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+
+        assertThat(metadataParcel.image).isEqualTo(IMAGE);
+        assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL);
+        assertThat(metadataParcel.initialNotificationDescription).isEqualTo(
+                INITIAL_NOTIFICATION_DESCRIPTION);
+        assertThat(metadataParcel.initialNotificationDescriptionNoAccount).isEqualTo(
+                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+        assertThat(metadataParcel.initialPairingDescription).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+        assertThat(metadataParcel.intentUri).isEqualTo(INTENT_URI);
+
+        assertThat(metadataParcel.name).isEqualTo(NAME);
+
+        assertThat(metadataParcel.openCompanionAppDescription).isEqualTo(
+                OPEN_COMPANION_APP_DESCRIPTION);
+
+        assertThat(metadataParcel.retroactivePairingDescription).isEqualTo(
+                RETRO_ACTIVE_PAIRING_DESCRIPTION);
+
+        assertThat(metadataParcel.subsequentPairingDescription).isEqualTo(
+                SUBSEQUENT_PAIRING_DESCRIPTION);
+
+        assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE);
+        assertThat(metadataParcel.trueWirelessImageUrlCase).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+        assertThat(metadataParcel.trueWirelessImageUrlLeftBud).isEqualTo(
+                TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+        assertThat(metadataParcel.trueWirelessImageUrlRightBud).isEqualTo(
+                TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+
+        assertThat(metadataParcel.unableToConnectDescription).isEqualTo(
+                UNABLE_TO_CONNECT_DESCRIPTION);
+        assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+        assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo(
+                UPDATE_COMPANION_APP_DESCRIPTION);
+
+        assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo(
+                WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+    }
+
+    /* Verifies Happy Path DeviceMetadata. */
+    private static void ensureHappyPathAsExpected(FastPairDeviceMetadata metadata) {
+        assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
+        assertThat(metadata.getConnectSuccessCompanionAppInstalled())
+                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+        assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
+                .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+        assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
+        assertThat(metadata.getDownloadCompanionAppDescription())
+                .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+        assertThat(metadata.getFailConnectGoToSettingsDescription())
+                .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+        assertThat(metadata.getImage()).isEqualTo(IMAGE);
+        assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
+        assertThat(metadata.getInitialNotificationDescription())
+                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
+        assertThat(metadata.getInitialNotificationDescriptionNoAccount())
+                .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+        assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+        assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
+        assertThat(metadata.getName()).isEqualTo(NAME);
+        assertThat(metadata.getOpenCompanionAppDescription())
+                .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
+        assertThat(metadata.getRetroactivePairingDescription())
+                .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+        assertThat(metadata.getSubsequentPairingDescription())
+                .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
+        assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
+        assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+        assertThat(metadata.getTrueWirelessImageUrlLeftBud())
+                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+        assertThat(metadata.getTrueWirelessImageUrlRightBud())
+                .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+        assertThat(metadata.getUnableToConnectDescription())
+                .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
+        assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+        assertThat(metadata.getUpdateCompanionAppDescription())
+                .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
+        assertThat(metadata.getWaitLaunchCompanionAppDescription())
+                .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+    }
+
+    /* Verifies Happy Path FastPairDiscoveryItemParcel. */
+    private static void ensureHappyPathAsExpected(FastPairDiscoveryItemParcel itemParcel) {
+        assertThat(itemParcel.actionUrl).isEqualTo(ACTION_URL);
+        assertThat(itemParcel.actionUrlType).isEqualTo(ACTION_URL_TYPE);
+        assertThat(itemParcel.appName).isEqualTo(APP_NAME);
+        assertThat(itemParcel.authenticationPublicKeySecp256r1)
+                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+        assertThat(itemParcel.description).isEqualTo(DESCRIPTION);
+        assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME);
+        assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL);
+        assertThat(itemParcel.firstObservationTimestampMillis)
+                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+        assertThat(itemParcel.iconFifeUrl).isEqualTo(ICON_FIFE_URL);
+        assertThat(itemParcel.iconPng).isEqualTo(ICON_PNG);
+        assertThat(itemParcel.id).isEqualTo(ID);
+        assertThat(itemParcel.lastObservationTimestampMillis)
+                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+        assertThat(itemParcel.macAddress).isEqualTo(MAC_ADDRESS);
+        assertThat(itemParcel.packageName).isEqualTo(PACKAGE_NAME);
+        assertThat(itemParcel.pendingAppInstallTimestampMillis)
+                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+        assertThat(itemParcel.rssi).isEqualTo(RSSI);
+        assertThat(itemParcel.state).isEqualTo(STATE);
+        assertThat(itemParcel.title).isEqualTo(TITLE);
+        assertThat(itemParcel.triggerId).isEqualTo(TRIGGER_ID);
+        assertThat(itemParcel.txPower).isEqualTo(TX_POWER);
+    }
+
+    /* Verifies Happy Path FastPairDiscoveryItem. */
+    private static void ensureHappyPathAsExpected(FastPairDiscoveryItem item) {
+        assertThat(item.getActionUrl()).isEqualTo(ACTION_URL);
+        assertThat(item.getActionUrlType()).isEqualTo(ACTION_URL_TYPE);
+        assertThat(item.getAppName()).isEqualTo(APP_NAME);
+        assertThat(item.getAuthenticationPublicKeySecp256r1())
+                .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+        assertThat(item.getDescription()).isEqualTo(DESCRIPTION);
+        assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
+        assertThat(item.getDisplayUrl()).isEqualTo(DISPLAY_URL);
+        assertThat(item.getFirstObservationTimestampMillis())
+                .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+        assertThat(item.getIconFfeUrl()).isEqualTo(ICON_FIFE_URL);
+        assertThat(item.getIconPng()).isEqualTo(ICON_PNG);
+        assertThat(item.getId()).isEqualTo(ID);
+        assertThat(item.getLastObservationTimestampMillis())
+                .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+        assertThat(item.getMacAddress()).isEqualTo(MAC_ADDRESS);
+        assertThat(item.getPackageName()).isEqualTo(PACKAGE_NAME);
+        assertThat(item.getPendingAppInstallTimestampMillis())
+                .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+        assertThat(item.getRssi()).isEqualTo(RSSI);
+        assertThat(item.getState()).isEqualTo(STATE);
+        assertThat(item.getTitle()).isEqualTo(TITLE);
+        assertThat(item.getTriggerId()).isEqualTo(TRIGGER_ID);
+        assertThat(item.getTxPower()).isEqualTo(TX_POWER);
+    }
+
+    /* Verifies Happy Path EligibleAccountParcel[]. */
+    private static void ensureHappyPathAsExpected(FastPairEligibleAccountParcel[] accountsParcel) {
+        assertThat(accountsParcel).hasLength(ELIGIBLE_ACCOUNTS_NUM);
+
+        assertThat(accountsParcel[0].account).isEqualTo(ELIGIBLE_ACCOUNT_1);
+        assertThat(accountsParcel[0].optIn).isEqualTo(ELIGIBLE_ACCOUNT_1_OPT_IN);
+
+        assertThat(accountsParcel[1].account).isEqualTo(ELIGIBLE_ACCOUNT_2);
+        assertThat(accountsParcel[1].optIn).isEqualTo(ELIGIBLE_ACCOUNT_2_OPT_IN);
+    }
+}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
new file mode 100644
index 0000000..1d7e8ac
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FastPairDeviceTest {
+    private static final String NAME = "name";
+    private static final byte[] DATA = new byte[] {0x01, 0x02};
+    private static final String MODEL_ID = "112233";
+    private static final int RSSI = -80;
+    private static final int TX_POWER = -10;
+    private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+    private static List<Integer> sMediums = new ArrayList<Integer>(List.of(1));
+    private static FastPairDevice sDevice;
+
+
+    @Before
+    public void setup() {
+        sDevice = new FastPairDevice(NAME, sMediums, RSSI, TX_POWER, MODEL_ID, MAC_ADDRESS, DATA);
+    }
+
+    @Test
+    public void testParcelable() {
+        Parcel dest = Parcel.obtain();
+        sDevice.writeToParcel(dest, 0);
+        dest.setDataPosition(0);
+        FastPairDevice compareDevice = FastPairDevice.CREATOR.createFromParcel(dest);
+        assertThat(compareDevice.getName()).isEqualTo(NAME);
+        assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+        assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+        assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+        assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+        assertThat(compareDevice.getData()).isEqualTo(DATA);
+        assertThat(compareDevice.equals(sDevice)).isFalse();
+        assertThat(compareDevice.hashCode()).isEqualTo(sDevice.hashCode());
+    }
+
+    @Test
+    public void describeContents() {
+        assertThat(sDevice.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testToString() {
+        assertThat(sDevice.toString()).isEqualTo(
+                "FastPairDevice [name=name, medium={BLE} "
+                        + "rssi=-80 txPower=-10 "
+                        + "modelId=112233 bluetoothAddress=00:11:22:33:44:55]");
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        FastPairDevice[] fastPairDevices = FastPairDevice.CREATOR.newArray(2);
+        assertThat(fastPairDevices.length).isEqualTo(2);
+    }
+
+    @Test
+    public void testBuilder() {
+        FastPairDevice.Builder builder = new FastPairDevice.Builder();
+        FastPairDevice compareDevice = builder.setName(NAME)
+                .addMedium(1)
+                .setBluetoothAddress(MAC_ADDRESS)
+                .setRssi(RSSI)
+                .setTxPower(TX_POWER)
+                .setData(DATA)
+                .setModelId(MODEL_ID)
+                .build();
+        assertThat(compareDevice.getName()).isEqualTo(NAME);
+        assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+        assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+        assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+        assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+        assertThat(compareDevice.getData()).isEqualTo(DATA);
+    }
+}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java b/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java
new file mode 100644
index 0000000..da5a518
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accounts.Account;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairEligibleAccountTest {
+
+    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
+    private static final Account ACCOUNT_NULL = null;
+
+    private static final boolean OPT_IN_TRUE = true;
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetGetFastPairEligibleAccountNotNull() {
+        FastPairEligibleAccount eligibleAccount =
+                genFastPairEligibleAccount(ACCOUNT, OPT_IN_TRUE);
+
+        assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT);
+        assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetGetFastPairEligibleAccountNull() {
+        FastPairEligibleAccount eligibleAccount =
+                genFastPairEligibleAccount(ACCOUNT_NULL, OPT_IN_TRUE);
+
+        assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT_NULL);
+        assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
+    }
+
+    /* Generates FastPairEligibleAccount. */
+    private static FastPairEligibleAccount genFastPairEligibleAccount(
+            Account account, boolean optIn) {
+        FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
+        builder.setAccount(account);
+        builder.setOptIn(optIn);
+
+        return builder.build();
+    }
+}
diff --git a/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java b/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java
new file mode 100644
index 0000000..7bc6519
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class PairStatusMetadataTest {
+    private static final int UNKNOWN = 1000;
+    private static final int SUCCESS = 1001;
+    private static final int FAIL = 1002;
+    private static final int DISMISS = 1003;
+
+    @Test
+    public void statusToString() {
+        assertThat(PairStatusMetadata.statusToString(UNKNOWN)).isEqualTo("UNKNOWN");
+        assertThat(PairStatusMetadata.statusToString(SUCCESS)).isEqualTo("SUCCESS");
+        assertThat(PairStatusMetadata.statusToString(FAIL)).isEqualTo("FAIL");
+        assertThat(PairStatusMetadata.statusToString(DISMISS)).isEqualTo("DISMISS");
+    }
+
+    @Test
+    public void getStatus() {
+        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1001);
+        pairStatusMetadata = new PairStatusMetadata(FAIL);
+        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1002);
+        pairStatusMetadata = new PairStatusMetadata(DISMISS);
+        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1003);
+        pairStatusMetadata = new PairStatusMetadata(UNKNOWN);
+        assertThat(pairStatusMetadata.getStatus()).isEqualTo(1000);
+    }
+
+    @Test
+    public void testToString() {
+        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+        assertThat(pairStatusMetadata.toString())
+                .isEqualTo("PairStatusMetadata[ status=SUCCESS]");
+        pairStatusMetadata = new PairStatusMetadata(FAIL);
+        assertThat(pairStatusMetadata.toString())
+                .isEqualTo("PairStatusMetadata[ status=FAIL]");
+        pairStatusMetadata = new PairStatusMetadata(DISMISS);
+        assertThat(pairStatusMetadata.toString())
+                .isEqualTo("PairStatusMetadata[ status=DISMISS]");
+        pairStatusMetadata = new PairStatusMetadata(UNKNOWN);
+        assertThat(pairStatusMetadata.toString())
+                .isEqualTo("PairStatusMetadata[ status=UNKNOWN]");
+    }
+
+    @Test
+    public void testEquals() {
+        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+        PairStatusMetadata pairStatusMetadata1 = new PairStatusMetadata(SUCCESS);
+        PairStatusMetadata pairStatusMetadata2 = new PairStatusMetadata(UNKNOWN);
+        assertThat(pairStatusMetadata.equals(pairStatusMetadata1)).isTrue();
+        assertThat(pairStatusMetadata.equals(pairStatusMetadata2)).isFalse();
+        assertThat(pairStatusMetadata.hashCode()).isEqualTo(pairStatusMetadata1.hashCode());
+    }
+
+    @Test
+    public void testParcelable() {
+        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+        Parcel dest = Parcel.obtain();
+        pairStatusMetadata.writeToParcel(dest, 0);
+        dest.setDataPosition(0);
+        PairStatusMetadata comparStatusMetadata =
+                PairStatusMetadata.CREATOR.createFromParcel(dest);
+        assertThat(pairStatusMetadata.equals(comparStatusMetadata)).isTrue();
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        PairStatusMetadata[] pairStatusMetadatas = PairStatusMetadata.CREATOR.newArray(2);
+        assertThat(pairStatusMetadatas.length).isEqualTo(2);
+    }
+
+    @Test
+    public void describeContents() {
+        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+        assertThat(pairStatusMetadata.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void  getStability() {
+        PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+        assertThat(pairStatusMetadata.getStability()).isEqualTo(0);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
index 1d3653b..c4a9729 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
@@ -20,7 +20,11 @@
 
 import static org.junit.Assert.fail;
 
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAssignedNumbers;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.os.Parcel;
 import android.os.ParcelUuid;
 import android.util.SparseArray;
 
@@ -44,6 +48,83 @@
     public static final ParcelUuid EDDYSTONE_SERVICE_DATA_PARCELUUID =
             ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
 
+    private final BleFilter mEddystoneFilter = createEddystoneFilter();
+    private final BleFilter mEddystoneUidFilter = createEddystoneUidFilter();
+    private final BleFilter mEddystoneUrlFilter = createEddystoneUrlFilter();
+    private final BleFilter mEddystoneEidFilter = createEddystoneEidFilter();
+    private final BleFilter mIBeaconWithoutUuidFilter = createIBeaconWithoutUuidFilter();
+    private final BleFilter mIBeaconWithUuidFilter = createIBeaconWithUuidFilter();
+    private final BleFilter mChromecastFilter =
+            new BleFilter.Builder().setServiceUuid(
+                    new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")))
+                    .build();
+    private final BleFilter mEddystoneWithDeviceNameFilter =
+            new BleFilter.Builder()
+                    .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+                    .setDeviceName("BERT")
+                    .build();
+    private final BleFilter mEddystoneWithDeviceAddressFilter =
+            new BleFilter.Builder()
+                    .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+                    .setDeviceAddress("00:11:22:33:AA:BB")
+                    .build();
+    private final BleFilter mServiceUuidWithMaskFilter1 =
+            new BleFilter.Builder()
+                    .setServiceUuid(
+                            new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+                            new ParcelUuid(UUID.fromString("0000000-0000-000-FFFF-FFFFFFFFFFFF")))
+                    .build();
+    private final BleFilter mServiceUuidWithMaskFilter2 =
+            new BleFilter.Builder()
+                    .setServiceUuid(
+                            new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+                            new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")))
+                    .build();
+
+    private final BleFilter mSmartSetupFilter =
+            new BleFilter.Builder()
+                    .setManufacturerData(
+                            BluetoothAssignedNumbers.GOOGLE,
+                            new byte[] {0x00, 0x10},
+                            new byte[] {0x00, (byte) 0xFF})
+                    .build();
+    private final BleFilter mWearFilter =
+            new BleFilter.Builder()
+                    .setManufacturerData(
+                            BluetoothAssignedNumbers.GOOGLE,
+                            new byte[] {0x00, 0x00, 0x00},
+                            new byte[] {0x00, 0x00, (byte) 0xFF})
+                    .build();
+    private final BleFilter mFakeSmartSetupSubsetFilter =
+            new BleFilter.Builder()
+                    .setManufacturerData(
+                            BluetoothAssignedNumbers.GOOGLE,
+                            new byte[] {0x00, 0x10, 0x50},
+                            new byte[] {0x00, (byte) 0xFF, (byte) 0xFF})
+                    .build();
+    private final BleFilter mFakeSmartSetupNotSubsetFilter =
+            new BleFilter.Builder()
+                    .setManufacturerData(
+                            BluetoothAssignedNumbers.GOOGLE,
+                            new byte[] {0x00, 0x10, 0x50},
+                            new byte[] {0x00, (byte) 0x00, (byte) 0xFF})
+                    .build();
+
+    private final BleFilter mFakeFilter1 =
+            new BleFilter.Builder()
+                    .setServiceData(
+                            ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+                            new byte[] {0x51, 0x64},
+                            new byte[] {0x00, (byte) 0xFF})
+                    .build();
+    private final BleFilter mFakeFilter2 =
+            new BleFilter.Builder()
+                    .setServiceData(
+                            ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+                            new byte[] {0x51, 0x64, 0x34},
+                            new byte[] {0x00, (byte) 0xFF, (byte) 0xFF})
+                    .build();
+
     private ParcelUuid mServiceDataUuid;
     private BleSighting mBleSighting;
     private BleFilter.Builder mFilterBuilder;
@@ -229,6 +310,16 @@
     }
 
     @Test
+    public void serviceDataUuidNotInBleRecord() {
+        byte[] bleRecord = FastPairTestData.eir_1;
+        byte[] serviceData = {(byte) 0xe0, (byte) 0x00};
+
+        // Verify Service Data with 2-byte UUID, no data, and NOT in scan record
+        BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test
     public void serviceDataMask() {
         byte[] bleRecord = FastPairTestData.sd1;
         BleFilter filter;
@@ -263,6 +354,18 @@
         mFilterBuilder.setServiceData(mServiceDataUuid, serviceData, mask).build();
     }
 
+    @Test
+    public void serviceDataMaskNotInBleRecord() {
+        byte[] bleRecord = FastPairTestData.eir_1;
+        BleFilter filter;
+
+        // Verify matching partial manufacturer with data and mask
+        byte[] serviceData1 = {(byte) 0xe0, (byte) 0x00, (byte) 0x15};
+        byte[] mask1 = {(byte) 0xff, (byte) 0xff, (byte) 0xff};
+        filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData1, mask1).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
 
     @Test
     public void deviceNameTest() {
@@ -280,12 +383,241 @@
         assertThat(matches(filter, null, 0, bleRecord)).isFalse();
     }
 
+    @Test
+    public void deviceNameNotInBleRecord() {
+        // Verify the name filter does not match
+        byte[] bleRecord = FastPairTestData.eir_1;
+        BleFilter filter = mFilterBuilder.setDeviceName("Pedometer").build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test
+    public void serviceUuid() {
+        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+        ParcelUuid uuid = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+
+        BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
+        assertMatches(filter, null, 0, bleRecord);
+    }
+
+    @Test
+    public void serviceUuidNoMatch() {
+        // Verify the name filter does not match
+        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+        ParcelUuid uuid = ParcelUuid.fromString("00001804-0000-1000-8000-000000000000");
+
+        BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test
+    public void serviceUuidNotInBleRecord() {
+        // Verify the name filter does not match
+        byte[] bleRecord = FastPairTestData.eir_1;
+        ParcelUuid uuid = ParcelUuid.fromString("00001804-0000-1000-8000-000000000000");
+
+        BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test
+    public void serviceUuidMask() {
+        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+        ParcelUuid uuid = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+        ParcelUuid mask = ParcelUuid.fromString("00000000-0000-0000-0000-FFFFFFFFFFFF");
+        BleFilter filter = mFilterBuilder.setServiceUuid(uuid, mask).build();
+        assertMatches(filter, null, 0, bleRecord);
+    }
+
+
+    @Test
+    public void macAddress() {
+        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+        String macAddress = "00:11:22:33:AA:BB";
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+        BluetoothDevice device = adapter.getRemoteDevice(macAddress);
+        BleFilter filter = mFilterBuilder.setDeviceAddress(macAddress).build();
+        assertMatches(filter, device, 0, bleRecord);
+    }
+
+    @Test
+    public void macAddressNoMatch() {
+        byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+        String macAddress = "00:11:22:33:AA:00";
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+        BluetoothDevice device = adapter.getRemoteDevice("00:11:22:33:AA:BB");
+        BleFilter filter = mFilterBuilder.setDeviceAddress(macAddress).build();
+        assertThat(matches(filter, device, 0, bleRecord)).isFalse();
+    }
+
+    @Test
+    public void eddystoneIsSuperset() {
+        // Verify eddystone subtypes pass.
+        assertThat(mEddystoneFilter.isSuperset(mEddystoneFilter)).isTrue();
+        assertThat(mEddystoneUidFilter.isSuperset(mEddystoneUidFilter)).isTrue();
+        assertThat(mEddystoneFilter.isSuperset(mEddystoneUidFilter)).isTrue();
+        assertThat(mEddystoneFilter.isSuperset(mEddystoneEidFilter)).isTrue();
+        assertThat(mEddystoneFilter.isSuperset(mEddystoneUrlFilter)).isTrue();
+
+        // Non-eddystone beacon filters should never be supersets.
+        assertThat(mEddystoneFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+        assertThat(mEddystoneFilter.isSuperset(mWearFilter)).isFalse();
+        assertThat(mEddystoneFilter.isSuperset(mSmartSetupFilter)).isFalse();
+        assertThat(mEddystoneFilter.isSuperset(mChromecastFilter)).isFalse();
+        assertThat(mEddystoneFilter.isSuperset(mFakeFilter1)).isFalse();
+        assertThat(mEddystoneFilter.isSuperset(mFakeFilter2)).isFalse();
+
+        assertThat(mEddystoneUidFilter.isSuperset(mWearFilter)).isFalse();
+        assertThat(mEddystoneUidFilter.isSuperset(mSmartSetupFilter)).isFalse();
+        assertThat(mEddystoneUidFilter.isSuperset(mChromecastFilter)).isFalse();
+        assertThat(mEddystoneUidFilter.isSuperset(mFakeFilter1)).isFalse();
+        assertThat(mEddystoneUidFilter.isSuperset(mFakeFilter2)).isFalse();
+    }
+
+    @Test
+    public void iBeaconIsSuperset() {
+        // Verify that an iBeacon filter is a superset of itself and any filters that specify UUIDs.
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mIBeaconWithoutUuidFilter)).isTrue();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mIBeaconWithUuidFilter)).isTrue();
+
+        // Non-iBeacon filters should never be supersets.
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mWearFilter)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mSmartSetupFilter)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mChromecastFilter)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mFakeFilter1)).isFalse();
+        assertThat(mIBeaconWithoutUuidFilter.isSuperset(mFakeFilter2)).isFalse();
+    }
+
+    @Test
+    public void mixedFilterIsSuperset() {
+        // Compare service data vs manufacturer data filters to verify we detect supersets
+        // correctly in filters that aren't for iBeacon and Eddystone.
+        assertThat(mWearFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+
+        assertThat(mWearFilter.isSuperset(mEddystoneFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mEddystoneFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mEddystoneFilter)).isFalse();
+
+        assertThat(mWearFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+
+        assertThat(mWearFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+
+        assertThat(mWearFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+
+        assertThat(mWearFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
+
+        assertThat(mWearFilter.isSuperset(mChromecastFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mChromecastFilter)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mWearFilter)).isFalse();
+        assertThat(mChromecastFilter.isSuperset(mWearFilter)).isFalse();
+
+        assertThat(mFakeFilter1.isSuperset(mFakeFilter2)).isTrue();
+        assertThat(mFakeFilter2.isSuperset(mFakeFilter1)).isFalse();
+        assertThat(mSmartSetupFilter.isSuperset(mFakeSmartSetupSubsetFilter)).isTrue();
+        assertThat(mSmartSetupFilter.isSuperset(mFakeSmartSetupNotSubsetFilter)).isFalse();
+
+        assertThat(mEddystoneFilter.isSuperset(mEddystoneWithDeviceNameFilter)).isTrue();
+        assertThat(mEddystoneFilter.isSuperset(mEddystoneWithDeviceAddressFilter)).isTrue();
+        assertThat(mEddystoneWithDeviceAddressFilter.isSuperset(mEddystoneFilter)).isFalse();
+
+        assertThat(mChromecastFilter.isSuperset(mServiceUuidWithMaskFilter1)).isTrue();
+        assertThat(mServiceUuidWithMaskFilter2.isSuperset(mServiceUuidWithMaskFilter1)).isFalse();
+        assertThat(mServiceUuidWithMaskFilter1.isSuperset(mServiceUuidWithMaskFilter2)).isTrue();
+        assertThat(mEddystoneFilter.isSuperset(mServiceUuidWithMaskFilter1)).isFalse();
+    }
+
+    @Test
+    public void toOsFilter_getTheSameFilterParameter() {
+        BleFilter nearbyFilter = createTestFilter();
+        ScanFilter osFilter = nearbyFilter.toOsFilter();
+        assertFilterValuesEqual(nearbyFilter, osFilter);
+    }
+
+    @Test
+    public void describeContents() {
+        BleFilter nearbyFilter = createTestFilter();
+        assertThat(nearbyFilter.describeContents()).isEqualTo(0);
+    }
+
+    @Test
+    public void testHashCode() {
+        BleFilter nearbyFilter = createTestFilter();
+        BleFilter compareFilter = new BleFilter("BERT", "00:11:22:33:AA:BB",
+                new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+                new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")),
+                ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+                new byte[] {0x51, 0x64}, new byte[] {0x00, (byte) 0xFF},
+                BluetoothAssignedNumbers.GOOGLE, new byte[] {0x00, 0x10},
+                new byte[] {0x00, (byte) 0xFF});
+        assertThat(nearbyFilter.hashCode()).isEqualTo(compareFilter.hashCode());
+    }
+
+    @Test
+    public void testToString() {
+        BleFilter nearbyFilter = createTestFilter();
+        assertThat(nearbyFilter.toString()).isEqualTo("BleFilter [deviceName=BERT,"
+                + " deviceAddress=00:11:22:33:AA:BB, uuid=0000fea0-0000-1000-8000-00805f9b34fb,"
+                + " uuidMask=0fffffff-ffff-0fff-ffff-ffffffffffff,"
+                + " serviceDataUuid=0000110b-0000-1000-8000-00805f9b34fb,"
+                + " serviceData=[81, 100], serviceDataMask=[0, -1],"
+                + " manufacturerId=224, manufacturerData=[0, 16], manufacturerDataMask=[0, -1]]");
+    }
+
+    @Test
+    public void testParcel() {
+        BleFilter nearbyFilter = createTestFilter();
+        Parcel parcel = Parcel.obtain();
+        nearbyFilter.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+        BleFilter compareFilter = BleFilter.CREATOR.createFromParcel(
+                parcel);
+        parcel.recycle();
+        assertThat(compareFilter.getDeviceName()).isEqualTo("BERT");
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        BleFilter[] nearbyFilters  = BleFilter.CREATOR.newArray(2);
+        assertThat(nearbyFilters.length).isEqualTo(2);
+    }
+
     private static boolean matches(
             BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecord) {
         return filter.matches(new BleSighting(device,
                 bleRecord, rssi, 0 /* timestampNanos */));
     }
 
+    private static void assertFilterValuesEqual(BleFilter nearbyFilter, ScanFilter osFilter) {
+        assertThat(osFilter.getDeviceAddress()).isEqualTo(nearbyFilter.getDeviceAddress());
+        assertThat(osFilter.getDeviceName()).isEqualTo(nearbyFilter.getDeviceName());
+
+        assertThat(osFilter.getManufacturerData()).isEqualTo(nearbyFilter.getManufacturerData());
+        assertThat(osFilter.getManufacturerDataMask())
+                .isEqualTo(nearbyFilter.getManufacturerDataMask());
+        assertThat(osFilter.getManufacturerId()).isEqualTo(nearbyFilter.getManufacturerId());
+
+        assertThat(osFilter.getServiceData()).isEqualTo(nearbyFilter.getServiceData());
+        assertThat(osFilter.getServiceDataMask()).isEqualTo(nearbyFilter.getServiceDataMask());
+        assertThat(osFilter.getServiceDataUuid()).isEqualTo(nearbyFilter.getServiceDataUuid());
+
+        assertThat(osFilter.getServiceUuid()).isEqualTo(nearbyFilter.getServiceUuid());
+        assertThat(osFilter.getServiceUuidMask()).isEqualTo(nearbyFilter.getServiceUuidMask());
+    }
 
     private static void assertMatches(
             BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecordBytes) {
@@ -325,10 +657,10 @@
 
         // UUID match.
         if (filter.getServiceUuid() != null
-                && !matchesServiceUuids(filter.getServiceUuid(), filter.getServiceUuidMask(),
-                bleRecord.getServiceUuids())) {
-            fail("The filter specifies a service UUID but it doesn't match "
-                    + "what's in the scan record");
+                && !matchesServiceUuids(filter.getServiceUuid(),
+                filter.getServiceUuidMask(), bleRecord.getServiceUuids())) {
+            fail("The filter specifies a service UUID "
+                    + "but it doesn't match what's in the scan record");
         }
 
         // Service data match
@@ -401,6 +733,95 @@
         }
     }
 
+    private static String byteString(Map<ParcelUuid, byte[]> bytesMap) {
+        StringBuilder builder = new StringBuilder();
+        for (Map.Entry<ParcelUuid, byte[]> entry : bytesMap.entrySet()) {
+            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
+            builder.append(entry.getKey().toString());
+            builder.append(" --> ");
+            builder.append(byteString(entry.getValue()));
+        }
+        return builder.toString();
+    }
+
+    private static String byteString(SparseArray<byte[]> bytesArray) {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < bytesArray.size(); i++) {
+            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
+            builder.append(byteString(bytesArray.valueAt(i)));
+        }
+        return builder.toString();
+    }
+
+    private static BleFilter createTestFilter() {
+        BleFilter.Builder builder = new BleFilter.Builder();
+        builder
+                .setServiceUuid(
+                        new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+                        new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")))
+                .setDeviceAddress("00:11:22:33:AA:BB")
+                .setDeviceName("BERT")
+                .setManufacturerData(
+                        BluetoothAssignedNumbers.GOOGLE,
+                        new byte[] {0x00, 0x10},
+                        new byte[] {0x00, (byte) 0xFF})
+                .setServiceData(
+                        ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+                        new byte[] {0x51, 0x64},
+                        new byte[] {0x00, (byte) 0xFF});
+        return builder.build();
+    }
+
+    // ref to beacon.decode.BeaconFilterBuilder.eddystoneFilter()
+    private static BleFilter createEddystoneFilter() {
+        return new BleFilter.Builder().setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID).build();
+    }
+    // ref to beacon.decode.BeaconFilterBuilder.eddystoneUidFilter()
+    private static BleFilter createEddystoneUidFilter() {
+        return new BleFilter.Builder()
+                .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+                .setServiceData(
+                        EDDYSTONE_SERVICE_DATA_PARCELUUID, new byte[] {(short) 0x00},
+                        new byte[] {(byte) 0xf0})
+                .build();
+    }
+
+    // ref to beacon.decode.BeaconFilterBuilder.eddystoneUrlFilter()
+    private static BleFilter createEddystoneUrlFilter() {
+        return new BleFilter.Builder()
+                .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+                .setServiceData(
+                        EDDYSTONE_SERVICE_DATA_PARCELUUID,
+                        new byte[] {(short) 0x10}, new byte[] {(byte) 0xf0})
+                .build();
+    }
+
+    // ref to beacon.decode.BeaconFilterBuilder.eddystoneEidFilter()
+    private static BleFilter createEddystoneEidFilter() {
+        return new BleFilter.Builder()
+                .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+                .setServiceData(
+                        EDDYSTONE_SERVICE_DATA_PARCELUUID,
+                        new byte[] {(short) 0x30}, new byte[] {(byte) 0xf0})
+                .build();
+    }
+
+    // ref to beacon.decode.BeaconFilterBuilder.iBeaconWithoutUuidFilter()
+    private static BleFilter createIBeaconWithoutUuidFilter() {
+        byte[] data = {(byte) 0x02, (byte) 0x15};
+        byte[] mask = {(byte) 0xff, (byte) 0xff};
+
+        return new BleFilter.Builder().setManufacturerData((short) 0x004C, data, mask).build();
+    }
+
+    // ref to beacon.decode.BeaconFilterBuilder.iBeaconWithUuidFilter()
+    private static BleFilter createIBeaconWithUuidFilter() {
+        byte[] data = getFilterData(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"));
+        byte[] mask = getFilterMask(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"));
+
+        return new BleFilter.Builder().setManufacturerData((short) 0x004C, data, mask).build();
+    }
+
     // Ref to beacon.decode.AppleBeaconDecoder.getFilterData
     private static byte[] getFilterData(ParcelUuid uuid) {
         byte[] data = new byte[18];
@@ -418,6 +839,20 @@
         return data;
     }
 
+    // Ref to beacon.decode.AppleBeaconDecoder.getFilterMask
+    private static byte[] getFilterMask(ParcelUuid uuid) {
+        byte[] mask = new byte[18];
+        mask[0] = (byte) 0xff;
+        mask[1] = (byte) 0xff;
+        // Check if UUID is needed in data
+        if (uuid != null) {
+            for (int i = 0; i < 16; i++) {
+                mask[i + 2] = (byte) 0xff;
+            }
+        }
+        return mask;
+    }
+
     // Ref to beacon.decode.AppleBeaconDecoder.uuidToByteArray
     private static byte[] uuidToByteArray(ParcelUuid uuid) {
         ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
@@ -453,24 +888,4 @@
         return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
                 == (data.getMostSignificantBits() & mask.getMostSignificantBits()));
     }
-
-    private static String byteString(Map<ParcelUuid, byte[]> bytesMap) {
-        StringBuilder builder = new StringBuilder();
-        for (Map.Entry<ParcelUuid, byte[]> entry : bytesMap.entrySet()) {
-            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
-            builder.append(entry.getKey().toString());
-            builder.append(" --> ");
-            builder.append(byteString(entry.getValue()));
-        }
-        return builder.toString();
-    }
-
-    private static String byteString(SparseArray<byte[]> bytesArray) {
-        StringBuilder builder = new StringBuilder();
-        for (int i = 0; i < bytesArray.size(); i++) {
-            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
-            builder.append(byteString(bytesArray.valueAt(i)));
-        }
-        return builder.toString();
-    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
index 5da98e2..3f9a259 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
@@ -34,6 +34,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
 import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
@@ -238,7 +241,6 @@
         BleRecord record = BleRecord.parseFromBytes(BEACON);
         BleRecord record2 = BleRecord.parseFromBytes(SAME_BEACON);
 
-
         assertThat(record).isEqualTo(record2);
 
         // Different items.
@@ -246,5 +248,48 @@
         assertThat(record).isNotEqualTo(record2);
         assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
     }
+
+    @Test
+    public void testFields() {
+        BleRecord record = BleRecord.parseFromBytes(BEACON);
+        assertThat(byteString(record.getManufacturerSpecificData()))
+                .isEqualTo("  0215F7826DA64FA24E988024BC5B71E0893E44D02522B3");
+        assertThat(
+                byteString(record.getServiceData(
+                        ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB"))))
+                .isEqualTo("[null]");
+        assertThat(record.getTxPowerLevel()).isEqualTo(-12);
+        assertThat(record.toString()).isEqualTo(
+                "BleRecord [advertiseFlags=6, serviceUuids=[], "
+                        + "manufacturerSpecificData={76=[2, 21, -9, -126, 109, -90, 79, -94, 78,"
+                        + " -104, -128, 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]},"
+                        + " serviceData={0000d00d-0000-1000-8000-00805f9b34fb"
+                        + "=[116, 109, 77, 107, 50, 54, 100]},"
+                        + " txPowerLevel=-12, deviceName=Kontakt]");
+    }
+
+    private static String byteString(SparseArray<byte[]> bytesArray) {
+        StringBuilder builder = new StringBuilder();
+        for (int i = 0; i < bytesArray.size(); i++) {
+            builder.append(builder.toString().isEmpty() ? "  " : "\n  ");
+            builder.append(byteString(bytesArray.valueAt(i)));
+        }
+        return builder.toString();
+    }
+
+    private static String byteString(byte[] bytes) {
+        if (bytes == null) {
+            return "[null]";
+        } else {
+            final char[] hexArray = "0123456789ABCDEF".toCharArray();
+            char[] hexChars = new char[bytes.length * 2];
+            for (int i = 0; i < bytes.length; i++) {
+                int v = bytes[i] & 0xFF;
+                hexChars[i * 2] = hexArray[v >>> 4];
+                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+            }
+            return new String(hexChars);
+        }
+    }
 }
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
new file mode 100644
index 0000000..b318842
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+/** Test for Bluetooth LE {@link BleSighting}. */
+public class BleSightingTest {
+    private static final String DEVICE_NAME = "device1";
+    private static final String OTHER_DEVICE_NAME = "device2";
+    private static final long TIME_EPOCH_MILLIS = 123456;
+    private static final long OTHER_TIME_EPOCH_MILLIS = 456789;
+    private static final int RSSI = 1;
+    private static final int OTHER_RSSI = 2;
+
+    private final BluetoothDevice mBluetoothDevice1 =
+            BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+    private final BluetoothDevice mBluetoothDevice2 =
+            BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF");
+
+
+    @Test
+    public void testEquals() {
+        BleSighting sighting =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        BleSighting sighting2 =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        assertThat(sighting.equals(sighting2)).isTrue();
+        assertThat(sighting2.equals(sighting)).isTrue();
+        assertThat(sighting.hashCode()).isEqualTo(sighting2.hashCode());
+
+        // Transitive property.
+        BleSighting sighting3 =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        assertThat(sighting2.equals(sighting3)).isTrue();
+        assertThat(sighting.equals(sighting3)).isTrue();
+
+        // Set different values for each field, one at a time.
+        sighting2 = buildBleSighting(mBluetoothDevice2, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        assertSightingsNotEquals(sighting, sighting2);
+
+        sighting2 = buildBleSighting(mBluetoothDevice1, OTHER_DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        assertSightingsNotEquals(sighting, sighting2);
+
+        sighting2 = buildBleSighting(mBluetoothDevice1, DEVICE_NAME, OTHER_TIME_EPOCH_MILLIS, RSSI);
+        assertSightingsNotEquals(sighting, sighting2);
+
+        sighting2 = buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, OTHER_RSSI);
+        assertSightingsNotEquals(sighting, sighting2);
+    }
+
+    @Test
+    public void getNormalizedRSSI_usingNearbyRssiOffset_getCorrectValue() {
+        BleSighting sighting =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+
+        int defaultRssiOffset = 3;
+        assertThat(sighting.getNormalizedRSSI()).isEqualTo(RSSI + defaultRssiOffset);
+    }
+
+    @Test
+    public void testFields() {
+        BleSighting sighting =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        assertThat(byteString(sighting.getBleRecordBytes()))
+                .isEqualTo("080964657669636531");
+        assertThat(sighting.getRssi()).isEqualTo(RSSI);
+        assertThat(sighting.getTimestampMillis()).isEqualTo(TIME_EPOCH_MILLIS);
+        assertThat(sighting.getTimestampNanos())
+                .isEqualTo(TimeUnit.MILLISECONDS.toNanos(TIME_EPOCH_MILLIS));
+        assertThat(sighting.toString()).isEqualTo(
+                "BleSighting{device=00:11:22:33:44:55,"
+                        + " bleRecord=BleRecord [advertiseFlags=-1,"
+                        + " serviceUuids=[],"
+                        + " manufacturerSpecificData={}, serviceData={},"
+                        + " txPowerLevel=-2147483648,"
+                        + " deviceName=device1],"
+                        + " rssi=1,"
+                        + " timestampNanos=123456000000}");
+    }
+
+    @Test
+    public void testParcelable() {
+        BleSighting sighting =
+                buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+        Parcel dest = Parcel.obtain();
+        sighting.writeToParcel(dest, 0);
+        dest.setDataPosition(0);
+        BleSighting compareSighting = BleSighting.CREATOR.createFromParcel(dest);
+        assertThat(sighting.getRssi()).isEqualTo(RSSI);
+    }
+
+    @Test
+    public void testCreatorNewArray() {
+        BleSighting[]  sightings =
+                BleSighting.CREATOR.newArray(2);
+        assertThat(sightings.length).isEqualTo(2);
+    }
+
+    private static String byteString(byte[] bytes) {
+        if (bytes == null) {
+            return "[null]";
+        } else {
+            final char[] hexArray = "0123456789ABCDEF".toCharArray();
+            char[] hexChars = new char[bytes.length * 2];
+            for (int i = 0; i < bytes.length; i++) {
+                int v = bytes[i] & 0xFF;
+                hexChars[i * 2] = hexArray[v >>> 4];
+                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+            }
+            return new String(hexChars);
+        }
+    }
+
+        /** Builds a BleSighting instance which will correctly match filters by device name. */
+    private static BleSighting buildBleSighting(
+            BluetoothDevice bluetoothDevice, String deviceName, long timeEpochMillis, int rssi) {
+        byte[] nameBytes = deviceName.getBytes(UTF_8);
+        byte[] bleRecordBytes = new byte[nameBytes.length + 2];
+        bleRecordBytes[0] = (byte) (nameBytes.length + 1);
+        bleRecordBytes[1] = 0x09; // Value of private BleRecord.DATA_TYPE_LOCAL_NAME_COMPLETE;
+        System.arraycopy(nameBytes, 0, bleRecordBytes, 2, nameBytes.length);
+
+        return new BleSighting(bluetoothDevice, bleRecordBytes,
+                rssi, TimeUnit.MILLISECONDS.toNanos(timeEpochMillis));
+    }
+
+    private static void assertSightingsNotEquals(BleSighting sighting1, BleSighting sighting2) {
+        assertThat(sighting1.equals(sighting2)).isFalse();
+        assertThat(sighting1.hashCode()).isNotEqualTo(sighting2.hashCode());
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
new file mode 100644
index 0000000..9a9181d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.ble.decode;
+
+import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class BeaconDecoderTest {
+    private BeaconDecoder mBeaconDecoder = new FastPairDecoder();;
+
+    @Test
+    public void testFields() {
+        assertThat(mBeaconDecoder.getTelemetry(parseFromBytes(getFastPairRecord()))).isNull();
+        assertThat(mBeaconDecoder.getUrl(parseFromBytes(getFastPairRecord()))).isNull();
+        assertThat(mBeaconDecoder.supportsBeaconIdAndTxPower(parseFromBytes(getFastPairRecord())))
+                .isTrue();
+        assertThat(mBeaconDecoder.supportsTxPower()).isTrue();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
index 1ad04f8..6552699 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
@@ -17,15 +17,23 @@
 package com.android.server.nearby.common.ble.decode;
 
 import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.DEVICE_ADDRESS;
 import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_MODEL_ID;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.RSSI;
 import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
 import static com.android.server.nearby.common.ble.testing.FastPairTestData.newFastPairRecord;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
 import com.android.server.nearby.common.ble.BleRecord;
+import com.android.server.nearby.common.ble.BleSighting;
+import com.android.server.nearby.common.ble.testing.FastPairTestData;
 import com.android.server.nearby.util.Hex;
 
 import com.google.common.primitives.Bytes;
@@ -34,12 +42,13 @@
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 @RunWith(AndroidJUnit4.class)
 public class FastPairDecoderTest {
     private static final String LONG_MODEL_ID = "1122334455667788";
-    private final FastPairDecoder mDecoder = new FastPairDecoder();
     // Bits 3-6 are model ID length bits = 0b1000 = 8
     private static final byte LONG_MODEL_ID_HEADER = 0b00010000;
     private static final String PADDED_LONG_MODEL_ID = "00001111";
@@ -49,74 +58,239 @@
     private static final byte MODEL_ID_HEADER = 0b00000110;
     private static final String MODEL_ID = "112233";
     private static final byte BLOOM_FILTER_HEADER = 0b01100000;
+    private static final byte BLOOM_FILTER_NO_NOTIFICATION_HEADER = 0b01100010;
     private static final String BLOOM_FILTER = "112233445566";
+    private static final byte LONG_BLOOM_FILTER_HEADER = (byte) 0b10100000;
+    private static final String LONG_BLOOM_FILTER = "00112233445566778899";
     private static final byte BLOOM_FILTER_SALT_HEADER = 0b00010001;
     private static final String BLOOM_FILTER_SALT = "01";
+    private static final byte BATTERY_HEADER = 0b00110011;
+    private static final byte BATTERY_NO_NOTIFICATION_HEADER = 0b00110100;
+    private static final String BATTERY = "01048F";
     private static final byte RANDOM_RESOLVABLE_DATA_HEADER = 0b01000110;
     private static final String RANDOM_RESOLVABLE_DATA = "11223344";
-    private static final byte BLOOM_FILTER_NO_NOTIFICATION_HEADER = 0b01100010;
 
+    private final FastPairDecoder mDecoder = new FastPairDecoder();
+    private final BluetoothDevice mBluetoothDevice =
+            BluetoothAdapter.getDefaultAdapter().getRemoteDevice(DEVICE_ADDRESS);
+
+    @Test
+    public void filter() {
+        assertThat(FastPairDecoder.FILTER.matches(bleSighting(getFastPairRecord()))).isTrue();
+        assertThat(FastPairDecoder.FILTER.matches(bleSighting(FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD)))
+                .isTrue();
+
+        // Any ID is a valid frame.
+        assertThat(FastPairDecoder.FILTER.matches(
+                bleSighting(newFastPairRecord(Hex.stringToBytes("000001"))))).isTrue();
+        assertThat(FastPairDecoder.FILTER.matches(
+                bleSighting(newFastPairRecord(Hex.stringToBytes("098FEC"))))).isTrue();
+        assertThat(FastPairDecoder.FILTER.matches(
+                bleSighting(FastPairTestData.newFastPairRecord(
+                        LONG_MODEL_ID_HEADER, Hex.stringToBytes(LONG_MODEL_ID))))).isTrue();
+    }
 
     @Test
     public void getModelId() {
         assertThat(mDecoder.getBeaconIdBytes(parseFromBytes(getFastPairRecord())))
                 .isEqualTo(FAST_PAIR_MODEL_ID);
-        FastPairServiceData fastPairServiceData1 =
-                new FastPairServiceData(LONG_MODEL_ID_HEADER,
-                        LONG_MODEL_ID);
-        assertThat(
-                mDecoder.getBeaconIdBytes(
-                        newBleRecord(fastPairServiceData1.createServiceData())))
-                .isEqualTo(Hex.stringToBytes(LONG_MODEL_ID));
         FastPairServiceData fastPairServiceData =
-                new FastPairServiceData(PADDED_LONG_MODEL_ID_HEADER,
-                        PADDED_LONG_MODEL_ID);
+                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
         assertThat(
                 mDecoder.getBeaconIdBytes(
                         newBleRecord(fastPairServiceData.createServiceData())))
+                .isEqualTo(Hex.stringToBytes(LONG_MODEL_ID));
+
+        FastPairServiceData fastPairServiceData1 =
+                new FastPairServiceData(PADDED_LONG_MODEL_ID_HEADER, PADDED_LONG_MODEL_ID);
+        assertThat(
+                mDecoder.getBeaconIdBytes(
+                        newBleRecord(fastPairServiceData1.createServiceData())))
                 .isEqualTo(Hex.stringToBytes(TRIMMED_LONG_MODEL_ID));
     }
 
     @Test
-    public void getBloomFilter() {
-        FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
-                MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    public void getBeaconIdType() {
+        assertThat(mDecoder.getBeaconIdType()).isEqualTo(1);
     }
 
     @Test
-    public void getBloomFilter_smallModelId() {
-        FastPairServiceData fastPairServiceData = new FastPairServiceData(null, MODEL_ID);
-        assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+    public void getCalibratedBeaconTxPower() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
+        assertThat(
+                mDecoder.getCalibratedBeaconTxPower(
+                        newBleRecord(fastPairServiceData.createServiceData())))
                 .isNull();
     }
 
     @Test
-    public void getBloomFilterSalt_modelIdAndMultipleExtraFields() {
-        FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
-                MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
-        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+    public void getServiceDataArray() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
         assertThat(
-                FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
+                mDecoder.getServiceDataArray(
+                        newBleRecord(fastPairServiceData.createServiceData())))
+                .isEqualTo(Hex.stringToBytes("101122334455667788"));
     }
 
     @Test
-    public void getRandomResolvableData_whenContainConnectionState() {
-        FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
-                MODEL_ID);
-        fastPairServiceData.mExtraFieldHeaders.add(RANDOM_RESOLVABLE_DATA_HEADER);
-        fastPairServiceData.mExtraFields.add(RANDOM_RESOLVABLE_DATA);
+    public void hasBloomFilter() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
         assertThat(
-                FastPairDecoder.getRandomResolvableData(fastPairServiceData
-                                .createServiceData()))
-                .isEqualTo(Hex.stringToBytes(RANDOM_RESOLVABLE_DATA));
+                mDecoder.hasBloomFilter(
+                        newBleRecord(fastPairServiceData.createServiceData())))
+                .isFalse();
+    }
+
+    @Test
+    public void hasModelId_allCases() {
+        // One type of the format is just the 3-byte model ID. This format has no header byte (all 3
+        // service data bytes are the model ID in little endian).
+        assertThat(hasModelId("112233", mDecoder)).isTrue();
+
+        // If the model ID is shorter than 3 bytes, then return null.
+        assertThat(hasModelId("11", mDecoder)).isFalse();
+
+        // If the data is longer than 3 bytes,
+        // byte 0 must be 0bVVVLLLLR (version, ID length, reserved).
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData((byte) 0b00001000, "11223344");
+        assertThat(
+                FastPairDecoder.hasBeaconIdBytes(
+                        newBleRecord(fastPairServiceData.createServiceData()))).isTrue();
+
+        FastPairServiceData fastPairServiceData1 =
+                new FastPairServiceData((byte) 0b00001010, "1122334455");
+        assertThat(
+                FastPairDecoder.hasBeaconIdBytes(
+                        newBleRecord(fastPairServiceData1.createServiceData()))).isTrue();
+
+        // Length bits correct, but version bits != version 0 (only supported version).
+        FastPairServiceData fastPairServiceData2 =
+                new FastPairServiceData((byte) 0b00101000, "11223344");
+        assertThat(
+                FastPairDecoder.hasBeaconIdBytes(
+                        newBleRecord(fastPairServiceData2.createServiceData()))).isFalse();
+
+        // Version bits correct, but length bits incorrect (too big, too small).
+        FastPairServiceData fastPairServiceData3 =
+                new FastPairServiceData((byte) 0b00001010, "11223344");
+        assertThat(
+                FastPairDecoder.hasBeaconIdBytes(
+                        newBleRecord(fastPairServiceData3.createServiceData()))).isFalse();
+
+        FastPairServiceData fastPairServiceData4 =
+                new FastPairServiceData((byte) 0b00000010, "11223344");
+        assertThat(
+                FastPairDecoder.hasBeaconIdBytes(
+                        newBleRecord(fastPairServiceData4.createServiceData()))).isFalse();
+    }
+
+    @Test
+    public void getBatteryLevel() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
+        fastPairServiceData.mExtraFields.add(BATTERY);
+        assertThat(
+                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BATTERY));
+    }
+
+    @Test
+    public void getBatteryLevel_notIncludedInPacket() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData())).isNull();
+    }
+
+    @Test
+    public void getBatteryLevel_noModelId() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData((byte) 0b00000000, null);
+        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
+        fastPairServiceData.mExtraFields.add(BATTERY);
+        assertThat(
+                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BATTERY));
+    }
+
+    @Test
+    public void getBatteryLevel_multipelExtraField() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
+        fastPairServiceData.mExtraFields.add(BATTERY);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BATTERY));
+    }
+
+    @Test
+    public void getBatteryLevelNoNotification() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
+        fastPairServiceData.mExtraFields.add(BATTERY);
+        assertThat(
+                FastPairDecoder.getBatteryLevelNoNotification(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BATTERY));
+    }
+
+    @Test
+    public void getBatteryLevelNoNotification_notIncludedInPacket() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBatteryLevelNoNotification(
+                        fastPairServiceData.createServiceData())).isNull();
+    }
+
+    @Test
+    public void getBatteryLevelNoNotification_noModelId() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData((byte) 0b00000000, null);
+        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
+        fastPairServiceData.mExtraFields.add(BATTERY);
+        assertThat(
+                FastPairDecoder.getBatteryLevelNoNotification(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BATTERY));
+    }
+
+    @Test
+    public void getBatteryLevelNoNotification_multipleExtraField() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
+        fastPairServiceData.mExtraFields.add(BATTERY);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBatteryLevelNoNotification(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BATTERY));
+    }
+
+    @Test
+    public void getBloomFilter() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
     }
 
     @Test
@@ -125,14 +299,222 @@
                 new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
         fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_NO_NOTIFICATION_HEADER);
         fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(FastPairDecoder.getBloomFilterNoNotification(fastPairServiceData
-                        .createServiceData())).isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+        assertThat(
+                FastPairDecoder.getBloomFilterNoNotification(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @Test
+    public void getBloomFilter_smallModelId() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(null, MODEL_ID);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isNull();
+    }
+
+    @Test
+    public void getBloomFilter_headerVersionBitsNotZero() {
+        // Header bits are defined as 0bVVVLLLLR (V=version, L=length, R=reserved), must be zero.
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData((byte) 0b00100000, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isNull();
+    }
+
+    @Test
+    public void getBloomFilter_noExtraFieldBytesIncluded() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(null);
+        fastPairServiceData.mExtraFields.add(null);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isNull();
+    }
+
+    @Test
+    public void getBloomFilter_extraFieldLengthIsZero() {
+        // The extra field header is formatted as 0bLLLLTTTT (L=length, T=type).
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00000000);
+        fastPairServiceData.mExtraFields.add(null);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .hasLength(0);
+    }
+
+    @Test
+    public void getBloomFilter_extraFieldLengthLongerThanPacket() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b11110000);
+        fastPairServiceData.mExtraFields.add("1122");
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isNull();
+    }
+
+    @Test
+    public void getBloomFilter_secondExtraFieldLengthLongerThanPacket() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00100000);
+        fastPairServiceData.mExtraFields.add("1122");
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b11110001);
+        fastPairServiceData.mExtraFields.add("3344");
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData())).isNull();
+    }
+
+    @Test
+    public void getBloomFilter_typeIsWrong() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b01100001);
+        fastPairServiceData.mExtraFields.add("112233445566");
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isNull();
+    }
+
+    @Test
+    public void getBloomFilter_noModelId() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData((byte) 0b00000000, null);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @Test
+    public void getBloomFilter_noModelIdAndMultipleExtraFields() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData((byte) 0b00000000, null);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00010001);
+        fastPairServiceData.mExtraFields.add("00");
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @Test
+    public void getBloomFilter_modelIdAndMultipleExtraFields() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @Test
+    public void getBloomFilterSalt_modelIdAndMultipleExtraFields() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+        assertThat(
+                FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
+    }
+
+    @Test
+    public void getBloomFilter_modelIdAndMultipleExtraFieldsWithBloomFilterLast() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00010001);
+        fastPairServiceData.mExtraFields.add("1A");
+        fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00100010);
+        fastPairServiceData.mExtraFields.add("2CFE");
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @Test
+    public void getBloomFilter_modelIdAndMultipleExtraFieldsWithSameType() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add("000000000000");
+        assertThat(
+                FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @Test
+    public void getBloomFilter_longExtraField() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(LONG_BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(LONG_BLOOM_FILTER);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add("000000000000");
+        assertThat(
+                FastPairDecoder.getBloomFilter(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(LONG_BLOOM_FILTER));
+    }
+
+    @Test
+    public void getRandomResolvableData_whenNoConnectionState() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        assertThat(
+                FastPairDecoder.getRandomResolvableData(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(null);
+    }
+
+    @Test
+    public void getRandomResolvableData_whenContainConnectionState() {
+        FastPairServiceData fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(RANDOM_RESOLVABLE_DATA_HEADER);
+        fastPairServiceData.mExtraFields.add(RANDOM_RESOLVABLE_DATA);
+        assertThat(
+                FastPairDecoder.getRandomResolvableData(
+                        fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(RANDOM_RESOLVABLE_DATA));
     }
 
     private static BleRecord newBleRecord(byte[] serviceDataBytes) {
         return parseFromBytes(newFastPairRecord(serviceDataBytes));
     }
-    class FastPairServiceData {
+
+    private static boolean hasModelId(String modelId, FastPairDecoder decoder) {
+        byte[] modelIdBytes = Hex.stringToBytes(modelId);
+        BleRecord bleRecord =
+                parseFromBytes(FastPairTestData.newFastPairRecord((byte) 0, modelIdBytes));
+        return FastPairDecoder.hasBeaconIdBytes(bleRecord)
+                && Arrays.equals(decoder.getBeaconIdBytes(bleRecord), modelIdBytes);
+    }
+
+    private BleSighting bleSighting(byte[] frame) {
+        return new BleSighting(mBluetoothDevice, frame, RSSI,
+                TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()));
+    }
+
+    static class FastPairServiceData {
         private Byte mHeader;
         private String mModelId;
         List<Byte> mExtraFieldHeaders = new ArrayList<>();
@@ -164,6 +546,4 @@
             return serviceData;
         }
     }
-
-
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java
new file mode 100644
index 0000000..0f5877b
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.ble.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.nearby.common.ble.BleRecord;
+
+import org.junit.Test;
+
+public class StringUtilsTest {
+    // iBeacon (Apple) Packet 1
+    private static final byte[] BEACON = {
+            // Flags
+            (byte) 0x02,
+            (byte) 0x01,
+            (byte) 0x06,
+            // Manufacturer-specific data header
+            (byte) 0x1a,
+            (byte) 0xff,
+            (byte) 0x4c,
+            (byte) 0x00,
+            // iBeacon Type
+            (byte) 0x02,
+            // Frame length
+            (byte) 0x15,
+            // iBeacon Proximity UUID
+            (byte) 0xf7,
+            (byte) 0x82,
+            (byte) 0x6d,
+            (byte) 0xa6,
+            (byte) 0x4f,
+            (byte) 0xa2,
+            (byte) 0x4e,
+            (byte) 0x98,
+            (byte) 0x80,
+            (byte) 0x24,
+            (byte) 0xbc,
+            (byte) 0x5b,
+            (byte) 0x71,
+            (byte) 0xe0,
+            (byte) 0x89,
+            (byte) 0x3e,
+            // iBeacon Instance ID (Major/Minor)
+            (byte) 0x44,
+            (byte) 0xd0,
+            (byte) 0x25,
+            (byte) 0x22,
+            // Tx Power
+            (byte) 0xb3,
+            // RSP
+            (byte) 0x08,
+            (byte) 0x09,
+            (byte) 0x4b,
+            (byte) 0x6f,
+            (byte) 0x6e,
+            (byte) 0x74,
+            (byte) 0x61,
+            (byte) 0x6b,
+            (byte) 0x74,
+            (byte) 0x02,
+            (byte) 0x0a,
+            (byte) 0xf4,
+            (byte) 0x0a,
+            (byte) 0x16,
+            (byte) 0x0d,
+            (byte) 0xd0,
+            (byte) 0x74,
+            (byte) 0x6d,
+            (byte) 0x4d,
+            (byte) 0x6b,
+            (byte) 0x32,
+            (byte) 0x36,
+            (byte) 0x64,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00
+    };
+
+    @Test
+    public void testToString() {
+        BleRecord record = BleRecord.parseFromBytes(BEACON);
+        assertThat(StringUtils.toString(record.getManufacturerSpecificData()))
+                .isEqualTo("{76=[2, 21, -9, -126, 109, -90, 79, -94, 78, -104, -128,"
+                        + " 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]}");
+        assertThat(StringUtils.toString(record.getServiceData()))
+                .isEqualTo("{0000d00d-0000-1000-8000-00805f9b34fb="
+                        + "[116, 109, 77, 107, 50, 54, 100]}");
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
new file mode 100644
index 0000000..30df81f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bloomfilter;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit-tests for the {@link BloomFilter} class.
+ */
+public class BloomFilterTest {
+    private static final int BYTE_ARRAY_LENGTH = 100;
+
+    private final BloomFilter mBloomFilter =
+            new BloomFilter(new byte[BYTE_ARRAY_LENGTH], newHasher());
+
+    public BloomFilter.Hasher newHasher() {
+        return new FastPairBloomFilterHasher();
+    }
+
+    @Test
+    public void emptyFilter_returnsEmptyArray() throws Exception {
+        assertThat(mBloomFilter.asBytes()).isEqualTo(new byte[BYTE_ARRAY_LENGTH]);
+    }
+
+    @Test
+    public void emptyFilter_neverContains() throws Exception {
+        assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
+        assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
+        assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
+    }
+
+    @Test
+    public void add() throws Exception {
+        assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
+        mBloomFilter.add(element(1));
+        assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
+    }
+
+    @Test
+    public void add_onlyGivenArgAdded() throws Exception {
+        mBloomFilter.add(element(1));
+        assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
+        assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
+        assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
+    }
+
+    @Test
+    public void add_multipleArgs() throws Exception {
+        mBloomFilter.add(element(1));
+        mBloomFilter.add(element(2));
+        assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
+        assertThat(mBloomFilter.possiblyContains(element(2))).isTrue();
+        assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
+    }
+
+    /**
+     * This test was added because of a bug where the BloomFilter doesn't utilize all bits given.
+     * Functionally, the filter still works, but we just have a much higher false positive rate. The
+     * bug was caused by confusing bit length and byte length, which made our BloomFilter only set
+     * bits on the first byteLength (bitLength / 8) bits rather than the whole bitLength bits.
+     *
+     * <p>Here, we're verifying that the bits set are somewhat scattered. So instead of something
+     * like [ 0, 1, 1, 0, 0, 0, 0, ..., 0 ], we should be getting something like
+     * [ 0, 1, 0, 0, 1, 1, 0, 0,0, 1, ..., 1, 0].
+     */
+    @Test
+    public void randomness_noEndBias() throws Exception {
+        // Add one element to our BloomFilter.
+        mBloomFilter.add(element(1));
+
+        // Record the amount of non-zero bytes and the longest streak of zero bytes in the resulting
+        // BloomFilter. This is an approximation of reasonable distribution since we're recording by
+        // bytes instead of bits.
+        int nonZeroCount = 0;
+        int longestZeroStreak = 0;
+        int currentZeroStreak = 0;
+        for (byte b : mBloomFilter.asBytes()) {
+            if (b == 0) {
+                currentZeroStreak++;
+            } else {
+                // Increment the number of non-zero bytes we've seen, update the longest zero
+                // streak, and then reset the current zero streak.
+                nonZeroCount++;
+                longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
+                currentZeroStreak = 0;
+            }
+        }
+        // Update the longest zero streak again for the tail case.
+        longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
+
+        // Since randomness is hard to measure within one unit test, we instead do a valid check.
+        // All non-zero bytes should not be packed into one end of the array.
+        //
+        // In this case, the size of one end is approximated to be:
+        //   BYTE_ARRAY_LENGTH / nonZeroCount.
+        // Therefore, the longest zero streak should be less than:
+        //   BYTE_ARRAY_LENGTH - one end of the array.
+        int longestAcceptableZeroStreak = BYTE_ARRAY_LENGTH - (BYTE_ARRAY_LENGTH / nonZeroCount);
+        assertThat(longestZeroStreak).isAtMost(longestAcceptableZeroStreak);
+    }
+
+    @Test
+    public void randomness_falsePositiveRate() throws Exception {
+        // Create a new BloomFilter with a length of only 10 bytes.
+        BloomFilter bloomFilter = new BloomFilter(new byte[10], newHasher());
+
+        // Add 5 distinct elements to the BloomFilter.
+        for (int i = 0; i < 5; i++) {
+            bloomFilter.add(element(i));
+        }
+
+        // Now test 100 other elements and record the number of false positives.
+        int falsePositives = 0;
+        for (int i = 5; i < 105; i++) {
+            falsePositives += bloomFilter.possiblyContains(element(i)) ? 1 : 0;
+        }
+
+        // We expect the false positive rate to be 3% with 5 elements in a 10 byte filter. Thus,
+        // we give a little leeway and verify that the false positive rate is no more than 5%.
+        assertWithMessage(
+                String.format(
+                        "False positive rate too large. Expected <= 5%%, but got %d%%.",
+                        falsePositives))
+                .that(falsePositives <= 5)
+                .isTrue();
+        System.out.printf("False positive rate: %d%%%n", falsePositives);
+    }
+
+
+    private String element(int index) {
+        return "ELEMENT_" + index;
+    }
+
+    @Test
+    public void specificBitPattern() throws Exception {
+        // Create a new BloomFilter along with a fixed set of elements
+        // and bit patterns to verify with.
+        BloomFilter bloomFilter = new BloomFilter(new byte[6], newHasher());
+        // Combine an account key and mac address.
+        byte[] element =
+                concat(
+                        base16().decode("11223344556677889900AABBCCDDEEFF"),
+                        base16().withSeparator(":", 2).decode("84:68:3E:00:02:11"));
+        byte[] expectedBitPattern = new byte[] {0x50, 0x00, 0x04, 0x15, 0x08, 0x01};
+
+        // Add the fixed elements to the filter.
+        bloomFilter.add(element);
+
+        // Verify that the resulting bytes match the expected one.
+        byte[] bloomFilterBytes = bloomFilter.asBytes();
+        assertWithMessage(
+                "Unexpected bit pattern. Expected %s, but got %s.",
+                base16().encode(expectedBitPattern), base16().encode(bloomFilterBytes))
+                .that(Arrays.equals(expectedBitPattern, bloomFilterBytes))
+                .isTrue();
+
+        // Verify that the expected bit pattern creates a BloomFilter containing all fixed elements.
+        bloomFilter = new BloomFilter(expectedBitPattern, newHasher());
+        assertThat(bloomFilter.possiblyContains(element)).isTrue();
+    }
+
+    // This test case has been on the spec,
+    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
+    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
+    // address) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasOneAccountKey() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
+        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+
+        // Add the fixed elements to the filter.
+        bloomFilter1.add(concat(accountKey, salt1));
+        bloomFilter2.add(concat(accountKey, salt2));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("0A428810"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("020C802A"));
+    }
+
+    // Adds this test case to spec. We can easily change the parameters (e.g. account key, ble
+    // address, battery data) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasOneAccountKey_withBatteryData() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
+        byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+        byte[] batteryData = {
+                0b00110011, // length = 3, show UI indication.
+                0b01000000, // Left bud: not charging, battery level = 64.
+                0b01000000, // Right bud: not charging, battery level = 64.
+                0b01000000 // Case: not charging, battery level = 64.
+        };
+
+        // Adds battery data to build bloom filter.
+        bloomFilter1.add(concat(accountKey, salt1, batteryData));
+        bloomFilter2.add(concat(accountKey, salt2, batteryData));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("4A00F000"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("0101460A"));
+    }
+
+    // This test case has been on the spec,
+    // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
+    // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
+    // address) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasTwoAccountKeys() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+
+        // Adds the fixed elements to the filter.
+        bloomFilter1.add(concat(accountKey1, salt1));
+        bloomFilter1.add(concat(accountKey2, salt1));
+        bloomFilter2.add(concat(accountKey1, salt2));
+        bloomFilter2.add(concat(accountKey2, salt2));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("2FBA064200"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("844A62208B"));
+    }
+
+    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
+    // address, battery data) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasTwoAccountKeys_withBatteryData() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+        byte[] batteryData = {
+                0b00110011, // length = 3, show UI indication.
+                0b01000000, // Left bud: not charging, battery level = 64.
+                0b01000000, // Right bud: not charging, battery level = 64.
+                0b01000000 // Case: not charging, battery level = 64.
+        };
+
+        // Adds battery data to build bloom filter.
+        bloomFilter1.add(concat(accountKey1, salt1, batteryData));
+        bloomFilter1.add(concat(accountKey2, salt1, batteryData));
+        bloomFilter2.add(concat(accountKey1, salt2, batteryData));
+        bloomFilter2.add(concat(accountKey2, salt2, batteryData));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("102256C04D"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("461524D008"));
+    }
+
+    // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
+    // address, battery data and battery remaining time) to clarify test results with partners.
+    @Test
+    public void specificBitPattern_hasTwoAccountKeys_withBatteryLevelAndRemainingTime() {
+        BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+        BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+        byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+        byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+        byte[] salt1 = base16().decode("C7");
+        byte[] salt2 = base16().decode("C7C8");
+        byte[] batteryData = {
+                0b00110011, // length = 3, show UI indication.
+                0b01000000, // Left bud: not charging, battery level = 64.
+                0b01000000, // Right bud: not charging, battery level = 64.
+                0b01000000 // Case: not charging, battery level = 64.
+        };
+        byte[] batteryRemainingTime = {
+                0b00010101, // length = 1, type = 0b0101 (remaining battery time).
+                0x1E, // remaining battery time (in minutes) =  30 minutes.
+        };
+
+        // Adds battery data to build bloom filter.
+        bloomFilter1.add(concat(accountKey1, salt1, batteryData, batteryRemainingTime));
+        bloomFilter1.add(concat(accountKey2, salt1, batteryData, batteryRemainingTime));
+        bloomFilter2.add(concat(accountKey1, salt2, batteryData, batteryRemainingTime));
+        bloomFilter2.add(concat(accountKey2, salt2, batteryData, batteryRemainingTime));
+
+        assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("32A086B41A"));
+        assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("C2A042043E"));
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
new file mode 100644
index 0000000..0923b95
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bloomfilter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+
+public class FastPairBloomFilterHasherTest {
+    private static final int BYTE_ARRAY_LENGTH = 100;
+    private static final Charset CHARSET = UTF_8;
+    private static FastPairBloomFilterHasher sFastPairBloomFilterHasher =
+            new FastPairBloomFilterHasher();
+    @Test
+    public void getHashes() {
+        int[] hashe1 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
+        int[] hashe2 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
+        int[] hashe3 = sFastPairBloomFilterHasher.getHashes(element(2).getBytes(CHARSET));
+        assertThat(hashe1).isEqualTo(hashe2);
+        assertThat(hashe1).isNotEqualTo(hashe3);
+    }
+
+    private String element(int index) {
+        return "ELEMENT_" + index;
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java
new file mode 100644
index 0000000..2e46ef9
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import junit.framework.TestCase;
+
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link Bytes}.
+ */
+public class BytesTest extends TestCase {
+
+    private static final Bytes.Value VALUE1 =
+            new Bytes.Value(new byte[]{1, 2}, ByteOrder.BIG_ENDIAN);
+    private static final Bytes.Value VALUE2 =
+            new Bytes.Value(new byte[]{1, 2}, ByteOrder.BIG_ENDIAN);
+    private static final Bytes.Value VALUE3 =
+            new Bytes.Value(new byte[]{1, 3}, ByteOrder.BIG_ENDIAN);
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEquals_asExpected()  {
+        assertThat(VALUE1.equals(VALUE2)).isTrue();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testNotEquals_asExpected()  {
+        assertThat(VALUE1.equals(VALUE3)).isFalse();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetBytes_asExpected()  {
+        assertThat(Arrays.equals(VALUE1.getBytes(ByteOrder.BIG_ENDIAN), new byte[]{1, 2})).isTrue();
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testToString()  {
+        assertThat(VALUE1.toString()).isEqualTo("0102");
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
index f7ffa24..6684fbc 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
@@ -33,6 +33,8 @@
 import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
 import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
 
+import com.google.common.collect.ImmutableList;
+
 import junit.framework.TestCase;
 
 import org.mockito.Mock;
@@ -44,6 +46,8 @@
  */
 public class ConstantsTest extends TestCase {
 
+    private static final int PASSKEY = 32689;
+
     @Mock
     private BluetoothGattConnection mMockGattConnection;
 
@@ -78,4 +82,62 @@
         assertThat(KeyBasedPairingCharacteristic.getId(mMockGattConnection))
                 .isEqualTo(OLD_KEY_BASE_PAIRING_CHARACTERISTICS);
     }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_accountKeyCharacteristic_notCrash() throws BluetoothException {
+        Constants.FastPairService.AccountKeyCharacteristic.getId(mMockGattConnection);
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_additionalDataCharacteristic_notCrash() throws BluetoothException {
+        Constants.FastPairService.AdditionalDataCharacteristic.getId(mMockGattConnection);
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_nameCharacteristic_notCrash() throws BluetoothException {
+        Constants.FastPairService.NameCharacteristic.getId(mMockGattConnection);
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_passKeyCharacteristic_encryptDecryptSuccessfully()
+            throws java.security.GeneralSecurityException {
+        byte[] secret = AesEcbSingleBlockEncryption.generateKey();
+
+        Constants.FastPairService.PasskeyCharacteristic.getId(mMockGattConnection);
+        assertThat(
+                Constants.FastPairService.PasskeyCharacteristic.decrypt(
+                        Constants.FastPairService.PasskeyCharacteristic.Type.SEEKER,
+                        secret,
+                        Constants.FastPairService.PasskeyCharacteristic.encrypt(
+                                Constants.FastPairService.PasskeyCharacteristic.Type.SEEKER,
+                                secret,
+                                PASSKEY))
+        ).isEqualTo(PASSKEY);
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_beaconActionsCharacteristic_notCrash() throws BluetoothException {
+        Constants.FastPairService.BeaconActionsCharacteristic.getId(mMockGattConnection);
+        for (byte b : ImmutableList.of(
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .READ_BEACON_PARAMETERS,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .READ_PROVISIONING_STATE,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .SET_EPHEMERAL_IDENTITY_KEY,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .CLEAR_EPHEMERAL_IDENTITY_KEY,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .READ_EPHEMERAL_IDENTITY_KEY,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .RING,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .READ_RINGING_STATE,
+                (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+                        .UNKNOWN
+        )) {
+            assertThat(Constants.FastPairService.BeaconActionsCharacteristic
+                    .valueOf(b)).isEqualTo(b);
+        }
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java
new file mode 100644
index 0000000..052e696
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link CreateBondException}.
+ */
+public class CreateBondExceptionTest extends TestCase {
+
+    private static final String FORMAT = "FORMAT";
+    private static final int REASON = 0;
+    private static final CreateBondException EXCEPTION = new CreateBondException(
+            FastPairEventIntDefs.CreateBondErrorCode.INCORRECT_VARIANT, REASON, FORMAT);
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_getter_asExpected() throws BluetoothException {
+        assertThat(EXCEPTION.getErrorCode()).isEqualTo(
+                FastPairEventIntDefs.CreateBondErrorCode.INCORRECT_VARIANT);
+        assertThat(EXCEPTION.getReason()).isSameInstanceAs(REASON);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java
new file mode 100644
index 0000000..94bf111
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.fastpair;
+
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link DeviceIntentReceiver}.
+ */
+public class DeviceIntentReceiverTest extends TestCase {
+    @Mock Preferences mPreferences;
+    @Mock BluetoothDevice mBluetoothDevice;
+
+    private DeviceIntentReceiver mDeviceIntentReceiver;
+    private Intent mIntent;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        initMocks(this);
+
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        mDeviceIntentReceiver = DeviceIntentReceiver.oneShotReceiver(
+                context, mPreferences, mBluetoothDevice);
+
+        mIntent = new Intent().putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
+    }
+
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void test_onReceive_notCrash() throws Exception {
+        mDeviceIntentReceiver.onReceive(mIntent);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
index 28e925f..1b63ad0 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
@@ -58,6 +58,13 @@
                         .setBluetoothDevice(BLUETOOTH_DEVICE)
                         .setProfile(PROFILE)
                         .build();
+        assertThat(event.hasBluetoothDevice()).isTrue();
+        assertThat(event.hasProfile()).isTrue();
+        assertThat(event.isFailure()).isTrue();
+        assertThat(event.toString()).isEqualTo(
+                "Event{eventCode=1120, timestamp=1234, profile=1, "
+                        + "bluetoothDevice=11:22:33:44:55:66, "
+                        + "exception=java.lang.Exception: Test exception}");
 
         Parcel parcel = Parcel.obtain();
         event.writeToParcel(parcel, event.describeContents());
@@ -70,5 +77,8 @@
         assertThat(result.getEventCode()).isEqualTo(event.getEventCode());
         assertThat(result.getBluetoothDevice()).isEqualTo(event.getBluetoothDevice());
         assertThat(result.getProfile()).isEqualTo(event.getProfile());
+        assertThat(result.equals(event)).isTrue();
+
+        assertThat(Event.CREATOR.newArray(10)).isNotEmpty();
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java
new file mode 100644
index 0000000..5763d69
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.BaseEncoding;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.GeneralSecurityException;
+import java.time.Duration;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link HandshakeHandler}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class HandshakeHandlerTest {
+
+    public static final byte[] PUBLIC_KEY =
+            BaseEncoding.base64().decode(
+                    "d2JTfvfdS6u7LmGfMOmco3C7ra3lW1k17AOly0LrBydDZURacfTY"
+                            + "IMmo5K1ejfD9e8b6qHsDTNzselhifi10kQ==");
+    private static final String SEEKER_ADDRESS = "A1:A2:A3:A4:A5:A6";
+    private static final String PROVIDER_BLE_ADDRESS = "11:22:33:44:55:66";
+    private static final byte[] SHARED_SECRET =
+            BaseEncoding.base16().decode("0123456789ABCDEF0123456789ABCDEF");
+
+    @Mock EventLoggerWrapper mEventLoggerWrapper;
+    @Mock BluetoothGattConnection mBluetoothGattConnection;
+    @Mock BluetoothGattConnection.ChangeObserver mChangeObserver;
+    @Mock private Consumer<Integer> mRescueFromError;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void handshakeGattError_noRetryError_failed() throws BluetoothException {
+        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+                new HandshakeHandler.KeyBasedPairingRequest.Builder()
+                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+                        .build();
+        BluetoothGattException exception =
+                new BluetoothGattException("Exception for no retry", 257);
+        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+        GattConnectionManager gattConnectionManager =
+                createGattConnectionManager(Preferences.builder(), () -> {});
+        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+        when(mBluetoothGattConnection.enableNotification(any(), any()))
+                .thenReturn(mChangeObserver);
+        InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+        assertThrows(
+                BluetoothGattException.class,
+                () ->
+                        getHandshakeHandler(gattConnectionManager, address -> address)
+                                .doHandshakeWithRetryAndSignalLostCheck(
+                                        PUBLIC_KEY,
+                                        keyBasedPairingRequest,
+                                        mRescueFromError));
+
+        inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
+                NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void handshakeGattError_retryAndNoCount_throwException() throws BluetoothException {
+        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+                new HandshakeHandler.KeyBasedPairingRequest.Builder()
+                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+                        .build();
+        BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
+        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+        GattConnectionManager gattConnectionManager =
+                createGattConnectionManager(Preferences.builder(), () -> {});
+        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+        when(mBluetoothGattConnection.enableNotification(any(), any()))
+                .thenReturn(mChangeObserver);
+        InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+        HandshakeHandler.HandshakeException handshakeException =
+                assertThrows(
+                        HandshakeHandler.HandshakeException.class,
+                        () -> getHandshakeHandler(gattConnectionManager, address -> address)
+                                .doHandshakeWithRetryAndSignalLostCheck(
+                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+        inOrder.verify(mEventLoggerWrapper)
+                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        inOrder.verify(mEventLoggerWrapper)
+                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        inOrder.verify(mEventLoggerWrapper)
+                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        inOrder.verify(mEventLoggerWrapper)
+                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        inOrder.verifyNoMoreInteractions();
+        assertThat(handshakeException.getOriginalException()).isEqualTo(exception);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void handshakeGattError_noRetryOnTimeout_throwException() throws BluetoothException {
+        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+                new HandshakeHandler.KeyBasedPairingRequest.Builder()
+                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+                        .build();
+        BluetoothOperationExecutor.BluetoothOperationTimeoutException exception =
+                new BluetoothOperationExecutor.BluetoothOperationTimeoutException("Test timeout");
+        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+        GattConnectionManager gattConnectionManager =
+                createGattConnectionManager(Preferences.builder(), () -> {});
+        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+        when(mBluetoothGattConnection.enableNotification(any(), any()))
+                .thenReturn(mChangeObserver);
+        InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+        assertThrows(
+                HandshakeHandler.HandshakeException.class,
+                () ->
+                        new HandshakeHandler(
+                                gattConnectionManager,
+                                PROVIDER_BLE_ADDRESS,
+                                Preferences.builder().setRetrySecretHandshakeTimeout(false).build(),
+                                mEventLoggerWrapper,
+                                address -> address)
+                                .doHandshakeWithRetryAndSignalLostCheck(
+                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+        inOrder.verify(mEventLoggerWrapper)
+                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void handshakeGattError_signalLost() throws BluetoothException {
+        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+                new HandshakeHandler.KeyBasedPairingRequest.Builder()
+                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+                        .build();
+        BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
+        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+        GattConnectionManager gattConnectionManager =
+                createGattConnectionManager(Preferences.builder(), () -> {});
+        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+        when(mBluetoothGattConnection.enableNotification(any(), any()))
+                .thenReturn(mChangeObserver);
+        InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+        SignalLostException signalLostException =
+                assertThrows(
+                        SignalLostException.class,
+                        () -> getHandshakeHandler(gattConnectionManager, address -> null)
+                                .doHandshakeWithRetryAndSignalLostCheck(
+                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+        inOrder.verify(mEventLoggerWrapper)
+                .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        assertThat(signalLostException).hasCauseThat().isEqualTo(exception);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void handshakeGattError_addressRotate() throws BluetoothException {
+        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+                new HandshakeHandler.KeyBasedPairingRequest.Builder()
+                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+                        .build();
+        BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
+        when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+        GattConnectionManager gattConnectionManager =
+                createGattConnectionManager(Preferences.builder(), () -> {});
+        gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+        when(mBluetoothGattConnection.enableNotification(any(), any()))
+                .thenReturn(mChangeObserver);
+        InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+        SignalRotatedException signalRotatedException =
+                assertThrows(
+                        SignalRotatedException.class,
+                        () -> getHandshakeHandler(
+                                gattConnectionManager, address -> "AA:BB:CC:DD:EE:FF")
+                                .doHandshakeWithRetryAndSignalLostCheck(
+                                        PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+        inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
+                NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+        inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+        assertThat(signalRotatedException.getNewAddress()).isEqualTo("AA:BB:CC:DD:EE:FF");
+        assertThat(signalRotatedException).hasCauseThat().isEqualTo(exception);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void constructBytes_setRetroactiveFlag_decodeCorrectly() throws
+            GeneralSecurityException {
+        HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+                new HandshakeHandler.KeyBasedPairingRequest.Builder()
+                        .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+                        .addFlag(REQUEST_RETROACTIVE_PAIR)
+                        .setSeekerPublicAddress(BluetoothAddress.decode(SEEKER_ADDRESS))
+                        .build();
+
+        byte[] encryptedRawMessage =
+                AesEcbSingleBlockEncryption.encrypt(
+                        SHARED_SECRET, keyBasedPairingRequest.getBytes());
+        HandshakeRequest handshakeRequest =
+                new HandshakeRequest(SHARED_SECRET, encryptedRawMessage);
+
+        assertThat(handshakeRequest.getType())
+                .isEqualTo(HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST);
+        assertThat(handshakeRequest.requestRetroactivePair()).isTrue();
+        assertThat(handshakeRequest.getVerificationData())
+                .isEqualTo(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS));
+        assertThat(handshakeRequest.getSeekerPublicAddress())
+                .isEqualTo(BluetoothAddress.decode(SEEKER_ADDRESS));
+        assertThat(handshakeRequest.requestDeviceName()).isFalse();
+        assertThat(handshakeRequest.requestDiscoverable()).isFalse();
+        assertThat(handshakeRequest.requestProviderInitialBonding()).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void getTimeout_notOverShortRetryMaxSpentTime_getShort() {
+        Preferences preferences = Preferences.builder().build();
+
+        assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
+                .getTimeoutMs(
+                        preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+                                - 1))
+                .isEqualTo(preferences.getSecretHandshakeShortTimeoutMs());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void getTimeout_overShortRetryMaxSpentTime_getLong() {
+        Preferences preferences = Preferences.builder().build();
+
+        assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
+                .getTimeoutMs(
+                        preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+                                + 1))
+                .isEqualTo(preferences.getSecretHandshakeLongTimeoutMs());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void getTimeout_retryNotEnabled_getOrigin() {
+        Preferences preferences = Preferences.builder().build();
+
+        assertThat(
+                new HandshakeHandler(
+                        createGattConnectionManager(Preferences.builder(), () -> {}),
+                        PROVIDER_BLE_ADDRESS,
+                        Preferences.builder()
+                                .setRetryGattConnectionAndSecretHandshake(false).build(),
+                        mEventLoggerWrapper,
+                        /* fastPairSignalChecker= */ null)
+                        .getTimeoutMs(0))
+                .isEqualTo(Duration.ofSeconds(
+                        preferences.getGattOperationTimeoutSeconds()).toMillis());
+    }
+
+    private GattConnectionManager createGattConnectionManager(
+            Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
+        return new GattConnectionManager(
+                ApplicationProvider.getApplicationContext(),
+                prefs.build(),
+                new EventLoggerWrapper(null),
+                BluetoothAdapter.getDefaultAdapter(),
+                toggleBluetooth,
+                PROVIDER_BLE_ADDRESS,
+                new TimingLogger("GattConnectionManager", prefs.build()),
+                /* fastPairSignalChecker= */ null,
+                /* setMtu= */ false);
+    }
+
+    private HandshakeHandler getHandshakeHandler(
+            GattConnectionManager gattConnectionManager,
+            @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
+        return new HandshakeHandler(
+                gattConnectionManager,
+                PROVIDER_BLE_ADDRESS,
+                Preferences.builder()
+                        .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of(257))
+                        .setRetrySecretHandshakeTimeout(true)
+                        .build(),
+                mEventLoggerWrapper,
+                fastPairSignalChecker);
+    }
+
+    private HandshakeHandler getHandshakeHandler(
+            boolean getEnable128BitCustomGattCharacteristicsId) {
+        return new HandshakeHandler(
+                createGattConnectionManager(Preferences.builder(), () -> {}),
+                PROVIDER_BLE_ADDRESS,
+                Preferences.builder()
+                        .setGattOperationTimeoutSeconds(5)
+                        .setEnable128BitCustomGattCharacteristicsId(
+                                getEnable128BitCustomGattCharacteristicsId)
+                        .build(),
+                mEventLoggerWrapper,
+                /* fastPairSignalChecker= */ null);
+    }
+
+    private static class HandshakeRequest {
+
+        /**
+         * 16 bytes data: 1-byte for type, 1-byte for flags, 6-bytes for provider's BLE address, 8
+         * bytes optional data.
+         *
+         * @see {go/fast-pair-spec-handshake-message1}
+         */
+        private final byte[] mDecryptedMessage;
+
+        HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
+                throws GeneralSecurityException {
+            mDecryptedMessage = AesEcbSingleBlockEncryption.decrypt(key, encryptedPairingRequest);
+        }
+
+        /**
+         * Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based
+         * Pairing Request and 0x10 for Action Request.
+         */
+        public Type getType() {
+            return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
+        }
+
+        /**
+         * Gets verification data of this handshake request.
+         * Currently, we use Provider's BLE address.
+         */
+        public byte[] getVerificationData() {
+            return Arrays.copyOfRange(
+                    mDecryptedMessage,
+                    Request.VERIFICATION_DATA_INDEX,
+                    Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
+        }
+
+        /** Gets Seeker's public address of the handshake request. */
+        public byte[] getSeekerPublicAddress() {
+            return Arrays.copyOfRange(
+                    mDecryptedMessage,
+                    Request.SEEKER_PUBLIC_ADDRESS_INDEX,
+                    Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
+        }
+
+        /** Checks whether the Seeker request discoverability from flags byte. */
+        public boolean requestDiscoverable() {
+            return (getFlags() & REQUEST_DISCOVERABLE) != 0;
+        }
+
+        /**
+         * Checks whether the Seeker requests that the Provider shall initiate bonding from
+         * flags byte.
+         */
+        public boolean requestProviderInitialBonding() {
+            return (getFlags() & PROVIDER_INITIATES_BONDING) != 0;
+        }
+
+        /** Checks whether the Seeker requests that the Provider shall notify the existing name. */
+        public boolean requestDeviceName() {
+            return (getFlags() & REQUEST_DEVICE_NAME) != 0;
+        }
+
+        /** Checks whether this is for retroactively writing account key. */
+        public boolean requestRetroactivePair() {
+            return (getFlags() & REQUEST_RETROACTIVE_PAIR) != 0;
+        }
+
+        /** Gets the flags of this handshake request. */
+        private byte getFlags() {
+            return mDecryptedMessage[Request.FLAGS_INDEX];
+        }
+
+        /** Checks whether the Seeker requests a device action. */
+        public boolean requestDeviceAction() {
+            return (getFlags() & DEVICE_ACTION) != 0;
+        }
+
+        /**
+         * Checks whether the Seeker requests an action which will be followed by an additional
+         * data.
+         */
+        public boolean requestFollowedByAdditionalData() {
+            return (getFlags() & ADDITIONAL_DATA_CHARACTERISTIC) != 0;
+        }
+
+        /** Gets the {@link AdditionalDataType} of this handshake request. */
+        @AdditionalDataType
+        public int getAdditionalDataType() {
+            if (!requestFollowedByAdditionalData()
+                    || mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
+                return AdditionalDataType.UNKNOWN;
+            }
+            return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
+        }
+
+        /** Enumerates the handshake message types. */
+        public enum Type {
+            KEY_BASED_PAIRING_REQUEST(Request.TYPE_KEY_BASED_PAIRING_REQUEST),
+            ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
+            UNKNOWN((byte) 0xFF);
+
+            private final byte mValue;
+
+            Type(byte type) {
+                mValue = type;
+            }
+
+            public static Type valueOf(byte value) {
+                for (Type type : Type.values()) {
+                    if (type.getValue() == value) {
+                        return type;
+                    }
+                }
+                return UNKNOWN;
+            }
+
+            public byte getValue() {
+                return mValue;
+            }
+        }
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java
new file mode 100644
index 0000000..81a5d92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Ltv}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LtvTest {
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testParseEmpty_throwsException() throws Ltv.ParseException {
+        assertThrows(Ltv.ParseException.class,
+                () -> Ltv.parse(new byte[]{0}));
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testParse_finishesSuccessfully() throws Ltv.ParseException {
+        assertThat(Ltv.parse(new byte[]{3, 4, 5, 6})).isNotEmpty();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java
new file mode 100644
index 0000000..6ebe373
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link BluetoothAdapter}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAdapterTest {
+
+    private static final byte[] BYTES = new byte[]{0, 1, 2, 3, 4, 5};
+    private static final String ADDRESS = "00:11:22:33:AA:BB";
+
+    @Mock private android.bluetooth.BluetoothAdapter mBluetoothAdapter;
+    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+    @Mock private android.bluetooth.le.BluetoothLeAdvertiser mBluetoothLeAdvertiser;
+    @Mock private android.bluetooth.le.BluetoothLeScanner mBluetoothLeScanner;
+
+    BluetoothAdapter mTestabilityBluetoothAdapter;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mTestabilityBluetoothAdapter = BluetoothAdapter.wrap(mBluetoothAdapter);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNullAdapter_isNull() {
+        assertThat(BluetoothAdapter.wrap(null)).isNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+        assertThat(mTestabilityBluetoothAdapter).isNotNull();
+        assertThat(mTestabilityBluetoothAdapter.unwrap()).isSameInstanceAs(mBluetoothAdapter);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testDisable_callsWrapped() {
+        when(mBluetoothAdapter.disable()).thenReturn(true);
+        assertThat(mTestabilityBluetoothAdapter.disable()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEnable_callsWrapped() {
+        when(mBluetoothAdapter.enable()).thenReturn(true);
+        assertThat(mTestabilityBluetoothAdapter.enable()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetBluetoothLeAdvertiser_callsWrapped() {
+        when(mBluetoothAdapter.getBluetoothLeAdvertiser()).thenReturn(mBluetoothLeAdvertiser);
+        assertThat(mTestabilityBluetoothAdapter.getBluetoothLeAdvertiser().unwrap())
+                .isSameInstanceAs(mBluetoothLeAdvertiser);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetBluetoothLeScanner_callsWrapped() {
+        when(mBluetoothAdapter.getBluetoothLeScanner()).thenReturn(mBluetoothLeScanner);
+        assertThat(mTestabilityBluetoothAdapter.getBluetoothLeScanner().unwrap())
+                .isSameInstanceAs(mBluetoothLeScanner);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetBondedDevices_callsWrapped() {
+        when(mBluetoothAdapter.getBondedDevices()).thenReturn(null);
+        assertThat(mTestabilityBluetoothAdapter.getBondedDevices()).isNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testIsDiscovering_pcallsWrapped() {
+        when(mBluetoothAdapter.isDiscovering()).thenReturn(true);
+        assertThat(mTestabilityBluetoothAdapter.isDiscovering()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStartDiscovery_callsWrapped() {
+        when(mBluetoothAdapter.startDiscovery()).thenReturn(true);
+        assertThat(mTestabilityBluetoothAdapter.startDiscovery()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCancelDiscovery_callsWrapped() {
+        when(mBluetoothAdapter.cancelDiscovery()).thenReturn(true);
+        assertThat(mTestabilityBluetoothAdapter.cancelDiscovery()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetRemoteDeviceBytes_callsWrapped() {
+        when(mBluetoothAdapter.getRemoteDevice(BYTES)).thenReturn(mBluetoothDevice);
+        assertThat(mTestabilityBluetoothAdapter.getRemoteDevice(BYTES).unwrap())
+                .isSameInstanceAs(mBluetoothDevice);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetRemoteDeviceString_callsWrapped() {
+        when(mBluetoothAdapter.getRemoteDevice(ADDRESS)).thenReturn(mBluetoothDevice);
+        assertThat(mTestabilityBluetoothAdapter.getRemoteDevice(ADDRESS).unwrap())
+                .isSameInstanceAs(mBluetoothDevice);
+
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java
new file mode 100644
index 0000000..a962b16
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothDevice}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothDeviceTest {
+    private static final UUID UUID_CONST = UUID.randomUUID();
+    private static final String ADDRESS = "ADDRESS";
+    private static final String STRING = "STRING";
+
+    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+    @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
+    @Mock private android.bluetooth.BluetoothSocket mBluetoothSocket;
+    @Mock private android.bluetooth.BluetoothClass mBluetoothClass;
+
+    BluetoothDevice mTestabilityBluetoothDevice;
+    BluetoothGattCallback mTestBluetoothGattCallback;
+    Context mContext;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mTestabilityBluetoothDevice = BluetoothDevice.wrap(mBluetoothDevice);
+        mTestBluetoothGattCallback = new TestBluetoothGattCallback();
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+        assertThat(mTestabilityBluetoothDevice).isNotNull();
+        assertThat(mTestabilityBluetoothDevice.unwrap()).isSameInstanceAs(mBluetoothDevice);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEquality_asExpected() {
+        assertThat(mTestabilityBluetoothDevice.equals(null)).isFalse();
+        assertThat(mTestabilityBluetoothDevice.equals(mTestabilityBluetoothDevice)).isTrue();
+        assertThat(mTestabilityBluetoothDevice.equals(BluetoothDevice.wrap(mBluetoothDevice)))
+                .isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConnectGattWithThreeParameters_callsWrapped() {
+        when(mBluetoothDevice
+                .connectGatt(mContext, true, mTestBluetoothGattCallback.unwrap()))
+                .thenReturn(mBluetoothGatt);
+        assertThat(mTestabilityBluetoothDevice
+                .connectGatt(mContext, true, mTestBluetoothGattCallback)
+                .unwrap())
+                .isSameInstanceAs(mBluetoothGatt);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConnectGattWithFourParameters_callsWrapped() {
+        when(mBluetoothDevice
+                .connectGatt(mContext, true, mTestBluetoothGattCallback.unwrap(), 1))
+                .thenReturn(mBluetoothGatt);
+        assertThat(mTestabilityBluetoothDevice
+                .connectGatt(mContext, true, mTestBluetoothGattCallback, 1)
+                .unwrap())
+                .isSameInstanceAs(mBluetoothGatt);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreateRfcommSocketToServiceRecord_callsWrapped() throws IOException {
+        when(mBluetoothDevice.createRfcommSocketToServiceRecord(UUID_CONST))
+                .thenReturn(mBluetoothSocket);
+        assertThat(mTestabilityBluetoothDevice.createRfcommSocketToServiceRecord(UUID_CONST))
+                .isSameInstanceAs(mBluetoothSocket);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreateInsecureRfcommSocketToServiceRecord_callsWrapped() throws IOException {
+        when(mBluetoothDevice.createInsecureRfcommSocketToServiceRecord(UUID_CONST))
+                .thenReturn(mBluetoothSocket);
+        assertThat(mTestabilityBluetoothDevice
+                .createInsecureRfcommSocketToServiceRecord(UUID_CONST))
+                .isSameInstanceAs(mBluetoothSocket);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetPairingConfirmation_callsWrapped() throws IOException {
+        when(mBluetoothDevice.setPairingConfirmation(true)).thenReturn(true);
+        assertThat(mTestabilityBluetoothDevice.setPairingConfirmation(true)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testFetchUuidsWithSdp_callsWrapped() throws IOException {
+        when(mBluetoothDevice.fetchUuidsWithSdp()).thenReturn(true);
+        assertThat(mTestabilityBluetoothDevice.fetchUuidsWithSdp()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCreateBond_callsWrapped() throws IOException {
+        when(mBluetoothDevice.createBond()).thenReturn(true);
+        assertThat(mTestabilityBluetoothDevice.createBond()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetUuids_callsWrapped() throws IOException {
+        when(mBluetoothDevice.getUuids()).thenReturn(null);
+        assertThat(mTestabilityBluetoothDevice.getUuids()).isNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetBondState_callsWrapped() throws IOException {
+        when(mBluetoothDevice.getBondState()).thenReturn(1);
+        assertThat(mTestabilityBluetoothDevice.getBondState()).isEqualTo(1);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetAddress_callsWrapped() throws IOException {
+        when(mBluetoothDevice.getAddress()).thenReturn(ADDRESS);
+        assertThat(mTestabilityBluetoothDevice.getAddress()).isSameInstanceAs(ADDRESS);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetBluetoothClass_callsWrapped() throws IOException {
+        when(mBluetoothDevice.getBluetoothClass()).thenReturn(mBluetoothClass);
+        assertThat(mTestabilityBluetoothDevice.getBluetoothClass())
+                .isSameInstanceAs(mBluetoothClass);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetType_callsWrapped() throws IOException {
+        when(mBluetoothDevice.getType()).thenReturn(1);
+        assertThat(mTestabilityBluetoothDevice.getType()).isEqualTo(1);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetName_callsWrapped() throws IOException {
+        when(mBluetoothDevice.getName()).thenReturn(STRING);
+        assertThat(mTestabilityBluetoothDevice.getName()).isSameInstanceAs(STRING);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testToString_callsWrapped() {
+        when(mBluetoothDevice.toString()).thenReturn(STRING);
+        assertThat(mTestabilityBluetoothDevice.toString()).isSameInstanceAs(STRING);
+    }
+
+    private static class TestBluetoothGattCallback extends BluetoothGattCallback {}
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java
new file mode 100644
index 0000000..26ae6d7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link BluetoothGattCallback}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattCallbackTest {
+    @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
+    @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+    @Mock private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
+
+    TestBluetoothGattCallback mTestBluetoothGattCallback = new TestBluetoothGattCallback();
+
+    @Test
+    public void testOnConnectionStateChange_notCrash() {
+        mTestBluetoothGattCallback.unwrap()
+                .onConnectionStateChange(mBluetoothGatt, 1, 1);
+    }
+
+    @Test
+    public void testOnServiceDiscovered_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onServicesDiscovered(mBluetoothGatt, 1);
+    }
+
+    @Test
+    public void testOnCharacteristicRead_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onCharacteristicRead(mBluetoothGatt,
+                mBluetoothGattCharacteristic, 1);
+    }
+
+    @Test
+    public void testOnCharacteristicWrite_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onCharacteristicWrite(mBluetoothGatt,
+                mBluetoothGattCharacteristic, 1);
+    }
+
+    @Test
+    public void testOnDescriptionRead_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onDescriptorRead(mBluetoothGatt,
+                mBluetoothGattDescriptor, 1);
+    }
+
+    @Test
+    public void testOnDescriptionWrite_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onDescriptorWrite(mBluetoothGatt,
+                mBluetoothGattDescriptor, 1);
+    }
+
+    @Test
+    public void testOnReadRemoteRssi_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onReadRemoteRssi(mBluetoothGatt, 1, 1);
+    }
+
+    @Test
+    public void testOnReliableWriteCompleted_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onReliableWriteCompleted(mBluetoothGatt, 1);
+    }
+
+    @Test
+    public void testOnMtuChanged_notCrash() {
+        mTestBluetoothGattCallback.unwrap().onMtuChanged(mBluetoothGatt, 1, 1);
+    }
+
+    @Test
+    public void testOnCharacteristicChanged_notCrash() {
+        mTestBluetoothGattCallback.unwrap()
+                .onCharacteristicChanged(mBluetoothGatt, mBluetoothGattCharacteristic);
+    }
+
+    private static class TestBluetoothGattCallback extends BluetoothGattCallback { }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java
new file mode 100644
index 0000000..fb99317
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link BluetoothGattServerCallback}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattServerCallbackTest {
+    @Mock
+    private android.bluetooth.BluetoothDevice mBluetoothDevice;
+    @Mock
+    private android.bluetooth.BluetoothGattService mBluetoothGattService;
+    @Mock
+    private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+    @Mock
+    private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
+
+    TestBluetoothGattServerCallback
+            mTestBluetoothGattServerCallback = new TestBluetoothGattServerCallback();
+
+    @Test
+    public void testOnCharacteristicReadRequest_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onCharacteristicReadRequest(
+                mBluetoothDevice, 1, 1, mBluetoothGattCharacteristic);
+    }
+
+    @Test
+    public void testOnCharacteristicWriteRequest_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onCharacteristicWriteRequest(
+                mBluetoothDevice,
+                1,
+                mBluetoothGattCharacteristic,
+                false,
+                true,
+                1,
+                new byte[]{1});
+    }
+
+    @Test
+    public void testOnConnectionStateChange_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onConnectionStateChange(
+                mBluetoothDevice,
+                1,
+                2);
+    }
+
+    @Test
+    public void testOnDescriptorReadRequest_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onDescriptorReadRequest(
+                mBluetoothDevice,
+                1,
+                2, mBluetoothGattDescriptor);
+    }
+
+    @Test
+    public void testOnDescriptorWriteRequest_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onDescriptorWriteRequest(
+                mBluetoothDevice,
+                1,
+                mBluetoothGattDescriptor,
+                false,
+                true,
+                2,
+                new byte[]{1});
+    }
+
+    @Test
+    public void testOnExecuteWrite_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onExecuteWrite(
+                mBluetoothDevice,
+                1,
+                false);
+    }
+
+    @Test
+    public void testOnMtuChanged_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onMtuChanged(
+                mBluetoothDevice,
+                1);
+    }
+
+    @Test
+    public void testOnNotificationSent_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onNotificationSent(
+                mBluetoothDevice,
+                1);
+    }
+
+    @Test
+    public void testOnServiceAdded_notCrash() {
+        mTestBluetoothGattServerCallback.unwrap().onServiceAdded(1, mBluetoothGattService);
+    }
+
+    private static class TestBluetoothGattServerCallback extends BluetoothGattServerCallback { }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java
new file mode 100644
index 0000000..48283d1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothGattServer}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattServerTest {
+    private static final UUID UUID_CONST = UUID.randomUUID();
+    private static final byte[] BYTES = new byte[]{1, 2, 3};
+
+    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+    @Mock private android.bluetooth.BluetoothGattServer mBluetoothGattServer;
+    @Mock private android.bluetooth.BluetoothGattService mBluetoothGattService;
+    @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+
+    BluetoothGattServer mTestabilityBluetoothGattServer;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mTestabilityBluetoothGattServer = BluetoothGattServer.wrap(mBluetoothGattServer);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+        assertThat(mTestabilityBluetoothGattServer).isNotNull();
+        assertThat(mTestabilityBluetoothGattServer.unwrap()).isSameInstanceAs(mBluetoothGattServer);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConnect_callsWrapped() {
+        when(mBluetoothGattServer
+                .connect(mBluetoothDevice, true))
+                .thenReturn(true);
+        assertThat(mTestabilityBluetoothGattServer
+                .connect(BluetoothDevice.wrap(mBluetoothDevice), true))
+                .isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testAddService_callsWrapped() {
+        when(mBluetoothGattServer
+                .addService(mBluetoothGattService))
+                .thenReturn(true);
+        assertThat(mTestabilityBluetoothGattServer
+                .addService(mBluetoothGattService))
+                .isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testClearServices_callsWrapped() {
+        doNothing().when(mBluetoothGattServer).clearServices();
+        mTestabilityBluetoothGattServer.clearServices();
+        verify(mBluetoothGattServer).clearServices();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testClose_callsWrapped() {
+        doNothing().when(mBluetoothGattServer).close();
+        mTestabilityBluetoothGattServer.close();
+        verify(mBluetoothGattServer).close();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testNotifyCharacteristicChanged_callsWrapped() {
+        when(mBluetoothGattServer
+                .notifyCharacteristicChanged(
+                        mBluetoothDevice,
+                        mBluetoothGattCharacteristic,
+                        true))
+                .thenReturn(true);
+        assertThat(mTestabilityBluetoothGattServer
+                .notifyCharacteristicChanged(
+                        BluetoothDevice.wrap(mBluetoothDevice),
+                        mBluetoothGattCharacteristic,
+                        true))
+                .isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSendResponse_callsWrapped() {
+        when(mBluetoothGattServer.sendResponse(
+                mBluetoothDevice, 1, 1, 1, BYTES)).thenReturn(true);
+        mTestabilityBluetoothGattServer.sendResponse(
+                BluetoothDevice.wrap(mBluetoothDevice), 1, 1, 1, BYTES);
+        verify(mBluetoothGattServer).sendResponse(
+                mBluetoothDevice, 1, 1, 1, BYTES);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testCancelConnection_callsWrapped() {
+        doNothing().when(mBluetoothGattServer).cancelConnection(mBluetoothDevice);
+        mTestabilityBluetoothGattServer.cancelConnection(BluetoothDevice.wrap(mBluetoothDevice));
+        verify(mBluetoothGattServer).cancelConnection(mBluetoothDevice);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetService_callsWrapped() {
+        when(mBluetoothGattServer.getService(UUID_CONST)).thenReturn(null);
+        assertThat(mTestabilityBluetoothGattServer.getService(UUID_CONST)).isNull();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java
new file mode 100644
index 0000000..199146d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothGattWrapper}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattWrapperTest {
+    private static final UUID UUID_CONST = UUID.randomUUID();
+    private static final byte[] BYTES = new byte[]{1, 2, 3};
+
+    @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+    @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
+    @Mock private android.bluetooth.BluetoothGattService mBluetoothGattService;
+    @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+    @Mock private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
+
+    BluetoothGattWrapper mBluetoothGattWrapper;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBluetoothGattWrapper = BluetoothGattWrapper.wrap(mBluetoothGatt);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+        assertThat(mBluetoothGattWrapper).isNotNull();
+        assertThat(mBluetoothGattWrapper.unwrap()).isSameInstanceAs(mBluetoothGatt);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testEquality_asExpected() {
+        assertThat(mBluetoothGattWrapper.equals(null)).isFalse();
+        assertThat(mBluetoothGattWrapper.equals(mBluetoothGattWrapper)).isTrue();
+        assertThat(mBluetoothGattWrapper.equals(BluetoothGattWrapper.wrap(mBluetoothGatt)))
+                .isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetDevice_callsWrapped() {
+        when(mBluetoothGatt.getDevice()).thenReturn(mBluetoothDevice);
+        assertThat(mBluetoothGattWrapper.getDevice().unwrap()).isSameInstanceAs(mBluetoothDevice);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetServices_callsWrapped() {
+        when(mBluetoothGatt.getServices()).thenReturn(null);
+        assertThat(mBluetoothGattWrapper.getServices()).isNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testGetService_callsWrapped() {
+        when(mBluetoothGatt.getService(UUID_CONST)).thenReturn(mBluetoothGattService);
+        assertThat(mBluetoothGattWrapper.getService(UUID_CONST))
+                .isSameInstanceAs(mBluetoothGattService);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testDiscoverServices_callsWrapped() {
+        when(mBluetoothGatt.discoverServices()).thenReturn(true);
+        assertThat(mBluetoothGattWrapper.discoverServices()).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testReadCharacteristic_callsWrapped() {
+        when(mBluetoothGatt.readCharacteristic(mBluetoothGattCharacteristic)).thenReturn(true);
+        assertThat(mBluetoothGattWrapper.readCharacteristic(mBluetoothGattCharacteristic)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWriteCharacteristic_callsWrapped() {
+        when(mBluetoothGatt.writeCharacteristic(mBluetoothGattCharacteristic, BYTES, 1))
+                .thenReturn(1);
+        assertThat(mBluetoothGattWrapper.writeCharacteristic(
+                mBluetoothGattCharacteristic, BYTES, 1)).isEqualTo(1);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testReadDescriptor_callsWrapped() {
+        when(mBluetoothGatt.readDescriptor(mBluetoothGattDescriptor)).thenReturn(false);
+        assertThat(mBluetoothGattWrapper.readDescriptor(mBluetoothGattDescriptor)).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWriteDescriptor_callsWrapped() {
+        when(mBluetoothGatt.writeDescriptor(mBluetoothGattDescriptor, BYTES)).thenReturn(5);
+        assertThat(mBluetoothGattWrapper.writeDescriptor(mBluetoothGattDescriptor, BYTES))
+                .isEqualTo(5);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testReadRemoteRssi_callsWrapped() {
+        when(mBluetoothGatt.readRemoteRssi()).thenReturn(false);
+        assertThat(mBluetoothGattWrapper.readRemoteRssi()).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testRequestConnectionPriority_callsWrapped() {
+        when(mBluetoothGatt.requestConnectionPriority(5)).thenReturn(false);
+        assertThat(mBluetoothGattWrapper.requestConnectionPriority(5)).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testRequestMtu_callsWrapped() {
+        when(mBluetoothGatt.requestMtu(5)).thenReturn(false);
+        assertThat(mBluetoothGattWrapper.requestMtu(5)).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testSetCharacteristicNotification_callsWrapped() {
+        when(mBluetoothGatt.setCharacteristicNotification(mBluetoothGattCharacteristic, true))
+                .thenReturn(false);
+        assertThat(mBluetoothGattWrapper
+                .setCharacteristicNotification(mBluetoothGattCharacteristic, true)).isFalse();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testDisconnect_callsWrapped() {
+        doNothing().when(mBluetoothGatt).disconnect();
+        mBluetoothGattWrapper.disconnect();
+        verify(mBluetoothGatt).disconnect();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testClose_callsWrapped() {
+        doNothing().when(mBluetoothGatt).close();
+        mBluetoothGattWrapper.close();
+        verify(mBluetoothGatt).close();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java
new file mode 100644
index 0000000..8468ed1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link BluetoothLeAdvertiser}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAdvertiserTest {
+    @Mock android.bluetooth.le.BluetoothLeAdvertiser mWrappedBluetoothLeAdvertiser;
+    @Mock AdvertiseSettings mAdvertiseSettings;
+    @Mock AdvertiseData mAdvertiseData;
+    @Mock AdvertiseCallback mAdvertiseCallback;
+
+    BluetoothLeAdvertiser mBluetoothLeAdvertiser;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBluetoothLeAdvertiser = BluetoothLeAdvertiser.wrap(mWrappedBluetoothLeAdvertiser);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNullAdapter_isNull() {
+        assertThat(BluetoothLeAdvertiser.wrap(null)).isNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+        assertThat(mWrappedBluetoothLeAdvertiser).isNotNull();
+        assertThat(mBluetoothLeAdvertiser.unwrap()).isSameInstanceAs(mWrappedBluetoothLeAdvertiser);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStartAdvertisingThreeParameters_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeAdvertiser)
+                .startAdvertising(mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
+        mBluetoothLeAdvertiser
+                .startAdvertising(mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
+        verify(mWrappedBluetoothLeAdvertiser).startAdvertising(
+                mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStartAdvertisingFourParameters_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeAdvertiser).startAdvertising(
+                mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
+        mBluetoothLeAdvertiser.startAdvertising(
+                mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
+        verify(mWrappedBluetoothLeAdvertiser).startAdvertising(
+                mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStopAdvertising_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeAdvertiser).stopAdvertising(mAdvertiseCallback);
+        mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
+        verify(mWrappedBluetoothLeAdvertiser).stopAdvertising(mAdvertiseCallback);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java
new file mode 100644
index 0000000..3fce54f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link BluetoothLeScanner}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothLeScannerTest {
+    @Mock android.bluetooth.le.BluetoothLeScanner mWrappedBluetoothLeScanner;
+    @Mock PendingIntent mPendingIntent;
+    @Mock ScanSettings mScanSettings;
+    @Mock ScanFilter mScanFilter;
+
+    TestScanCallback mTestScanCallback = new TestScanCallback();
+    BluetoothLeScanner mBluetoothLeScanner;
+    ImmutableList<ScanFilter> mImmutableScanFilterList;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mBluetoothLeScanner = BluetoothLeScanner.wrap(mWrappedBluetoothLeScanner);
+        mImmutableScanFilterList = ImmutableList.of(mScanFilter);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNullAdapter_isNull() {
+        assertThat(BluetoothLeAdvertiser.wrap(null)).isNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+        assertThat(mWrappedBluetoothLeScanner).isNotNull();
+        assertThat(mBluetoothLeScanner.unwrap()).isSameInstanceAs(mWrappedBluetoothLeScanner);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStartScan_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeScanner).startScan(mTestScanCallback.unwrap());
+        mBluetoothLeScanner.startScan(mTestScanCallback);
+        verify(mWrappedBluetoothLeScanner).startScan(mTestScanCallback.unwrap());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStartScanWithFiltersCallback_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeScanner)
+                .startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback.unwrap());
+        mBluetoothLeScanner.startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback);
+        verify(mWrappedBluetoothLeScanner)
+                .startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback.unwrap());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStartScanWithFiltersCallbackIntent_callsWrapped() {
+        when(mWrappedBluetoothLeScanner.startScan(
+                mImmutableScanFilterList, mScanSettings, mPendingIntent)).thenReturn(1);
+        mBluetoothLeScanner.startScan(mImmutableScanFilterList, mScanSettings, mPendingIntent);
+        verify(mWrappedBluetoothLeScanner)
+                .startScan(mImmutableScanFilterList, mScanSettings, mPendingIntent);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStopScan_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeScanner).stopScan(mTestScanCallback.unwrap());
+        mBluetoothLeScanner.stopScan(mTestScanCallback);
+        verify(mWrappedBluetoothLeScanner).stopScan(mTestScanCallback.unwrap());
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testStopScanPendingIntent_callsWrapped() {
+        doNothing().when(mWrappedBluetoothLeScanner).stopScan(mPendingIntent);
+        mBluetoothLeScanner.stopScan(mPendingIntent);
+        verify(mWrappedBluetoothLeScanner).stopScan(mPendingIntent);
+    }
+
+    private static class TestScanCallback extends ScanCallback {};
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java
new file mode 100644
index 0000000..6d68486
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link ScanCallback}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScanCallbackTest {
+    @Mock android.bluetooth.le.ScanResult mScanResult;
+
+    TestScanCallback mTestScanCallback = new TestScanCallback();
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    @Test
+    public void testOnScanFailed_notCrash() {
+        mTestScanCallback.unwrap().onScanFailed(1);
+    }
+
+    @Test
+    public void testOnScanResult_notCrash() {
+        mTestScanCallback.unwrap().onScanResult(1, mScanResult);
+    }
+
+    @Test
+    public void testOnBatchScanResult_notCrash() {
+        mTestScanCallback.unwrap().onBatchScanResults(ImmutableList.of(mScanResult));
+    }
+
+    private static class TestScanCallback extends ScanCallback { }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java
new file mode 100644
index 0000000..255c178
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.bluetooth.testability.android.bluetooth.le;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link ScanResult}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScanResultTest {
+
+    @Mock android.bluetooth.le.ScanResult mWrappedScanResult;
+    @Mock android.bluetooth.le.ScanRecord mScanRecord;
+    @Mock android.bluetooth.BluetoothDevice mBluetoothDevice;
+    ScanResult mScanResult;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mScanResult = ScanResult.wrap(mWrappedScanResult);
+    }
+
+    @Test
+    public void testGetScanRecord_calledWrapped() {
+        when(mWrappedScanResult.getScanRecord()).thenReturn(mScanRecord);
+        assertThat(mScanResult.getScanRecord()).isSameInstanceAs(mScanRecord);
+    }
+
+    @Test
+    public void testGetRssi_calledWrapped() {
+        when(mWrappedScanResult.getRssi()).thenReturn(3);
+        assertThat(mScanResult.getRssi()).isEqualTo(3);
+    }
+
+    @Test
+    public void testGetTimestampNanos_calledWrapped() {
+        when(mWrappedScanResult.getTimestampNanos()).thenReturn(4L);
+        assertThat(mScanResult.getTimestampNanos()).isEqualTo(4L);
+    }
+
+    @Test
+    public void testGetDevice_calledWrapped() {
+        when(mWrappedScanResult.getDevice()).thenReturn(mBluetoothDevice);
+        assertThat(mScanResult.getDevice().unwrap()).isSameInstanceAs(mBluetoothDevice);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
index 70dcec8..ae8258e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
@@ -36,7 +36,6 @@
     @Rule
     public ExpectedException thrown = ExpectedException.none();
 
-    /*
     @Test
     public void remove() {
         mEventLoop.postRunnable(new NumberedRunnable(0));
@@ -44,10 +43,8 @@
         mEventLoop.postRunnable(runnableToAddAndRemove);
         mEventLoop.removeRunnable(runnableToAddAndRemove);
         mEventLoop.postRunnable(new NumberedRunnable(2));
-
-        assertThat(mExecutedRunnables).containsExactly(0, 2);
+        assertThat(mExecutedRunnables).doesNotContain(1);
     }
-    */
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
@@ -88,4 +85,10 @@
             mExecutedRunnables.add(mId);
         }
     }
+
+    @Test
+    public void postEmptyQueueRunnable() {
+        mEventLoop.postEmptyQueueRunnable(new NumberedRunnable(0));
+        assertThat(mExecutedRunnables).isEmpty();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java
new file mode 100644
index 0000000..c352184
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.eventloop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HandlerEventLoopImplTest {
+    private static final String TAG = "HandlerEventLoopImplTest";
+    private final HandlerEventLoopImpl mHandlerEventLoopImpl =
+            new HandlerEventLoopImpl(TAG);
+    private final List<Integer> mExecutedRunnables = new ArrayList<>();
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void remove() {
+        mHandlerEventLoopImpl.postRunnable(new NumberedRunnable(0));
+        NumberedRunnable runnableToAddAndRemove = new NumberedRunnable(1);
+        mHandlerEventLoopImpl.postRunnable(runnableToAddAndRemove);
+        mHandlerEventLoopImpl.removeRunnable(runnableToAddAndRemove);
+        mHandlerEventLoopImpl.postRunnable(new NumberedRunnable(2));
+        assertThat(mExecutedRunnables).doesNotContain(1);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void isPosted() {
+        NumberedRunnable runnable = new HandlerEventLoopImplTest.NumberedRunnable(0);
+        mHandlerEventLoopImpl.postRunnableDelayed(runnable, 10 * 1000L);
+        assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isTrue();
+        mHandlerEventLoopImpl.removeRunnable(runnable);
+        assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isFalse();
+
+        // Let a runnable execute, then verify that it's not posted.
+        mHandlerEventLoopImpl.postRunnable(runnable);
+        assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isTrue();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void postAndWaitAfterDestroy() throws InterruptedException {
+        mHandlerEventLoopImpl.destroy();
+        mHandlerEventLoopImpl.postAndWait(new HandlerEventLoopImplTest.NumberedRunnable(0));
+        assertThat(mExecutedRunnables).isEmpty();
+    }
+
+
+    private class NumberedRunnable extends NamedRunnable {
+        private final int mId;
+
+        private NumberedRunnable(int id) {
+            super("NumberedRunnable:" + id);
+            this.mId = id;
+        }
+
+        @Override
+        public void run() {
+            // Note: when running in robolectric, this is not actually executed on a different
+            // thread, it's executed in the same thread the test runs in, so this is safe.
+            mExecutedRunnables.add(mId);
+        }
+    }
+
+    @Test
+    public void postEmptyQueueRunnable() {
+        mHandlerEventLoopImpl.postEmptyQueueRunnable(
+                new HandlerEventLoopImplTest.NumberedRunnable(0));
+        assertThat(mExecutedRunnables).isEmpty();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java
new file mode 100644
index 0000000..7005da1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.eventloop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class NamedRunnableTest {
+    private static final String TAG = "NamedRunnableTest";
+
+    @Test
+    public void testToString() {
+        assertThat(mNamedRunnable.toString()).isEqualTo("Runnable[" + TAG +  "]");
+    }
+
+    private final NamedRunnable mNamedRunnable = new NamedRunnable(TAG) {
+        @Override
+        public void run() {
+        }
+    };
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
new file mode 100644
index 0000000..d39d9cc
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import org.junit.Test;
+import org.mockito.Mock;
+
+public class IconUtilsTest {
+    private static final int MIN_ICON_SIZE = 16;
+    private static final int DESIRED_ICON_SIZE = 32;
+    @Mock
+    Context mContext;
+
+    @Test
+    public void isIconSizedCorrectly() {
+        // Null bitmap is not sized correctly
+        assertThat(IconUtils.isIconSizeCorrect(null)).isFalse();
+
+        int minIconSize = MIN_ICON_SIZE;
+        int desiredIconSize = DESIRED_ICON_SIZE;
+
+        // Bitmap that is 1x1 pixels is not sized correctly
+        Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
+        assertThat(IconUtils.isIconSizeCorrect(icon)).isFalse();
+
+        // Bitmap is categorized as small, and not regular
+        icon = Bitmap.createBitmap(minIconSize + 1,
+                minIconSize + 1, Bitmap.Config.ALPHA_8);
+        assertThat(IconUtils.isIconSizeCorrect(icon)).isTrue();
+        assertThat(IconUtils.isIconSizedSmall(icon)).isTrue();
+        assertThat(IconUtils.isIconSizedRegular(icon)).isFalse();
+
+        // Bitmap is categorized as regular, but not small
+        icon = Bitmap.createBitmap(desiredIconSize + 1,
+                desiredIconSize + 1, Bitmap.Config.ALPHA_8);
+        assertThat(IconUtils.isIconSizeCorrect(icon)).isTrue();
+        assertThat(IconUtils.isIconSizedSmall(icon)).isFalse();
+        assertThat(IconUtils.isIconSizedRegular(icon)).isTrue();
+    }
+
+    @Test
+    public void testAddWhiteCircleBackground() {
+        int minIconSize = MIN_ICON_SIZE;
+        Bitmap icon = Bitmap.createBitmap(minIconSize + 1, minIconSize + 1,
+                Bitmap.Config.ALPHA_8);
+
+        assertThat(
+                IconUtils.isIconSizeCorrect(IconUtils.addWhiteCircleBackground(mContext, icon)))
+                .isTrue();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java
new file mode 100644
index 0000000..ff7f6f9
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.nearby.common.locator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.fastpair.FastPairAdvHandler;
+import com.android.server.nearby.fastpair.FastPairModule;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+public class LocatorTest {
+    private Locator mLocator;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mLocator = src.com.android.server.nearby.fastpair.testing.MockingLocator.withMocksOnly(
+                ApplicationProvider.getApplicationContext());
+        mLocator.bind(new FastPairModule());
+    }
+
+    @Test
+    public void genericConstructor() {
+        assertThat(mLocator.get(FastPairCacheManager.class)).isNotNull();
+        assertThat(mLocator.get(FootprintsDeviceManager.class)).isNotNull();
+        assertThat(mLocator.get(EventLoop.class)).isNotNull();
+        assertThat(mLocator.get(FastPairHalfSheetManager.class)).isNotNull();
+        assertThat(mLocator.get(FastPairAdvHandler.class)).isNotNull();
+        assertThat(mLocator.get(Clock.class)).isNotNull();
+    }
+
+    @Test
+    public void genericDestroy() {
+        mLocator.destroy();
+    }
+
+    @Test
+    public void getOptional() {
+        assertThat(mLocator.getOptional(FastPairModule.class)).isNotNull();
+        mLocator.removeBindingForTest(FastPairModule.class);
+        assertThat(mLocator.getOptional(FastPairModule.class)).isNull();
+    }
+
+    @Test
+    public void getParent() {
+        assertThat(mLocator.getParent()).isNotNull();
+    }
+
+    @Test
+    public void getUnboundErrorMessage() {
+        assertThat(mLocator.getUnboundErrorMessage(FastPairModule.class))
+                .isEqualTo(
+                        "Unbound type: com.android.server.nearby.fastpair.FastPairModule\n"
+                        + "Searched locators:\n" + "android.app.Application ->\n"
+                                + "android.app.Application ->\n" + "android.app.Application");
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
index 346a961..01f2f46 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
@@ -16,24 +16,33 @@
 
 package com.android.server.nearby.fastpair;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.accounts.Account;
 import android.content.Context;
 import android.nearby.FastPairDevice;
 
+import com.android.server.nearby.common.bloomfilter.BloomFilter;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
 import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import service.proto.Cache;
+import service.proto.Data;
 import service.proto.Rpcs;
 
 public class FastPairAdvHandlerTest {
@@ -45,11 +54,22 @@
     private FastPairHalfSheetManager mFastPairHalfSheetManager;
     @Mock
     private FastPairNotificationManager mFastPairNotificationManager;
+    @Mock
+    private BloomFilter mBloomFilter;
+    @Mock
+    Cache.StoredDiscoveryItem mStoredDiscoveryItem;
+    @Mock
+    Cache.StoredFastPairItem mStoredFastPairItem;
+    @Mock
+    Data.FastPairDeviceWithAccountKey mFastPairDeviceWithAccountKey;
+    private static final byte[] ACCOUNT_KEY = new byte[] {0, 1, 2};
     private static final String BLUETOOTH_ADDRESS = "AA:BB:CC:DD";
     private static final int CLOSE_RSSI = -80;
     private static final int FAR_AWAY_RSSI = -120;
     private static final int TX_POWER = -70;
     private static final byte[] INITIAL_BYTE_ARRAY = new byte[]{0x01, 0x02, 0x03};
+    private static final byte[] SALT = new byte[]{0x01};
+    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
 
     LocatorContextWrapper mLocatorContextWrapper;
     FastPairAdvHandler mFastPairAdvHandler;
@@ -99,7 +119,7 @@
     }
 
     @Test
-    public void testSubsequentBroadcast() {
+    public void testSubsequentBroadcast_notShowHalfSheet() {
         byte[] fastPairRecordWithBloomFilter =
                 new byte[]{
                         (byte) 0x02,
@@ -132,4 +152,44 @@
 
         verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
     }
+
+    @Test
+    public void testFindRecognizedDevice_bloomFilterNotContains_notFound() {
+        when(mFastPairDeviceWithAccountKey.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
+
+        assertThat(FastPairAdvHandler.findRecognizedDevice(
+                ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNull();
+    }
+
+    @Test
+    public void testFindRecognizedDevice_bloomFilterContains_found() {
+        when(mFastPairDeviceWithAccountKey.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
+
+        assertThat(FastPairAdvHandler.findRecognizedDevice(
+                ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNotNull();
+    }
+
+    @Test
+    public void testFindRecognizedDeviceFromCachedItem_bloomFilterNotContains_notFound() {
+        when(mStoredFastPairItem.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
+
+        assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
+                ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNull();
+    }
+
+    @Test
+    public void testFindRecognizedDeviceFromCachedItem_bloomFilterContains_found() {
+        when(mStoredFastPairItem.getAccountKey())
+                .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+        when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
+
+        assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
+                ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNotNull();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
new file mode 100644
index 0000000..9cf65f4
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 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.nearby.fastpair;
+
+import org.junit.Test;
+
+public class FlagUtilsTest {
+
+    @Test
+    public void testGetPreferencesBuilder_notCrash() {
+        FlagUtils.getPreferencesBuilder().build();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
new file mode 100644
index 0000000..5d4ea22
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2022 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.nearby.fastpair.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.FastPairManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Cache;
+
+/** Unit tests for {@link DiscoveryItem} */
+public class DiscoveryItemTest {
+    private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
+    private static final String DEFAULT_DESCRIPITON = "description";
+    private static final long DEFAULT_TIMESTAMP = 1000000000L;
+    private static final String DEFAULT_TITLE = "title";
+    private static final String APP_NAME = "app_name";
+    private static final String ACTION_URL =
+            "intent:#Intent;action=com.android.server.nearby:ACTION_FAST_PAIR;"
+            + "package=com.google.android.gms;"
+            + "component=com.google.android.gms/"
+            + ".nearby.discovery.service.DiscoveryService;end";
+    private static final String DISPLAY_URL = "DISPLAY_URL";
+    private static final String TRIGGER_ID = "trigger.id";
+    private static final String FAST_PAIR_ID = "id";
+    private static final int RSSI = -80;
+    private static final int TX_POWER = -10;
+
+    @Mock private Context mContext;
+    private LocatorContextWrapper mLocatorContextWrapper;
+    private FastPairCacheManager mFastPairCacheManager;
+    private FastPairManager mFastPairManager;
+    private DiscoveryItem mDiscoveryItem;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+        mFastPairManager = new FastPairManager(mLocatorContextWrapper);
+        mFastPairCacheManager = mLocatorContextWrapper.getLocator().get(FastPairCacheManager.class);
+        when(mContext.getContentResolver()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+        mDiscoveryItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+    }
+
+    @Test
+    public void testMultipleFields() {
+        assertThat(mDiscoveryItem.getId()).isEqualTo(FAST_PAIR_ID);
+        assertThat(mDiscoveryItem.getDescription()).isEqualTo(DEFAULT_DESCRIPITON);
+        assertThat(mDiscoveryItem.getDisplayUrl()).isEqualTo(DISPLAY_URL);
+        assertThat(mDiscoveryItem.getTriggerId()).isEqualTo(TRIGGER_ID);
+        assertThat(mDiscoveryItem.getMacAddress()).isEqualTo(DEFAULT_MAC_ADDRESS);
+        assertThat(
+                mDiscoveryItem.getFirstObservationTimestampMillis()).isEqualTo(DEFAULT_TIMESTAMP);
+        assertThat(
+                mDiscoveryItem.getLastObservationTimestampMillis()).isEqualTo(DEFAULT_TIMESTAMP);
+        assertThat(mDiscoveryItem.getActionUrl()).isEqualTo(ACTION_URL);
+        assertThat(mDiscoveryItem.getAppName()).isEqualTo(APP_NAME);
+        assertThat(mDiscoveryItem.getRssi()).isEqualTo(RSSI);
+        assertThat(mDiscoveryItem.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(mDiscoveryItem.getFastPairInformation()).isNull();
+        assertThat(mDiscoveryItem.getFastPairSecretKey()).isNull();
+        assertThat(mDiscoveryItem.getIcon()).isNull();
+        assertThat(mDiscoveryItem.getIconFifeUrl()).isNotNull();
+        assertThat(mDiscoveryItem.getState()).isNotNull();
+        assertThat(mDiscoveryItem.getTitle()).isNotNull();
+        assertThat(mDiscoveryItem.isApp()).isFalse();
+        assertThat(mDiscoveryItem.isDeletable(
+                100000L, 0L)).isTrue();
+        assertThat(mDiscoveryItem.isDeviceType(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
+        assertThat(mDiscoveryItem.isExpired(
+                100000L, 0L)).isTrue();
+        assertThat(mDiscoveryItem.isFastPair()).isTrue();
+        assertThat(mDiscoveryItem.isPendingAppInstallValid(5)).isTrue();
+        assertThat(mDiscoveryItem.isPendingAppInstallValid(5,
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  null,
+                TRIGGER_ID,  DEFAULT_MAC_ADDRESS,  "", RSSI, TX_POWER))).isTrue();
+        assertThat(mDiscoveryItem.isTypeEnabled(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
+        assertThat(mDiscoveryItem.toString()).isNotNull();
+    }
+
+    @Test
+    public void isMuted() {
+        assertThat(mDiscoveryItem.isMuted()).isFalse();
+    }
+
+    @Test
+    public void itemWithDefaultDescription_shouldShowUp() {
+        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+
+        // Null description should not show up.
+        mDiscoveryItem.setStoredItemForTest(DiscoveryItem.newStoredDiscoveryItem());
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  null,
+                        TRIGGER_ID,  DEFAULT_MAC_ADDRESS,  "", RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+
+        // Empty description should not show up.
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID,  "",
+                        TRIGGER_ID,  DEFAULT_MAC_ADDRESS, DEFAULT_TITLE, RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+    }
+
+    @Test
+    public void itemWithEmptyTitle_shouldNotShowUp() {
+        // Null title should not show up.
+        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+        // Empty title should not show up.
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+
+        // Null title should not show up.
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, DEFAULT_MAC_ADDRESS, null, RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+    }
+
+    @Test
+    public void itemWithRssiAndTxPower_shouldHaveCorrectEstimatedDistance() {
+        assertThat(mDiscoveryItem.getEstimatedDistance()).isWithin(0.01).of(28.18);
+    }
+
+    @Test
+    public void itemWithoutRssiOrTxPower_shouldNotHaveEstimatedDistance() {
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", 0, 0));
+        assertThat(mDiscoveryItem.getEstimatedDistance()).isWithin(0.01).of(0);
+    }
+
+    @Test
+    public void getUiHashCode_differentAddress_differentHash() {
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+        DiscoveryItem compareTo =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        compareTo.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, "55:44:33:22:11:00", "", RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.getUiHashCode()).isNotEqualTo(compareTo.getUiHashCode());
+    }
+
+    @Test
+    public void getUiHashCode_sameAddress_sameHash() {
+        mDiscoveryItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+        DiscoveryItem compareTo =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        compareTo.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.getUiHashCode()).isEqualTo(compareTo.getUiHashCode());
+    }
+
+    @Test
+    public void isFastPair() {
+        DiscoveryItem fastPairItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        assertThat(fastPairItem.isFastPair()).isTrue();
+    }
+
+    @Test
+    public void testEqual() {
+        DiscoveryItem fastPairItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
+    }
+
+    @Test
+    public void testCompareTo() {
+        DiscoveryItem fastPairItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        assertThat(mDiscoveryItem.compareTo(fastPairItem)).isEqualTo(0);
+    }
+
+
+    @Test
+    public void testCopyOfStoredItem() {
+        DiscoveryItem fastPairItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        fastPairItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.equals(fastPairItem)).isFalse();
+        fastPairItem.setStoredItemForTest(mDiscoveryItem.getCopyOfStoredItem());
+        assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
+    }
+
+    @Test
+    public void testStoredItemForTest() {
+        DiscoveryItem fastPairItem =
+                FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+        fastPairItem.setStoredItemForTest(
+                FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+                        TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+        assertThat(mDiscoveryItem.equals(fastPairItem)).isFalse();
+        fastPairItem.setStoredItemForTest(mDiscoveryItem.getStoredItemForTest());
+        assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
index adae97d..0f6fb19 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
@@ -20,6 +20,7 @@
 
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.le.ScanResult;
 import android.content.Context;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -43,6 +44,7 @@
     private static final ByteString ACCOUNT_KEY = ByteString.copyFromUtf8("axgs");
     private static final String MAC_ADDRESS_B = "00:11:22:44";
     private static final ByteString ACCOUNT_KEY_B = ByteString.copyFromUtf8("axgb");
+    private static final String ITEM_ID = "ITEM_ID";
 
     @Mock
     DiscoveryItem mDiscoveryItem;
@@ -50,6 +52,10 @@
     DiscoveryItem mDiscoveryItem2;
     @Mock
     Cache.StoredFastPairItem mStoredFastPairItem;
+    @Mock
+    ScanResult mScanResult;
+
+    Context mContext;
     Cache.StoredDiscoveryItem mStoredDiscoveryItem = Cache.StoredDiscoveryItem.newBuilder()
             .setTriggerId(MODEL_ID)
             .setAppName(APP_NAME).build();
@@ -60,12 +66,12 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mContext = ApplicationProvider.getApplicationContext();
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void notSaveRetrieveInfo() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
         when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
 
@@ -78,7 +84,6 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void saveRetrieveInfo() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
         when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
 
@@ -91,7 +96,6 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void getAllInfo() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
         when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
         when(mDiscoveryItem2.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem2);
@@ -105,12 +109,13 @@
         fastPairCacheManager.saveDiscoveryItem(mDiscoveryItem2);
 
         assertThat(fastPairCacheManager.getAllSavedStoreDiscoveryItem()).hasSize(3);
+
+        fastPairCacheManager.cleanUp();
     }
 
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void saveRetrieveInfoStoredFastPairItem() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
                 .setMacAddress(MAC_ADDRESS)
                 .setAccountKey(ACCOUNT_KEY)
@@ -128,7 +133,6 @@
     @Test
     @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void checkGetAllFastPairItems() {
-        Context mContext = ApplicationProvider.getApplicationContext();
         Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
                 .setMacAddress(MAC_ADDRESS)
                 .setAccountKey(ACCOUNT_KEY)
@@ -149,5 +153,15 @@
 
         assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
                 .isEqualTo(1);
+
+        fastPairCacheManager.cleanUp();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void getDeviceFromScanResult_notCrash() {
+        FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+        fastPairCacheManager.getDeviceFromScanResult(mScanResult);
+
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
new file mode 100644
index 0000000..c5428f5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2021 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.nearby.fastpair.cache;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteException;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairDbHelperTest {
+
+    Context mContext;
+    FastPairDbHelper mFastPairDbHelper;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mContext = InstrumentationRegistry.getInstrumentation().getContext();
+        mFastPairDbHelper = new FastPairDbHelper(mContext);
+    }
+
+    @After
+    public void teardown() {
+        mFastPairDbHelper.close();
+    }
+
+    @Test
+    public void testUpgrade_notCrash() {
+        mFastPairDbHelper
+                .onUpgrade(mFastPairDbHelper.getWritableDatabase(), 1, 2);
+    }
+
+    @Test
+    public void testDowngrade_throwsException()  {
+        assertThrows(
+                SQLiteException.class,
+                () -> mFastPairDbHelper.onDowngrade(
+                        mFastPairDbHelper.getWritableDatabase(), 2, 1));
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
index 58e4c47..b51a295 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.fastpair.halfsheet;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +27,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
@@ -32,6 +35,8 @@
 import android.content.pm.ResolveInfo;
 import android.os.UserHandle;
 
+import androidx.test.platform.app.InstrumentationRegistry;
+
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.FastPairController;
@@ -50,13 +55,13 @@
 public class FastPairHalfSheetManagerTest {
     private static final String BLEADDRESS = "11:22:44:66";
     private static final String NAME = "device_name";
+    private static final int PASSKEY = 1234;
     private FastPairHalfSheetManager mFastPairHalfSheetManager;
     private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+    @Mock private Context mContext;
     @Mock
     LocatorContextWrapper mContextWrapper;
     @Mock
-    ResolveInfo mResolveInfo;
-    @Mock
     PackageManager mPackageManager;
     @Mock
     Locator mLocator;
@@ -66,7 +71,8 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-
+        when(mContext.getContentResolver()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
         mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
                 .setAddress(BLEADDRESS)
                 .setDeviceName(NAME)
@@ -133,4 +139,24 @@
         verify(mContextWrapper, never())
                 .startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
     }
+
+    @Test
+    public void getHalfSheetForegroundState() {
+        mFastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        assertThat(mFastPairHalfSheetManager.getHalfSheetForegroundState()).isTrue();
+    }
+
+    @Test
+    public void testEmptyMethods() {
+        mFastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        mFastPairHalfSheetManager.destroyBluetoothPairController();
+        mFastPairHalfSheetManager.disableDismissRunnable();
+        mFastPairHalfSheetManager.notifyPairingProcessDone(true, BLEADDRESS, null);
+        mFastPairHalfSheetManager.showPairingFailed();
+        mFastPairHalfSheetManager.showPairingHalfSheet(null);
+        mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLEADDRESS);
+        mFastPairHalfSheetManager.showPasskeyConfirmation(null, PASSKEY);
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
new file mode 100644
index 0000000..4fb6b37
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2022 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.nearby.fastpair.notification;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairNotificationManagerTest {
+
+    @Mock private Context mContext;
+    private static final boolean USE_LARGE_ICON = true;
+    private static final int NOTIFICATION_ID = 1;
+    private static final String COMPANION_APP = "companionApp";
+    private static final int BATTERY_LEVEL = 1;
+    private static final String DEVICE_NAME = "deviceName";
+    private static final String ADDRESS = "address";
+    private FastPairNotificationManager mFastPairNotificationManager;
+    private LocatorContextWrapper mLocatorContextWrapper;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+        when(mContext.getContentResolver()).thenReturn(
+                InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+        mFastPairNotificationManager =
+                new FastPairNotificationManager(mLocatorContextWrapper, null,
+                        USE_LARGE_ICON, NOTIFICATION_ID);
+    }
+
+    @Test
+    public void  notifyPairingProcessDone() {
+        mFastPairNotificationManager.notifyPairingProcessDone(true, true,
+                "privateAddress", "publicAddress");
+    }
+
+    @Test
+    public void  showConnectingNotification() {
+        mFastPairNotificationManager.showConnectingNotification();
+    }
+
+    @Test
+    public void   showPairingFailedNotification() {
+        mFastPairNotificationManager.showPairingFailedNotification(new byte[]{1});
+    }
+
+    @Test
+    public void  showPairingSucceededNotification() {
+        mFastPairNotificationManager.showPairingSucceededNotification(COMPANION_APP,
+                BATTERY_LEVEL, DEVICE_NAME, ADDRESS);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
new file mode 100644
index 0000000..bfe009c
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2022 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.nearby.fastpair.pairinghandler;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+public class HalfSheetPairingProgressHandlerTest {
+    @Mock
+    Locator mLocator;
+    @Mock
+    LocatorContextWrapper mContextWrapper;
+    @Mock
+    Clock mClock;
+    @Mock
+    FastPairCacheManager mFastPairCacheManager;
+    @Mock
+    FastPairConnection mFastPairConnection;
+    @Mock
+    FootprintsDeviceManager mFootprintsDeviceManager;
+
+    private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+    private static final int SUBSEQUENT_PAIR_START = 1310;
+    private static final int SUBSEQUENT_PAIR_END = 1320;
+    private static final int PASSKEY = 1234;
+    private static HalfSheetPairingProgressHandler sHalfSheetPairingProgressHandler;
+    private static DiscoveryItem sDiscoveryItem;
+    private static BluetoothDevice sBluetoothDevice;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mContextWrapper.getLocator()).thenReturn(mLocator);
+        mLocator.overrideBindingForTest(FastPairCacheManager.class, mFastPairCacheManager);
+        mLocator.overrideBindingForTest(Clock.class, mClock);
+        FastPairHalfSheetManager mfastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        mLocator.bind(FastPairHalfSheetManager.class, mfastPairHalfSheetManager);
+        when(mLocator.get(FastPairHalfSheetManager.class)).thenReturn(mfastPairHalfSheetManager);
+        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
+                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+                        .setMacAddress(MAC_ADDRESS)
+                        .setFastPairInformation(
+                                Cache.FastPairInformation.newBuilder()
+                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+                        .build());
+        sHalfSheetPairingProgressHandler =
+                new HalfSheetPairingProgressHandler(mContextWrapper, sDiscoveryItem,
+                        sDiscoveryItem.getAppPackageName(), ACCOUNT_KEY);
+
+        sBluetoothDevice =
+                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+    }
+
+    @Test
+    public void getPairEndEventCode() {
+        assertThat(sHalfSheetPairingProgressHandler
+                .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
+    }
+
+    @Test
+    public void getPairStartEventCode() {
+        assertThat(sHalfSheetPairingProgressHandler
+                .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
+    }
+
+    @Test
+    public void testOnHandlePasskeyConfirmation() {
+        sHalfSheetPairingProgressHandler.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
+    }
+
+    @Test
+    public void testOnPairedCallbackCalled() {
+        sHalfSheetPairingProgressHandler.onPairedCallbackCalled(mFastPairConnection, ACCOUNT_KEY,
+                mFootprintsDeviceManager, MAC_ADDRESS);
+    }
+
+    @Test
+    public void testonPairingFailed() {
+        Throwable e = new Throwable("onPairingFailed");
+        sHalfSheetPairingProgressHandler.onPairingFailed(e);
+    }
+
+    @Test
+    public void testonPairingStarted() {
+        sHalfSheetPairingProgressHandler.onPairingStarted();
+    }
+
+    @Test
+    public void testonPairingSuccess() {
+        sHalfSheetPairingProgressHandler.onPairingSuccess(MAC_ADDRESS);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
new file mode 100644
index 0000000..0405d04
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2022 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.nearby.fastpair.pairinghandler;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+public class NotificationPairingProgressHandlerTest {
+    @Mock
+    Locator mLocator;
+    @Mock
+    LocatorContextWrapper mContextWrapper;
+    @Mock
+    Clock mClock;
+    @Mock
+    FastPairCacheManager mFastPairCacheManager;
+
+    private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+    private static final int SUBSEQUENT_PAIR_START = 1310;
+    private static final int SUBSEQUENT_PAIR_END = 1320;
+    private static DiscoveryItem sDiscoveryItem;
+    private static  NotificationPairingProgressHandler sNotificationPairingProgressHandler;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mContextWrapper.getLocator()).thenReturn(mLocator);
+        mLocator.overrideBindingForTest(FastPairCacheManager.class,
+                mFastPairCacheManager);
+        mLocator.overrideBindingForTest(Clock.class, mClock);
+        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
+                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+                        .setFastPairInformation(
+                                Cache.FastPairInformation.newBuilder()
+                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+                        .build());
+        sNotificationPairingProgressHandler = createProgressHandler(ACCOUNT_KEY, sDiscoveryItem);
+    }
+
+    @Test
+    public void getPairEndEventCode() {
+        assertThat(sNotificationPairingProgressHandler
+                .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
+    }
+
+    @Test
+    public void getPairStartEventCode() {
+        assertThat(sNotificationPairingProgressHandler
+                .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
+    }
+
+    @Test
+    public void onReadyToPair() {
+        sNotificationPairingProgressHandler.onReadyToPair();
+    }
+
+    @Test
+    public void  onPairingFailed() {
+        Throwable e = new Throwable("Pairing Failed");
+        sNotificationPairingProgressHandler.onPairingFailed(e);
+    }
+
+    @Test
+    public void onPairingSuccess() {
+        sNotificationPairingProgressHandler.onPairingSuccess(sDiscoveryItem.getMacAddress());
+    }
+
+    private NotificationPairingProgressHandler createProgressHandler(
+            @Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
+        FastPairNotificationManager fastPairNotificationManager =
+                new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
+        FastPairHalfSheetManager fastPairHalfSheetManager =
+                new FastPairHalfSheetManager(mContextWrapper);
+        mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
+        NotificationPairingProgressHandler mNotificationPairingProgressHandler =
+                new NotificationPairingProgressHandler(
+                        mContextWrapper,
+                        fastPairItem,
+                        fastPairItem.getAppPackageName(),
+                        accountKey,
+                        fastPairNotificationManager);
+        return mNotificationPairingProgressHandler;
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
index 2ade5f2..b4b4f78 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
@@ -20,8 +20,13 @@
 
 import static org.mockito.Mockito.when;
 
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
 import androidx.annotation.Nullable;
 
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -42,7 +47,6 @@
 
 import service.proto.Cache;
 import service.proto.Rpcs;
-
 public class PairingProgressHandlerBaseTest {
     @Mock
     Locator mLocator;
@@ -54,24 +58,41 @@
     FastPairCacheManager mFastPairCacheManager;
     @Mock
     FootprintsDeviceManager mFootprintsDeviceManager;
+    @Mock
+    FastPairConnection mFastPairConnection;
+
     private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+    private static final int PASSKEY = 1234;
+    private static DiscoveryItem sDiscoveryItem;
+    private static PairingProgressHandlerBase sPairingProgressHandlerBase;
+    private static BluetoothDevice sBluetoothDevice;
 
     @Before
     public void setup() {
-
         MockitoAnnotations.initMocks(this);
         when(mContextWrapper.getLocator()).thenReturn(mLocator);
         mLocator.overrideBindingForTest(FastPairCacheManager.class,
                 mFastPairCacheManager);
         mLocator.overrideBindingForTest(Clock.class, mClock);
+        sBluetoothDevice =
+                BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+        sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
+                        .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+                        .setFastPairInformation(
+                                Cache.FastPairInformation.newBuilder()
+                                        .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+                        .build());
+
+        sPairingProgressHandlerBase =
+                createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
     }
 
     @Test
     public void createHandler_halfSheetSubsequentPairing_notificationPairingHandlerCreated() {
-
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
                         .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
                         .setFastPairInformation(
                                 Cache.FastPairInformation.newBuilder()
@@ -79,7 +100,7 @@
                         .build());
 
         PairingProgressHandlerBase progressHandler =
-                createProgressHandler(ACCOUNT_KEY, discoveryItem, /* isRetroactivePair= */ false);
+                createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
 
         assertThat(progressHandler).isInstanceOf(NotificationPairingProgressHandler.class);
     }
@@ -87,20 +108,88 @@
     @Test
     public void createHandler_halfSheetInitialPairing_halfSheetPairingHandlerCreated() {
         // No account key
-        DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
-        discoveryItem.setStoredItemForTest(
-                discoveryItem.getStoredItemForTest().toBuilder()
+        sDiscoveryItem.setStoredItemForTest(
+                sDiscoveryItem.getStoredItemForTest().toBuilder()
                         .setFastPairInformation(
                                 Cache.FastPairInformation.newBuilder()
                                         .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
                         .build());
 
         PairingProgressHandlerBase progressHandler =
-                createProgressHandler(null, discoveryItem, /* isRetroactivePair= */ false);
+                createProgressHandler(null, sDiscoveryItem, /* isRetroactivePair= */ false);
 
         assertThat(progressHandler).isInstanceOf(HalfSheetPairingProgressHandler.class);
     }
 
+    @Test
+    public void onPairingStarted() {
+        sPairingProgressHandlerBase.onPairingStarted();
+    }
+
+    @Test
+    public void onWaitForScreenUnlock() {
+        sPairingProgressHandlerBase.onWaitForScreenUnlock();
+    }
+
+    @Test
+    public void  onScreenUnlocked() {
+        sPairingProgressHandlerBase.onScreenUnlocked();
+    }
+
+    @Test
+    public void onReadyToPair() {
+        sPairingProgressHandlerBase.onReadyToPair();
+    }
+
+    @Test
+    public void  onSetupPreferencesBuilder() {
+        Preferences.Builder prefsBuilder =
+                Preferences.builder()
+                        .setEnableBrEdrHandover(false)
+                        .setIgnoreDiscoveryError(true);
+        sPairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
+    }
+
+    @Test
+    public void  onPairingSetupCompleted() {
+        sPairingProgressHandlerBase.onPairingSetupCompleted();
+    }
+
+    @Test
+    public void onHandlePasskeyConfirmation() {
+        sPairingProgressHandlerBase.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
+    }
+
+    @Test
+    public void getKeyForLocalCache() {
+        FastPairConnection.SharedSecret sharedSecret =
+                FastPairConnection.SharedSecret.create(ACCOUNT_KEY, sDiscoveryItem.getMacAddress());
+        sPairingProgressHandlerBase
+                .getKeyForLocalCache(ACCOUNT_KEY, mFastPairConnection, sharedSecret);
+    }
+
+    @Test
+    public void onPairingFailed() {
+        Throwable e = new Throwable("Pairing Failed");
+        sPairingProgressHandlerBase.onPairingFailed(e);
+    }
+
+    @Test
+    public void onPairingSuccess() {
+        sPairingProgressHandlerBase.onPairingSuccess(sDiscoveryItem.getMacAddress());
+    }
+
+    @Test
+    public void  optInFootprintsForInitialPairing() {
+        sPairingProgressHandlerBase.optInFootprintsForInitialPairing(
+                mFootprintsDeviceManager, sDiscoveryItem, ACCOUNT_KEY, null);
+    }
+
+    @Test
+    public void skipWaitingScreenUnlock() {
+        assertThat(sPairingProgressHandlerBase.skipWaitingScreenUnlock()).isFalse();
+    }
+
     private PairingProgressHandlerBase createProgressHandler(
             @Nullable byte[] accountKey, DiscoveryItem fastPairItem, boolean isRetroactivePair) {
         FastPairNotificationManager fastPairNotificationManager =
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
index c406e47..cdec04d 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
@@ -22,10 +22,17 @@
 import service.proto.Cache;
 
 public class FakeDiscoveryItems {
-    public static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
-    public static final long DEFAULT_TIMESTAMP = 1000000000L;
-    public static final String DEFAULT_DESCRIPITON = "description";
-    public static final String TRIGGER_ID = "trigger.id";
+    private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
+    private static final long DEFAULT_TIMESTAMP = 1000000000L;
+    private static final String DEFAULT_DESCRIPITON = "description";
+    private static final String APP_NAME = "app_name";
+    private static final String ACTION_URL =
+            "intent:#Intent;action=com.android.server.nearby:ACTION_FAST_PAIR;"
+                    + "package=com.google.android.gms;"
+                    + "component=com.google.android.gms/"
+                    + ".nearby.discovery.service.DiscoveryService;end";
+    private static final String DISPLAY_URL = "DISPLAY_URL";
+    private static final String TRIGGER_ID = "trigger.id";
     private static final String FAST_PAIR_ID = "id";
     private static final int RSSI = -80;
     private static final int TX_POWER = -10;
@@ -46,9 +53,36 @@
         item.setMacAddress(DEFAULT_MAC_ADDRESS);
         item.setFirstObservationTimestampMillis(DEFAULT_TIMESTAMP);
         item.setLastObservationTimestampMillis(DEFAULT_TIMESTAMP);
+        item.setActionUrl(ACTION_URL);
+        item.setAppName(APP_NAME);
         item.setRssi(RSSI);
         item.setTxPower(TX_POWER);
+        item.setDisplayUrl(DISPLAY_URL);
         return item.build();
     }
 
+    public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem(String id,
+            String description, String triggerId, String macAddress, String title,
+            int rssi, int txPower) {
+        Cache.StoredDiscoveryItem.Builder item = Cache.StoredDiscoveryItem.newBuilder();
+        item.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+        if (id != null) {
+            item.setId(id);
+        }
+        if (description != null) {
+            item.setDescription(description);
+        }
+        if (triggerId != null) {
+            item.setTriggerId(triggerId);
+        }
+        if (macAddress != null) {
+            item.setMacAddress(macAddress);
+        }
+        if (title != null) {
+            item.setTitle(title);
+        }
+        item.setRssi(rssi);
+        item.setTxPower(txPower);
+        return item.build();
+    }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
index 5e0ccbe..8e3e068 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -75,6 +75,15 @@
         assertThat(originalAdvertisement.getVersion()).isEqualTo(
                 BroadcastRequest.PRESENCE_VERSION_V0);
         assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+        assertThat(originalAdvertisement.getTxPower()).isEqualTo(TX_POWER);
+        assertThat(originalAdvertisement.toString())
+                .isEqualTo("FastAdvertisement:<VERSION: 0, length: 19,"
+                        + " ltvFieldCount: 4,"
+                        + " identityType: 1,"
+                        + " identity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],"
+                        + " salt: [2, 3],"
+                        + " actions: [123],"
+                        + " txPower: 4");
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index d06a785..88cd9af 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -78,6 +78,11 @@
                 .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
     }
 
+    @Test
+    public void testStop() {
+        mBleBroadcastProvider.stop();
+    }
+
     private static class TestInjector implements Injector {
 
         @Override
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index 902cc33..1d485ca 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -70,6 +70,7 @@
         // Wait for callback to be invoked
         Thread.sleep(500);
         verify(mListener, times(1)).onNearbyDeviceDiscovered(any());
+        mBleDiscoveryProvider.getScanCallback().onScanFailed(1);
     }
 
     @Test
@@ -78,6 +79,11 @@
         mBleDiscoveryProvider.onStop();
     }
 
+    @Test
+    public void testInvalidateScanMode() {
+        mBleDiscoveryProvider.invalidateScanMode();
+    }
+
     private class TestInjector implements Injector {
         @Override
         public BluetoothAdapter getBluetoothAdapter() {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
index d45d570..5090cc0 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
@@ -106,6 +106,11 @@
     }
 
     @Test
+    public void testStopAdvertising() {
+        mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
+    }
+
+    @Test
     public void testStartAdvertising_featureDisabled() throws Exception {
         DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY,
                 "false", false);
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
index 1b29b52..c90860e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.nearby.provider;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
@@ -106,6 +108,19 @@
                         new byte[] {1, 2, 3});
         mChreCommunication.onMessageFromNanoApp(mClient, message);
         verify(mChreCallback).onMessageFromNanoApp(eq(message));
+
+    }
+
+    @Test
+    public void testContextHubTransactionResultToString() {
+        NanoAppMessage message =
+                NanoAppMessage.createMessageToNanoApp(
+                        ChreDiscoveryProvider.NANOAPP_ID,
+                        ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+                        new byte[] {1, 2, 3});
+        assertThat(
+                mChreCommunication.contextHubTransactionResultToString(
+                        mClient.sendMessageToNanoApp(message))).isEqualTo("RESULT_SUCCESS");
     }
 
     @Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
new file mode 100644
index 0000000..1af959d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2022 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.nearby.provider;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
+
+import com.android.server.nearby.injector.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DiscoveryProviderManagerTest {
+    @Mock Injector mInjector;
+    @Mock Context mContext;
+    @Mock AppOpsManager mAppOpsManager;
+    private DiscoveryProviderManager mDiscoveryProviderManager;
+
+    private static final int RSSI = -60;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+        mDiscoveryProviderManager =
+                new DiscoveryProviderManager(mContext, mInjector);
+    }
+
+
+    @Test
+    public void testOnNearbyDeviceDiscovered() {
+        NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+                .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+                .build();
+        mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+    }
+
+    @Test
+    public void testInvalidateProviderScanMode() {
+        mDiscoveryProviderManager.invalidateProviderScanMode();
+    }
+
+    @Test
+    public void testStartChreProvider() {
+        mDiscoveryProviderManager.startChreProvider();
+    }
+
+    private static PresenceScanFilter getPresenceScanFilter() {
+        final byte[] secretId = new byte[]{1, 2, 3, 4};
+        final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+        final byte[] publicKey = new byte[]{1, 1, 2, 2};
+        final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+        final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+        PublicCredential credential = new PublicCredential.Builder(
+                secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+                .setIdentityType(IDENTITY_TYPE_PRIVATE)
+                .build();
+
+        final int action = 123;
+        return new PresenceScanFilter.Builder()
+                .addCredential(credential)
+                .setMaxPathLoss(RSSI)
+                .addPresenceAction(action)
+                .build();
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java
new file mode 100644
index 0000000..300efbd
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2022 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.nearby.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.nearby.FastPairDataProviderService;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Cache;
+import service.proto.FastPairString;
+import service.proto.Rpcs;
+
+public class FastPairDataProviderTest {
+
+    private static final Account ACCOUNT = new Account("abc@google.com", "type1");
+    private static final byte[] MODEL_ID = new byte[]{7, 9};
+    private static final int BLE_TX_POWER = 5;
+    private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+            "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+    private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+            "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+    private static final int DEVICE_TYPE = 1;
+    private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+            "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+    private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+            "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+    private static final byte[] IMAGE = new byte[]{7, 9};
+    private static final String IMAGE_URL = "IMAGE_URL";
+    private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+            "INITIAL_NOTIFICATION_DESCRIPTION";
+    private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+            "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+    private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+    private static final String INTENT_URI = "INTENT_URI";
+    private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+    private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+            "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+    private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+    private static final float TRIGGER_DISTANCE = 111;
+    private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+    private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+            "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+    private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+            "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+    private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+    private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+    private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+            "UPDATE_COMPANION_APP_DESCRIPTION";
+    private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+            "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+    private static final byte[] ACCOUNT_KEY = new byte[]{3};
+    private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[]{2, 8};
+    private static final byte[] ANTI_SPOOFING_KEY = new byte[]{4, 5, 6};
+    private static final String ACTION_URL = "ACTION_URL";
+    private static final String APP_NAME = "APP_NAME";
+    private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[]{5, 7};
+    private static final String DESCRIPTION = "DESCRIPTION";
+    private static final String DEVICE_NAME = "DEVICE_NAME";
+    private static final String DISPLAY_URL = "DISPLAY_URL";
+    private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
+    private static final String ICON_FIFE_URL = "ICON_FIFE_URL";
+    private static final byte[] ICON_PNG = new byte[]{2, 5};
+    private static final String ID = "ID";
+    private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
+    private static final String MAC_ADDRESS = "MAC_ADDRESS";
+    private static final String NAME = "NAME";
+    private static final String PACKAGE_NAME = "PACKAGE_NAME";
+    private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
+    private static final int RSSI = 9;
+    private static final String TITLE = "TITLE";
+    private static final String TRIGGER_ID = "TRIGGER_ID";
+    private static final int TX_POWER = 63;
+
+    @Mock ProxyFastPairDataProvider mProxyFastPairDataProvider;
+
+    FastPairDataProvider mFastPairDataProvider;
+    FastPairEligibleAccountParcel[] mFastPairEligibleAccountParcels =
+            { genHappyPathFastPairEligibleAccountParcel() };
+    FastPairAntispoofKeyDeviceMetadataParcel mFastPairAntispoofKeyDeviceMetadataParcel =
+            genHappyPathFastPairAntispoofKeyDeviceMetadataParcel();
+    FastPairUploadInfo mFastPairUploadInfo = genHappyPathFastPairUploadInfo();
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        mFastPairDataProvider = FastPairDataProvider.init(context);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testFailurePath_throwsException() throws IllegalStateException {
+        mFastPairDataProvider = FastPairDataProvider.getInstance();
+        assertThrows(
+                IllegalStateException.class,
+                () -> {
+                    mFastPairDataProvider.loadFastPairEligibleAccounts(); });
+        assertThrows(
+                IllegalStateException.class,
+                () -> {
+                    mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(MODEL_ID); });
+        assertThrows(
+                IllegalStateException.class,
+                () -> {
+                    mFastPairDataProvider.loadFastPairDeviceWithAccountKey(ACCOUNT); });
+        assertThrows(
+                IllegalStateException.class,
+                () -> {
+                    mFastPairDataProvider.loadFastPairDeviceWithAccountKey(
+                            ACCOUNT, ImmutableList.of()); });
+        assertThrows(
+                IllegalStateException.class,
+                () -> {
+                    mFastPairDataProvider.optIn(ACCOUNT); });
+        assertThrows(
+                IllegalStateException.class,
+                () -> {
+                    mFastPairDataProvider.upload(ACCOUNT, mFastPairUploadInfo); });
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testLoadFastPairAntispoofKeyDeviceMetadata_receivesResponse()  {
+        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+        when(mProxyFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(any()))
+                .thenReturn(mFastPairAntispoofKeyDeviceMetadataParcel);
+
+        mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(MODEL_ID);
+        ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataRequestParcel> captor =
+                ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataRequestParcel.class);
+        verify(mProxyFastPairDataProvider).loadFastPairAntispoofKeyDeviceMetadata(captor.capture());
+        assertThat(captor.getValue().modelId).isSameInstanceAs(MODEL_ID);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testOptIn_finishesSuccessfully()  {
+        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+        doNothing().when(mProxyFastPairDataProvider).manageFastPairAccount(any());
+        mFastPairDataProvider.optIn(ACCOUNT);
+        ArgumentCaptor<FastPairManageAccountRequestParcel> captor =
+                ArgumentCaptor.forClass(FastPairManageAccountRequestParcel.class);
+        verify(mProxyFastPairDataProvider).manageFastPairAccount(captor.capture());
+        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+        assertThat(captor.getValue().requestType).isEqualTo(
+                FastPairDataProviderService.MANAGE_REQUEST_ADD);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testUpload_finishesSuccessfully()  {
+        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+        doNothing().when(mProxyFastPairDataProvider).manageFastPairAccountDevice(any());
+        mFastPairDataProvider.upload(ACCOUNT, mFastPairUploadInfo);
+        ArgumentCaptor<FastPairManageAccountDeviceRequestParcel> captor =
+                ArgumentCaptor.forClass(FastPairManageAccountDeviceRequestParcel.class);
+        verify(mProxyFastPairDataProvider).manageFastPairAccountDevice(captor.capture());
+        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+        assertThat(captor.getValue().requestType).isEqualTo(
+                FastPairDataProviderService.MANAGE_REQUEST_ADD);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testLoadFastPairEligibleAccounts_receivesOneAccount()  {
+        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+        when(mProxyFastPairDataProvider.loadFastPairEligibleAccounts(any()))
+                .thenReturn(mFastPairEligibleAccountParcels);
+        assertThat(mFastPairDataProvider.loadFastPairEligibleAccounts().size())
+                .isEqualTo(1);
+        ArgumentCaptor<FastPairEligibleAccountsRequestParcel> captor =
+                ArgumentCaptor.forClass(FastPairEligibleAccountsRequestParcel.class);
+        verify(mProxyFastPairDataProvider).loadFastPairEligibleAccounts(captor.capture());
+        assertThat(captor.getValue()).isNotNull();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testLoadFastPairDeviceWithAccountKey_finishesSuccessfully()  {
+        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+        when(mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(any()))
+                .thenReturn(null);
+
+        mFastPairDataProvider.loadFastPairDeviceWithAccountKey(ACCOUNT);
+        ArgumentCaptor<FastPairAccountDevicesMetadataRequestParcel> captor =
+                ArgumentCaptor.forClass(FastPairAccountDevicesMetadataRequestParcel.class);
+        verify(mProxyFastPairDataProvider).loadFastPairAccountDevicesMetadata(captor.capture());
+        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+        assertThat(captor.getValue().deviceAccountKeys).isEmpty();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testLoadFastPairDeviceWithAccountKeyDeviceAccountKeys_finishesSuccessfully()  {
+        mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+        when(mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(any()))
+                .thenReturn(null);
+
+        mFastPairDataProvider.loadFastPairDeviceWithAccountKey(
+                ACCOUNT, ImmutableList.of(ACCOUNT_KEY));
+        ArgumentCaptor<FastPairAccountDevicesMetadataRequestParcel> captor =
+                ArgumentCaptor.forClass(FastPairAccountDevicesMetadataRequestParcel.class);
+        verify(mProxyFastPairDataProvider).loadFastPairAccountDevicesMetadata(captor.capture());
+        assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+        assertThat(captor.getValue().deviceAccountKeys.length).isEqualTo(1);
+        assertThat(captor.getValue().deviceAccountKeys[0].byteArray).isSameInstanceAs(ACCOUNT_KEY);
+    }
+
+    private static FastPairEligibleAccountParcel genHappyPathFastPairEligibleAccountParcel() {
+        FastPairEligibleAccountParcel parcel = new FastPairEligibleAccountParcel();
+        parcel.account = ACCOUNT;
+        parcel.optIn = true;
+
+        return parcel;
+    }
+
+    private static FastPairAntispoofKeyDeviceMetadataParcel
+                genHappyPathFastPairAntispoofKeyDeviceMetadataParcel() {
+        FastPairAntispoofKeyDeviceMetadataParcel parcel =
+                new FastPairAntispoofKeyDeviceMetadataParcel();
+        parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
+        parcel.deviceMetadata = genHappyPathFastPairDeviceMetadataParcel();
+
+        return parcel;
+    }
+
+    private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
+        FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
+
+        parcel.bleTxPower = BLE_TX_POWER;
+        parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
+        parcel.connectSuccessCompanionAppNotInstalled =
+                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
+        parcel.deviceType = DEVICE_TYPE;
+        parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
+        parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
+        parcel.image = IMAGE;
+        parcel.imageUrl = IMAGE_URL;
+        parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
+        parcel.initialNotificationDescriptionNoAccount =
+                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
+        parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
+        parcel.intentUri = INTENT_URI;
+        parcel.name = NAME;
+        parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
+        parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
+        parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
+        parcel.triggerDistance = TRIGGER_DISTANCE;
+        parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
+        parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
+        parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
+        parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
+        parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
+        parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
+        parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
+
+        return parcel;
+    }
+
+    private static Cache.StoredDiscoveryItem genHappyPathStoredDiscoveryItem() {
+        Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
+                Cache.StoredDiscoveryItem.newBuilder();
+        storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
+        storedDiscoveryItemBuilder.setActionUrlType(Cache.ResolvedUrlType.WEBPAGE);
+        storedDiscoveryItemBuilder.setAppName(APP_NAME);
+        storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
+                ByteString.copyFrom(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1));
+        storedDiscoveryItemBuilder.setDescription(DESCRIPTION);
+        storedDiscoveryItemBuilder.setDeviceName(DEVICE_NAME);
+        storedDiscoveryItemBuilder.setDisplayUrl(DISPLAY_URL);
+        storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
+                FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+        storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
+        storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
+        storedDiscoveryItemBuilder.setId(ID);
+        storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
+                LAST_OBSERVATION_TIMESTAMP_MILLIS);
+        storedDiscoveryItemBuilder.setMacAddress(MAC_ADDRESS);
+        storedDiscoveryItemBuilder.setPackageName(PACKAGE_NAME);
+        storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
+                PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+        storedDiscoveryItemBuilder.setRssi(RSSI);
+        storedDiscoveryItemBuilder.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+        storedDiscoveryItemBuilder.setTitle(TITLE);
+        storedDiscoveryItemBuilder.setTriggerId(TRIGGER_ID);
+        storedDiscoveryItemBuilder.setTxPower(TX_POWER);
+
+        FastPairString.FastPairStrings.Builder stringsBuilder =
+                FastPairString.FastPairStrings.newBuilder();
+        stringsBuilder.setPairingFinishedCompanionAppInstalled(
+                CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+        stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
+                CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+        stringsBuilder.setPairingFailDescription(
+                FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+        stringsBuilder.setTapToPairWithAccount(
+                INITIAL_NOTIFICATION_DESCRIPTION);
+        stringsBuilder.setTapToPairWithoutAccount(
+                INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+        stringsBuilder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+        stringsBuilder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+        stringsBuilder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+        stringsBuilder.setWaitAppLaunchDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+        storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
+
+        Cache.FastPairInformation.Builder fpInformationBuilder =
+                Cache.FastPairInformation.newBuilder();
+        Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
+                Rpcs.TrueWirelessHeadsetImages.newBuilder();
+        imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
+        imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+        imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+        fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
+        fpInformationBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
+
+        storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
+        storedDiscoveryItemBuilder.setTxPower(TX_POWER);
+
+        storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
+        storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
+        storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
+
+        return storedDiscoveryItemBuilder.build();
+    }
+
+    private static FastPairUploadInfo genHappyPathFastPairUploadInfo() {
+        return new FastPairUploadInfo(
+                genHappyPathStoredDiscoveryItem(),
+                ByteString.copyFrom(ACCOUNT_KEY),
+                ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
new file mode 100644
index 0000000..0fe28df
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 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.nearby.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+public final class ArrayUtilsTest {
+
+    private static final byte[] BYTES_ONE = new byte[] {7, 9};
+    private static final byte[] BYTES_TWO = new byte[] {8};
+    private static final byte[] BYTES_EMPTY = new byte[] {};
+    private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysNoInput() {
+        assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysOneEmptyArray() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysOneNonEmptyArray() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysMultipleNonEmptyArrays() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testConcatByteArraysMultipleArrays() {
+        assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
+                .isEqualTo(BYTES_ALL);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
index f098600..9867a37 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
@@ -107,6 +107,24 @@
     public void test_toString() {
         Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
                 createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+
+        assertThat(DataUtils.toString(item))
+                .isEqualTo("ScanFastPairStoreItem=[address:00:11:22:33:FF:EE, "
+                        + "actionUrl:intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
+                        + "package=to_be_set;component=to_be_set;"
+                        + "to_be_set%3AEXTRA_COMPANION_APP=test_package;"
+                        + "end, deviceName:My device, "
+                        + "iconFifeUrl:device_image_url, "
+                        + "fastPairStrings:FastPairStrings[tapToPairWithAccount=message 1, "
+                        + "tapToPairWithoutAccount=message 2, "
+                        + "initialPairingDescription=message 3 My device, "
+                        + "pairingFinishedCompanionAppInstalled=message 4, "
+                        + "pairingFinishedCompanionAppNotInstalled=message 5, "
+                        + "subsequentPairingDescription=message 6, "
+                        + "retroactivePairingDescription=message 7, "
+                        + "waitAppLaunchDescription=message 8, "
+                        + "pairingFailDescription=message 9]]");
+
         FastPairStrings strings = item.getFastPairStrings();
 
         assertThat(DataUtils.toString(strings))
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
new file mode 100644
index 0000000..e167cf4
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2022 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.nearby.util;
+
+import org.junit.Test;
+
+public class EnvironmentTest {
+
+    @Test
+    public void getNearbyDirectory() {
+        Environment.getNearbyDirectory();
+        Environment.getNearbyDirectory(1);
+    }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
new file mode 100644
index 0000000..a18aa1f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2022 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.nearby.util.identity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CallerIdentityTest {
+    private static final int UID = 100;
+    private static final int PID = 10002;
+    private static final String PACKAGE_NAME = "package_name";
+    private static final String ATTRIBUTION_TAG = "attribution_tag";
+
+    @Test
+    public void testToString() {
+        CallerIdentity callerIdentity =
+                CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+        assertThat(callerIdentity.toString()).isEqualTo("100/package_name[attribution_tag]");
+        assertThat(callerIdentity.isSystemServer()).isFalse();
+    }
+}
diff --git a/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 42d0de5..fad6bbb 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -101,8 +101,6 @@
     RETURN_IF_NOT_OK(mStatsMapA.init(STATS_MAP_A_PATH));
     RETURN_IF_NOT_OK(mStatsMapB.init(STATS_MAP_B_PATH));
     RETURN_IF_NOT_OK(mConfigurationMap.init(CONFIGURATION_MAP_PATH));
-    RETURN_IF_NOT_OK(mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY, SELECT_MAP_A,
-                                                  BPF_ANY));
     RETURN_IF_NOT_OK(mUidPermissionMap.init(UID_PERMISSION_MAP_PATH));
     ALOGI("%s successfully", __func__);
 
@@ -199,6 +197,7 @@
 
     BpfMap<StatsKey, StatsValue>& currentMap =
             (configuration.value() == SELECT_MAP_A) ? mStatsMapA : mStatsMapB;
+    // HACK: mStatsMapB becomes RW BpfMap here, but countUidStatsEntries doesn't modify so it works
     base::Result<void> res = currentMap.iterate(countUidStatsEntries);
     if (!res.ok()) {
         ALOGE("Failed to count the stats entry in map %d: %s", currentMap.getMap().get(),
diff --git a/netd/BpfHandler.h b/netd/BpfHandler.h
index 05b9ebc..5ee04d1 100644
--- a/netd/BpfHandler.h
+++ b/netd/BpfHandler.h
@@ -23,6 +23,7 @@
 #include "bpf_shared.h"
 
 using android::bpf::BpfMap;
+using android::bpf::BpfMapRO;
 
 namespace android {
 namespace net {
@@ -61,8 +62,8 @@
 
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
     BpfMap<StatsKey, StatsValue> mStatsMapA;
-    BpfMap<StatsKey, StatsValue> mStatsMapB;
-    BpfMap<uint32_t, uint32_t> mConfigurationMap;
+    BpfMapRO<StatsKey, StatsValue> mStatsMapB;
+    BpfMapRO<uint32_t, uint32_t> mConfigurationMap;
     BpfMap<uint32_t, uint8_t> mUidPermissionMap;
 
     std::mutex mMutex;
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index 1bd222d..99160da 100644
--- a/netd/BpfHandlerTest.cpp
+++ b/netd/BpfHandlerTest.cpp
@@ -21,7 +21,7 @@
 
 #include <gtest/gtest.h>
 
-#define TEST_BPF_MAP
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "BpfHandler.h"
 
 using namespace android::bpf;  // NOLINT(google-build-using-namespace): exempted
@@ -49,7 +49,7 @@
     BpfHandler mBh;
     BpfMap<uint64_t, UidTagValue> mFakeCookieTagMap;
     BpfMap<StatsKey, StatsValue> mFakeStatsMapA;
-    BpfMap<uint32_t, uint32_t> mFakeConfigurationMap;
+    BpfMapRO<uint32_t, uint32_t> mFakeConfigurationMap;
     BpfMap<uint32_t, uint8_t> mFakeUidPermissionMap;
 
     void SetUp() {
@@ -62,10 +62,10 @@
         mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeStatsMapA);
 
-        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_HASH, 1);
+        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
         ASSERT_VALID(mFakeConfigurationMap);
 
-        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
+        mFakeUidPermissionMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeUidPermissionMap);
 
         mBh.mCookieTagMap = mFakeCookieTagMap;
@@ -75,8 +75,8 @@
         mBh.mConfigurationMap = mFakeConfigurationMap;
         ASSERT_VALID(mBh.mConfigurationMap);
         // Always write to stats map A by default.
-        ASSERT_RESULT_OK(mBh.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
-                                                          SELECT_MAP_A, BPF_ANY));
+        static_assert(SELECT_MAP_A == 0, "bpf map arrays are zero-initialized");
+
         mBh.mUidPermissionMap = mFakeUidPermissionMap;
         ASSERT_VALID(mBh.mUidPermissionMap);
     }
diff --git a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
index 4974b96..6f9c8c2 100644
--- a/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
+++ b/service-t/native/libs/libnetworkstats/BpfNetworkStatsTest.cpp
@@ -33,6 +33,7 @@
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "bpf/BpfMap.h"
 #include "bpf/BpfUtils.h"
 #include "netdbpf/BpfNetworkStats.h"
@@ -80,19 +81,19 @@
         ASSERT_EQ(0, setrlimitForTest());
 
         mFakeCookieTagMap = BpfMap<uint64_t, UidTagValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeCookieTagMap.getMap());
+        ASSERT_TRUE(mFakeCookieTagMap.isValid());
 
         mFakeAppUidStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeAppUidStatsMap.getMap());
+        ASSERT_TRUE(mFakeAppUidStatsMap.isValid());
 
         mFakeStatsMap = BpfMap<StatsKey, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeStatsMap.getMap());
+        ASSERT_TRUE(mFakeStatsMap.isValid());
 
         mFakeIfaceIndexNameMap = BpfMap<uint32_t, IfaceValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeIfaceIndexNameMap.getMap());
+        ASSERT_TRUE(mFakeIfaceIndexNameMap.isValid());
 
         mFakeIfaceStatsMap = BpfMap<uint32_t, StatsValue>(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE, 0);
-        ASSERT_LE(0, mFakeIfaceStatsMap.getMap());
+        ASSERT_TRUE(mFakeIfaceStatsMap.isValid());
     }
 
     void expectUidTag(uint64_t cookie, uid_t uid, uint32_t tag) {
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 79802fb..c4ea9ae 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -488,8 +488,6 @@
             if (null != capabilities) {
                 setCapabilities(capabilities);
             }
-            // Send an abort callback if a request is filed before the previous one has completed.
-            maybeSendNetworkManagementCallbackForAbort();
             // TODO: Update this logic to only do a restart if required. Although a restart may
             //  be required due to the capabilities or ipConfiguration values, not all
             //  capabilities changes require a restart.
@@ -651,6 +649,8 @@
                 mIpClientCallback.awaitIpClientShutdown();
                 mIpClient = null;
             }
+            // Send an abort callback if an updateInterface request was in progress.
+            maybeSendNetworkManagementCallbackForAbort();
             mIpClientCallback = null;
 
             if (mNetworkAgent != null) {
@@ -662,7 +662,6 @@
 
         public void destroy() {
             mNetworkProvider.unregisterNetworkOffer(mNetworkOfferCallback);
-            maybeSendNetworkManagementCallbackForAbort();
             stop();
             mRequests.clear();
         }
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 7cc19a0..31caabc 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -270,6 +270,8 @@
                     + ", ipConfig: " + ipConfig);
         }
 
+        // TODO: do the right thing if the interface was in server mode: either fail this operation,
+        // or take the interface out of server mode.
         final IpConfiguration localIpConfig = ipConfig == null
                 ? null : new IpConfiguration(ipConfig);
         if (ipConfig != null) {
@@ -580,8 +582,8 @@
         }
         if (DBG) Log.i(TAG, "maybeTrackInterface: " + iface);
 
-        // TODO: avoid making an interface default if it has configured NetworkCapabilities.
-        if (mDefaultInterface == null) {
+        // Do not make an interface default if it has configured NetworkCapabilities.
+        if (mDefaultInterface == null && !mNetworkCapabilities.containsKey(iface)) {
             mDefaultInterface = iface;
         }
 
diff --git a/service-t/src/com/android/server/net/NetworkStatsRecorder.java b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
index d99e164..7c801d7 100644
--- a/service-t/src/com/android/server/net/NetworkStatsRecorder.java
+++ b/service-t/src/com/android/server/net/NetworkStatsRecorder.java
@@ -78,6 +78,7 @@
 
     private final long mBucketDuration;
     private final boolean mOnlyTags;
+    private final boolean mWipeOnError;
 
     private long mPersistThresholdBytes = 2 * MB_IN_BYTES;
     private NetworkStats mLastSnapshot;
@@ -102,6 +103,7 @@
         // slack to avoid overflow
         mBucketDuration = YEAR_IN_MILLIS;
         mOnlyTags = false;
+        mWipeOnError = true;
 
         mPending = null;
         mSinceBoot = new NetworkStatsCollection(mBucketDuration);
@@ -113,7 +115,8 @@
      * Persisted recorder.
      */
     public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer,
-            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) {
+            DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags,
+            boolean wipeOnError) {
         mRotator = Objects.requireNonNull(rotator, "missing FileRotator");
         mObserver = Objects.requireNonNull(observer, "missing NonMonotonicObserver");
         mDropBox = Objects.requireNonNull(dropBox, "missing DropBoxManager");
@@ -121,6 +124,7 @@
 
         mBucketDuration = bucketDuration;
         mOnlyTags = onlyTags;
+        mWipeOnError = wipeOnError;
 
         mPending = new NetworkStatsCollection(bucketDuration);
         mSinceBoot = new NetworkStatsCollection(bucketDuration);
@@ -552,7 +556,9 @@
             }
             mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0);
         }
-
-        mRotator.deleteAll();
+        // Delete all files if this recorder is set wipe on error.
+        if (mWipeOnError) {
+            mRotator.deleteAll();
+        }
     }
 }
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 06c8179..424dcd9 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -76,8 +76,10 @@
 import android.content.IntentFilter;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityResources;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsService;
@@ -140,6 +142,7 @@
 import android.util.SparseIntArray;
 import android.util.proto.ProtoOutputStream;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FileRotator;
@@ -765,6 +768,11 @@
                 return null;
             }
         }
+
+        /** Gets whether the build is userdebug. */
+        public boolean isDebuggable() {
+            return Build.isDebuggable();
+        }
     }
 
     /**
@@ -792,11 +800,14 @@
             mSystemReady = true;
 
             // create data recorders along with historical rotators
-            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir);
-            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir);
-            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir);
+            mDevRecorder = buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mXtRecorder = buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
+            mUidRecorder = buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, mStatsDir,
+                    true /* wipeOnError */);
             mUidTagRecorder = buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true,
-                    mStatsDir);
+                    mStatsDir, true /* wipeOnError */);
 
             updatePersistThresholdsLocked();
 
@@ -861,12 +872,13 @@
 
     private NetworkStatsRecorder buildRecorder(
             String prefix, NetworkStatsSettings.Config config, boolean includeTags,
-            File baseDir) {
+            File baseDir, boolean wipeOnError) {
         final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
                 Context.DROPBOX_SERVICE);
         return new NetworkStatsRecorder(new FileRotator(
                 baseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
-                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
+                mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags,
+                wipeOnError);
     }
 
     @GuardedBy("mStatsLock")
@@ -927,18 +939,27 @@
         final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
         final int attempts;
         final int fallbacks;
+        final boolean runComparison;
         try {
             attempts = mImportLegacyAttemptsCounter.get();
+            // Fallbacks counter would be set to non-zero value to indicate the migration was
+            // not successful.
             fallbacks = mImportLegacyFallbacksCounter.get();
+            runComparison = shouldRunComparison();
         } catch (IOException e) {
             Log.wtf(TAG, "Failed to read counters, skip.", e);
             return;
         }
-        // If fallbacks is not zero, proceed with reading only to give signals from dogfooders.
-        // TODO(b/233752318): Remove fallbacks counter check before T formal release.
-        if (attempts >= targetAttempts && fallbacks == 0) return;
 
-        final boolean dryRunImportOnly = (attempts >= targetAttempts);
+        // If the target number of attempts are reached, don't import any data.
+        // However, if comparison is requested, still read the legacy data and compare
+        // it to the importer output. This allows OEMs to debug issues with the
+        // importer code and to collect signals from the field.
+        final boolean dryRunImportOnly =
+                fallbacks != 0 && runComparison && (attempts >= targetAttempts);
+        // Return if target attempts are reached and there is no need to dry run.
+        if (attempts >= targetAttempts && !dryRunImportOnly) return;
+
         if (dryRunImportOnly) {
             Log.i(TAG, "Starting import : only perform read");
         } else {
@@ -951,69 +972,59 @@
         };
 
         // Legacy directories will be created by recorders if they do not exist
-        final File legacyBaseDir = mDeps.getLegacyStatsDir();
-        final NetworkStatsRecorder[] legacyRecorders = new NetworkStatsRecorder[]{
-                buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir),
-                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir),
-                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir),
-                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir)
-        };
+        final NetworkStatsRecorder[] legacyRecorders;
+        if (runComparison) {
+            final File legacyBaseDir = mDeps.getLegacyStatsDir();
+            // Set wipeOnError flag false so the recorder won't damage persistent data if reads
+            // failed and calling deleteAll.
+            legacyRecorders = new NetworkStatsRecorder[]{
+                buildRecorder(PREFIX_DEV, mSettings.getDevConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_XT, mSettings.getXtConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_UID, mSettings.getUidConfig(), false, legacyBaseDir,
+                        false /* wipeOnError */),
+                buildRecorder(PREFIX_UID_TAG, mSettings.getUidTagConfig(), true, legacyBaseDir,
+                        false /* wipeOnError */)};
+        } else {
+            legacyRecorders = null;
+        }
 
         long migrationEndTime = Long.MIN_VALUE;
-        boolean endedWithFallback = false;
         try {
             // First, read all legacy collections. This is OEM code and it can throw. Don't
             // commit any data to disk until all are read.
             for (int i = 0; i < migrations.length; i++) {
-                String errMsg = null;
-                Throwable exception = null;
                 final MigrationInfo migration = migrations[i];
 
-                // Read the collection from platform code, and using fallback method if throws.
+                // Read the collection from platform code, and set fallbacks counter if throws
+                // for better debugging.
                 try {
                     migration.collection = readPlatformCollectionForRecorder(migration.recorder);
                 } catch (Throwable e) {
-                    errMsg = "Failed to read stats from platform";
-                    exception = e;
-                }
-
-                // Also read the collection with legacy method
-                final NetworkStatsRecorder legacyRecorder = legacyRecorders[i];
-
-                final NetworkStatsCollection legacyStats;
-                try {
-                    legacyStats = legacyRecorder.getOrLoadCompleteLocked();
-                } catch (Throwable e) {
-                    Log.wtf(TAG, "Failed to read stats with legacy method for recorder " + i, e);
-                    if (exception != null) {
-                        throw exception;
+                    if (dryRunImportOnly) {
+                        Log.wtf(TAG, "Platform data read failed. ", e);
+                        return;
                     } else {
-                        // Use newer stats, since that's all that is available
-                        continue;
+                        // Data is not imported successfully, set fallbacks counter to non-zero
+                        // value to trigger dry run every later boot when the runComparison is
+                        // true, in order to make it easier to debug issues.
+                        tryIncrementLegacyFallbacksCounter();
+                        // Re-throw for error handling. This will increase attempts counter.
+                        throw e;
                     }
                 }
 
-                if (errMsg == null) {
-                    try {
-                        errMsg = compareStats(migration.collection, legacyStats);
-                    } catch (Throwable e) {
-                        errMsg = "Failed to compare migrated stats with all stats";
-                        exception = e;
+                if (runComparison) {
+                    final boolean success =
+                            compareImportedToLegacyStats(migration, legacyRecorders[i]);
+                    if (!success && !dryRunImportOnly) {
+                        tryIncrementLegacyFallbacksCounter();
                     }
                 }
-
-                if (errMsg != null) {
-                    Log.wtf(TAG, "NetworkStats import for migration " + i
-                            + " returned invalid data: " + errMsg, exception);
-                    // Fall back to legacy stats for this boot. The stats for old data will be
-                    // re-imported again on next boot until they succeed the import. This is fine
-                    // since every import clears the previous stats for the imported timespan.
-                    migration.collection = legacyStats;
-                    endedWithFallback = true;
-                }
             }
 
-            // For cases where the fallbacks is not zero but target attempts counts reached,
+            // For cases where the fallbacks are not zero but target attempts counts reached,
             // only perform reads above and return here.
             if (dryRunImportOnly) return;
 
@@ -1079,22 +1090,78 @@
         // Success ! No need to import again next time.
         try {
             mImportLegacyAttemptsCounter.set(targetAttempts);
-            if (endedWithFallback) {
-                Log.wtf(TAG, "Imported platform collections with legacy fallback");
-                final int fallbacksCount = mImportLegacyFallbacksCounter.get();
-                mImportLegacyFallbacksCounter.set(fallbacksCount + 1);
-            } else {
-                Log.i(TAG, "Successfully imported platform collections");
-                // The successes counter is only for debugging. Hence, the synchronization
-                // between successes counter and attempts counter are not very critical.
-                final int successCount = mImportLegacySuccessesCounter.get();
-                mImportLegacySuccessesCounter.set(successCount + 1);
-            }
+            Log.i(TAG, "Successfully imported platform collections");
+            // The successes counter is only for debugging. Hence, the synchronization
+            // between successes counter and attempts counter are not very critical.
+            final int successCount = mImportLegacySuccessesCounter.get();
+            mImportLegacySuccessesCounter.set(successCount + 1);
         } catch (IOException e) {
             Log.wtf(TAG, "Succeed but failed to update counters.", e);
         }
     }
 
+    void tryIncrementLegacyFallbacksCounter() {
+        try {
+            final int fallbacks = mImportLegacyFallbacksCounter.get();
+            mImportLegacyFallbacksCounter.set(fallbacks + 1);
+        } catch (IOException e) {
+            Log.wtf(TAG, "Failed to update fallback counter.", e);
+        }
+    }
+
+    @VisibleForTesting
+    boolean shouldRunComparison() {
+        final ConnectivityResources resources = new ConnectivityResources(mContext);
+        // 0 if id not found.
+        Boolean overlayValue = null;
+        try {
+            switch (resources.get().getInteger(R.integer.config_netstats_validate_import)) {
+                case 1:
+                    overlayValue = Boolean.TRUE;
+                    break;
+                case 0:
+                    overlayValue = Boolean.FALSE;
+                    break;
+            }
+        } catch (Resources.NotFoundException e) {
+            // Overlay value is not defined.
+        }
+        return overlayValue != null ? overlayValue : mDeps.isDebuggable();
+    }
+
+    /**
+     * Compare imported data with the data returned by legacy recorders.
+     *
+     * @return true if the data matches, false if the data does not match or throw with exceptions.
+     */
+    private boolean compareImportedToLegacyStats(@NonNull MigrationInfo migration,
+            @NonNull NetworkStatsRecorder legacyRecorder) {
+        final NetworkStatsCollection legacyStats;
+        try {
+            legacyStats = legacyRecorder.getOrLoadCompleteLocked();
+        } catch (Throwable e) {
+            Log.wtf(TAG, "Failed to read stats with legacy method for recorder "
+                    + legacyRecorder.getCookie(), e);
+            // Cannot read data from legacy method, skip comparison.
+            return false;
+        }
+
+        // The result of comparison is only for logging.
+        try {
+            final String error = compareStats(migration.collection, legacyStats);
+            if (error != null) {
+                Log.wtf(TAG, "Unexpected comparison result for recorder "
+                        + legacyRecorder.getCookie() + ": " + error);
+                return false;
+            }
+        } catch (Throwable e) {
+            Log.wtf(TAG, "Failed to compare migrated stats with legacy stats for recorder "
+                    + legacyRecorder.getCookie(), e);
+            return false;
+        }
+        return true;
+    }
+
     private static String str(NetworkStatsCollection.Key key) {
         StringBuilder sb = new StringBuilder()
                 .append(key.ident.toString())
diff --git a/service/ServiceConnectivityResources/res/values/config.xml b/service/ServiceConnectivityResources/res/values/config.xml
index 81782f9..bff6953 100644
--- a/service/ServiceConnectivityResources/res/values/config.xml
+++ b/service/ServiceConnectivityResources/res/values/config.xml
@@ -179,4 +179,13 @@
     Only supported up to S. On T+, the Wi-Fi code should use unregisterAfterReplacement in order
     to ensure that apps see the network disconnect and reconnect. -->
     <integer translatable="false" name="config_validationFailureAfterRoamIgnoreTimeMillis">-1</integer>
+
+    <!-- Whether the network stats service should run compare on the result of
+    {@link NetworkStatsDataMigrationUtils#readPlatformCollection} and the result
+    of reading from legacy recorders. Possible values are:
+      0 = never compare,
+      1 = always compare,
+      2 = compare on debuggable builds (default value)
+      -->
+    <integer translatable="false" name="config_netstats_validate_import">2</integer>
 </resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index b92dd08..3389d63 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -41,6 +41,7 @@
             <item type="array" name="config_ethernet_interfaces"/>
             <item type="string" name="config_ethernet_iface_regex"/>
             <item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
+            <item type="integer" name="config_netstats_validate_import" />
         </policy>
     </overlayable>
 </resources>
diff --git a/service/jarjar-excludes.txt b/service/jarjar-excludes.txt
new file mode 100644
index 0000000..b0d6763
--- /dev/null
+++ b/service/jarjar-excludes.txt
@@ -0,0 +1,9 @@
+# Classes loaded by SystemServer via their hardcoded name, so they can't be jarjared
+com\.android\.server\.ConnectivityServiceInitializer(\$.+)?
+com\.android\.server\.NetworkStatsServiceInitializer(\$.+)?
+
+# Do not jarjar com.android.server, as several unit tests fail because they lose
+# package-private visibility between jarjared and non-jarjared classes.
+# TODO: fix the tests and also jarjar com.android.server, or at least only exclude a package that
+# is specific to the module like com.android.server.connectivity
+com\.android\.server\..+
diff --git a/service/jni/com_android_server_BpfNetMaps.cpp b/service/jni/com_android_server_BpfNetMaps.cpp
index bc70c93..2780044 100644
--- a/service/jni/com_android_server_BpfNetMaps.cpp
+++ b/service/jni/com_android_server_BpfNetMaps.cpp
@@ -151,6 +151,12 @@
     return (jint)status.code();
 }
 
+static jint native_updateUidLockdownRule(JNIEnv* env, jobject self, jint uid, jboolean add) {
+    Status status = mTc.updateUidLockdownRule(uid, add);
+    CHECK_LOG(status);
+    return (jint)status.code();
+}
+
 static jint native_swapActiveStatsMap(JNIEnv* env, jobject self) {
     Status status = mTc.swapActiveStatsMap();
     CHECK_LOG(status);
@@ -203,6 +209,8 @@
     (void*)native_addUidInterfaceRules},
     {"native_removeUidInterfaceRules", "([I)I",
     (void*)native_removeUidInterfaceRules},
+    {"native_updateUidLockdownRule", "(IZ)I",
+    (void*)native_updateUidLockdownRule},
     {"native_swapActiveStatsMap", "()I",
     (void*)native_swapActiveStatsMap},
     {"native_setPermissionForUids", "(I[I)V",
diff --git a/service/jni/com_android_server_TestNetworkService.cpp b/service/jni/com_android_server_TestNetworkService.cpp
index 4efd0e1..9c7a761 100644
--- a/service/jni/com_android_server_TestNetworkService.cpp
+++ b/service/jni/com_android_server_TestNetworkService.cpp
@@ -51,7 +51,15 @@
     jniThrowException(env, "java/lang/IllegalStateException", msg.c_str());
 }
 
-static int createTunTapInterface(JNIEnv* env, bool isTun, const char* iface) {
+// enable or disable  carrier on tun / tap interface.
+static void setTunTapCarrierEnabledImpl(JNIEnv* env, const char* iface, int tunFd, bool enabled) {
+    uint32_t carrierOn = enabled;
+    if (ioctl(tunFd, TUNSETCARRIER, &carrierOn)) {
+        throwException(env, errno, "set carrier", iface);
+    }
+}
+
+static int createTunTapImpl(JNIEnv* env, bool isTun, bool hasCarrier, const char* iface) {
     base::unique_fd tun(open("/dev/tun", O_RDWR | O_NONBLOCK));
     ifreq ifr{};
 
@@ -63,6 +71,11 @@
         return -1;
     }
 
+    if (!hasCarrier) {
+        // disable carrier before setting IFF_UP
+        setTunTapCarrierEnabledImpl(env, iface, tun.get(), hasCarrier);
+    }
+
     // Activate interface using an unconnected datagram socket.
     base::unique_fd inet6CtrlSock(socket(AF_INET6, SOCK_DGRAM, 0));
     ifr.ifr_flags = IFF_UP;
@@ -79,23 +92,31 @@
 
 //------------------------------------------------------------------------------
 
-static jint create(JNIEnv* env, jobject /* thiz */, jboolean isTun, jstring jIface) {
+static void setTunTapCarrierEnabled(JNIEnv* env, jclass /* clazz */, jstring
+                                    jIface, jint tunFd, jboolean enabled) {
+    ScopedUtfChars iface(env, jIface);
+    if (!iface.c_str()) {
+        jniThrowNullPointerException(env, "iface");
+    }
+    setTunTapCarrierEnabledImpl(env, iface.c_str(), tunFd, enabled);
+}
+
+static jint createTunTap(JNIEnv* env, jclass /* clazz */, jboolean isTun,
+                             jboolean hasCarrier, jstring jIface) {
     ScopedUtfChars iface(env, jIface);
     if (!iface.c_str()) {
         jniThrowNullPointerException(env, "iface");
         return -1;
     }
 
-    int tun = createTunTapInterface(env, isTun, iface.c_str());
-
-    // Any exceptions will be thrown from the createTunTapInterface call
-    return tun;
+    return createTunTapImpl(env, isTun, hasCarrier, iface.c_str());
 }
 
 //------------------------------------------------------------------------------
 
 static const JNINativeMethod gMethods[] = {
-    {"jniCreateTunTap", "(ZLjava/lang/String;)I", (void*)create},
+    {"nativeSetTunTapCarrierEnabled", "(Ljava/lang/String;IZ)V", (void*)setTunTapCarrierEnabled},
+    {"nativeCreateTunTap", "(ZZLjava/lang/String;)I", (void*)createTunTap},
 };
 
 int register_com_android_server_TestNetworkService(JNIEnv* env) {
diff --git a/service/native/TrafficController.cpp b/service/native/TrafficController.cpp
index d05e6fa..4dc056d 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -56,7 +56,6 @@
 using bpf::BpfMap;
 using bpf::synchronizeKernelRCU;
 using netdutils::DumpWriter;
-using netdutils::getIfaceList;
 using netdutils::NetlinkListener;
 using netdutils::NetlinkListenerInterface;
 using netdutils::ScopedIndent;
@@ -111,14 +110,6 @@
     return matchType;
 }
 
-bool TrafficController::hasUpdateDeviceStatsPermission(uid_t uid) {
-    // This implementation is the same logic as method ActivityManager#checkComponentPermission.
-    // It implies that the calling uid can never be the same as PER_USER_RANGE.
-    uint32_t appId = uid % PER_USER_RANGE;
-    return ((appId == AID_ROOT) || (appId == AID_SYSTEM) ||
-            mPrivilegedUser.find(appId) != mPrivilegedUser.end());
-}
-
 const std::string UidPermissionTypeToString(int permission) {
     if (permission == INetd::PERMISSION_NONE) {
         return "PERMISSION_NONE";
@@ -198,16 +189,6 @@
 Status TrafficController::start() {
     RETURN_IF_NOT_OK(initMaps());
 
-    // Fetch the list of currently-existing interfaces. At this point NetlinkHandler is
-    // already running, so it will call addInterface() when any new interface appears.
-    // TODO: Clean-up addInterface() after interface monitoring is in
-    // NetworkStatsService.
-    std::map<std::string, uint32_t> ifacePairs;
-    ASSIGN_OR_RETURN(ifacePairs, getIfaceList());
-    for (const auto& ifacePair:ifacePairs) {
-        addInterface(ifacePair.first.c_str(), ifacePair.second);
-    }
-
     auto result = makeSkDestroyListener();
     if (!isOk(result)) {
         ALOGE("Unable to create SkDestroyListener: %s", toString(result).c_str());
@@ -245,22 +226,6 @@
     return netdutils::status::ok;
 }
 
-int TrafficController::addInterface(const char* name, uint32_t ifaceIndex) {
-    IfaceValue iface;
-    if (ifaceIndex == 0) {
-        ALOGE("Unknown interface %s(%d)", name, ifaceIndex);
-        return -1;
-    }
-
-    strlcpy(iface.name, name, sizeof(IfaceValue));
-    Status res = mIfaceIndexNameMap.writeValue(ifaceIndex, iface, BPF_ANY);
-    if (!isOk(res)) {
-        ALOGE("Failed to add iface %s(%d): %s", name, ifaceIndex, strerror(res.code()));
-        return -res.code();
-    }
-    return 0;
-}
-
 Status TrafficController::updateOwnerMapEntry(UidOwnerMatchType match, uid_t uid, FirewallRule rule,
                                               FirewallType type) {
     std::lock_guard guard(mMutex);
@@ -340,8 +305,6 @@
             return ALLOWLIST;
         case LOW_POWER_STANDBY:
             return ALLOWLIST;
-        case LOCKDOWN:
-            return DENYLIST;
         case OEM_DENY_1:
             return DENYLIST;
         case OEM_DENY_2:
@@ -373,9 +336,6 @@
         case LOW_POWER_STANDBY:
             res = updateOwnerMapEntry(LOW_POWER_STANDBY_MATCH, uid, rule, type);
             break;
-        case LOCKDOWN:
-            res = updateOwnerMapEntry(LOCKDOWN_VPN_MATCH, uid, rule, type);
-            break;
         case OEM_DENY_1:
             res = updateOwnerMapEntry(OEM_DENY_1_MATCH, uid, rule, type);
             break;
@@ -447,6 +407,18 @@
     return netdutils::status::ok;
 }
 
+Status TrafficController::updateUidLockdownRule(const uid_t uid, const bool add) {
+    std::lock_guard guard(mMutex);
+
+    netdutils::Status result = add ? addRule(uid, LOCKDOWN_VPN_MATCH)
+                               : removeRule(uid, LOCKDOWN_VPN_MATCH);
+    if (!isOk(result)) {
+        ALOGW("%s Lockdown rule failed(%d): uid=%d",
+              (add ? "add": "remove"), result.code(), uid);
+    }
+    return result;
+}
+
 int TrafficController::replaceUidOwnerMap(const std::string& name, bool isAllowlist __unused,
                                           const std::vector<int32_t>& uids) {
     // FirewallRule rule = isAllowlist ? ALLOW : DENY;
@@ -488,8 +460,6 @@
               oldConfigure.error().message().c_str());
         return -oldConfigure.error().code();
     }
-    Status res;
-    BpfConfig newConfiguration;
     uint32_t match;
     switch (chain) {
         case DOZABLE:
@@ -519,9 +489,9 @@
         default:
             return -EINVAL;
     }
-    newConfiguration =
-            enable ? (oldConfigure.value() | match) : (oldConfigure.value() & (~match));
-    res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
+    BpfConfig newConfiguration =
+            enable ? (oldConfigure.value() | match) : (oldConfigure.value() & ~match);
+    Status res = mConfigurationMap.writeValue(key, newConfiguration, BPF_EXIST);
     if (!isOk(res)) {
         ALOGE("Failed to toggleUidOwnerMap(%d): %s", chain, res.msg().c_str());
     }
diff --git a/service/native/TrafficControllerTest.cpp b/service/native/TrafficControllerTest.cpp
index 0134dea..7730c13 100644
--- a/service/native/TrafficControllerTest.cpp
+++ b/service/native/TrafficControllerTest.cpp
@@ -38,7 +38,7 @@
 
 #include <netdutils/MockSyscalls.h>
 
-#define TEST_BPF_MAP
+#define BPF_MAP_MAKE_VISIBLE_FOR_TESTING
 #include "TrafficController.h"
 #include "bpf/BpfUtils.h"
 #include "NetdUpdatablePublic.h"
@@ -98,7 +98,7 @@
         mFakeStatsMapA.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
         ASSERT_VALID(mFakeStatsMapA);
 
-        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_HASH, 1);
+        mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_ARRAY, CONFIGURATION_MAP_SIZE);
         ASSERT_VALID(mFakeConfigurationMap);
 
         mFakeUidOwnerMap.resetMap(BPF_MAP_TYPE_HASH, TEST_MAP_SIZE);
@@ -122,8 +122,8 @@
         ASSERT_VALID(mTc.mConfigurationMap);
 
         // Always write to stats map A by default.
-        ASSERT_RESULT_OK(mTc.mConfigurationMap.writeValue(CURRENT_STATS_MAP_CONFIGURATION_KEY,
-                                                          SELECT_MAP_A, BPF_ANY));
+        static_assert(SELECT_MAP_A == 0);
+
         mTc.mUidOwnerMap = mFakeUidOwnerMap;
         ASSERT_VALID(mTc.mUidOwnerMap);
         mTc.mUidPermissionMap = mFakeUidPermissionMap;
@@ -218,7 +218,7 @@
         checkEachUidValue(uids, match);
     }
 
-    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint8_t expectedRule,
+    void expectUidOwnerMapValues(const std::vector<uint32_t>& appUids, uint32_t expectedRule,
                                  uint32_t expectedIif) {
         for (uint32_t uid : appUids) {
             Result<UidOwnerValue> value = mFakeUidOwnerMap.readValue(uid);
@@ -407,7 +407,6 @@
     checkUidOwnerRuleForChain(POWERSAVE, POWERSAVE_MATCH);
     checkUidOwnerRuleForChain(RESTRICTED, RESTRICTED_MATCH);
     checkUidOwnerRuleForChain(LOW_POWER_STANDBY, LOW_POWER_STANDBY_MATCH);
-    checkUidOwnerRuleForChain(LOCKDOWN, LOCKDOWN_VPN_MATCH);
     checkUidOwnerRuleForChain(OEM_DENY_1, OEM_DENY_1_MATCH);
     checkUidOwnerRuleForChain(OEM_DENY_2, OEM_DENY_2_MATCH);
     checkUidOwnerRuleForChain(OEM_DENY_3, OEM_DENY_3_MATCH);
@@ -539,6 +538,21 @@
     expectMapEmpty(mFakeUidOwnerMap);
 }
 
+TEST_F(TrafficControllerTest, TestUpdateUidLockdownRule) {
+    // Add Lockdown rules
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, true /* add */)));
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, true /* add */)));
+    expectUidOwnerMapValues({1000, 1001}, LOCKDOWN_VPN_MATCH, 0);
+
+    // Remove one of Lockdown rules
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1000, false /* add */)));
+    expectUidOwnerMapValues({1001}, LOCKDOWN_VPN_MATCH, 0);
+
+    // Remove remaining Lockdown rule
+    ASSERT_TRUE(isOk(mTc.updateUidLockdownRule(1001, false /* add */)));
+    expectMapEmpty(mFakeUidOwnerMap);
+}
+
 TEST_F(TrafficControllerTest, TestUidInterfaceFilteringRulesCoexistWithExistingMatches) {
     // Set up existing PENALTY_BOX_MATCH rules
     ASSERT_TRUE(isOk(updateUidOwnerMaps({1000, 1001, 10012}, PENALTY_BOX_MATCH,
@@ -885,7 +899,6 @@
             {POWERSAVE, ALLOWLIST},
             {RESTRICTED, ALLOWLIST},
             {LOW_POWER_STANDBY, ALLOWLIST},
-            {LOCKDOWN, DENYLIST},
             {OEM_DENY_1, DENYLIST},
             {OEM_DENY_2, DENYLIST},
             {OEM_DENY_3, DENYLIST},
diff --git a/service/native/include/Common.h b/service/native/include/Common.h
index 2427aa9..03f449a 100644
--- a/service/native/include/Common.h
+++ b/service/native/include/Common.h
@@ -17,9 +17,12 @@
 #pragma once
 // TODO: deduplicate with the constants in NetdConstants.h.
 #include <aidl/android/net/INetd.h>
+#include "clat_mark.h"
 
 using aidl::android::net::INetd;
 
+static_assert(INetd::CLAT_MARK == CLAT_MARK, "must be 0xDEADC1A7");
+
 enum FirewallRule { ALLOW = INetd::FIREWALL_RULE_ALLOW, DENY = INetd::FIREWALL_RULE_DENY };
 
 // ALLOWLIST means the firewall denies all by default, uids must be explicitly ALLOWed
@@ -35,7 +38,6 @@
     POWERSAVE = 3,
     RESTRICTED = 4,
     LOW_POWER_STANDBY = 5,
-    LOCKDOWN = 6,
     OEM_DENY_1 = 7,
     OEM_DENY_2 = 8,
     OEM_DENY_3 = 9,
diff --git a/service/native/include/TrafficController.h b/service/native/include/TrafficController.h
index c019ce7..8512929 100644
--- a/service/native/include/TrafficController.h
+++ b/service/native/include/TrafficController.h
@@ -45,11 +45,6 @@
      */
     netdutils::Status swapActiveStatsMap() EXCLUDES(mMutex);
 
-    /*
-     * Add the interface name and index pair into the eBPF map.
-     */
-    int addInterface(const char* name, uint32_t ifaceIndex);
-
     int changeUidOwnerRule(ChildChain chain, const uid_t uid, FirewallRule rule, FirewallType type);
 
     int removeUidOwnerRule(const uid_t uid);
@@ -71,6 +66,8 @@
             EXCLUDES(mMutex);
     netdutils::Status removeUidInterfaceRules(const std::vector<int32_t>& uids) EXCLUDES(mMutex);
 
+    netdutils::Status updateUidLockdownRule(const uid_t uid, const bool add) EXCLUDES(mMutex);
+
     netdutils::Status updateUidOwnerMap(const uint32_t uid,
                                         UidOwnerMatchType matchType, IptOp op) EXCLUDES(mMutex);
 
@@ -185,8 +182,6 @@
     // need to call back to system server for permission check.
     std::set<uid_t> mPrivilegedUser GUARDED_BY(mMutex);
 
-    bool hasUpdateDeviceStatsPermission(uid_t uid) REQUIRES(mMutex);
-
     // For testing
     friend class TrafficControllerTest;
 };
diff --git a/service/src/com/android/server/BpfNetMaps.java b/service/src/com/android/server/BpfNetMaps.java
index c006bc6..151d0e3 100644
--- a/service/src/com/android/server/BpfNetMaps.java
+++ b/service/src/com/android/server/BpfNetMaps.java
@@ -216,6 +216,19 @@
     }
 
     /**
+     * Update lockdown rule for uid
+     *
+     * @param  uid          target uid to add/remove the rule
+     * @param  add          {@code true} to add the rule, {@code false} to remove the rule.
+     * @throws ServiceSpecificException in case of failure, with an error code indicating the
+     *                                  cause of the failure.
+     */
+    public void updateUidLockdownRule(final int uid, final boolean add) {
+        final int err = native_updateUidLockdownRule(uid, add);
+        maybeThrow(err, "Unable to update lockdown rule");
+    }
+
+    /**
      * Request netd to change the current active network stats map.
      *
      * @throws ServiceSpecificException in case of failure, with an error code indicating the
@@ -271,6 +284,7 @@
     private native int native_setUidRule(int childChain, int uid, int firewallRule);
     private native int native_addUidInterfaceRules(String ifName, int[] uids);
     private native int native_removeUidInterfaceRules(int[] uids);
+    private native int native_updateUidLockdownRule(int uid, boolean add);
     private native int native_swapActiveStatsMap();
     private native void native_setPermissionForUids(int permissions, int[] uids);
     private native void native_dump(FileDescriptor fd, boolean verbose);
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 3dbf678..d734029 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -7766,10 +7766,6 @@
         // when the old rules are removed and the time when new rules are added. To fix this,
         // make eBPF support two allowlisted interfaces so here new rules can be added before the
         // old rules are being removed.
-
-        // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to receive
-        // packets on all interfaces. This is required to accept incoming traffic in Lockdown mode
-        // by overriding the Lockdown blocking rule.
         if (wasFiltering) {
             mPermissionMonitor.onVpnUidRangesRemoved(oldIface, ranges, vpnAppUid);
         }
@@ -8095,12 +8091,14 @@
      * Returns whether we need to set interface filtering rule or not
      */
     private boolean requiresVpnAllowRule(NetworkAgentInfo nai, LinkProperties lp,
-            String filterIface) {
-        // Only filter if lp has an interface.
-        if (lp == null || lp.getInterfaceName() == null) return false;
-        // Before T, allow rules are only needed if VPN isolation is enabled.
-        // T and After T, allow rules are needed for all VPNs.
-        return filterIface != null || (nai.isVPN() && SdkLevel.isAtLeastT());
+            String isolationIface) {
+        // Allow rules are always needed if VPN isolation is enabled.
+        if (isolationIface != null) return true;
+
+        // On T and above, allow rules are needed for all VPNs. Allow rule with null iface is a
+        // wildcard to allow apps to receive packets on all interfaces. This is required to accept
+        // incoming traffic in Lockdown mode by overriding the Lockdown blocking rule.
+        return SdkLevel.isAtLeastT() && nai.isVPN() && lp != null && lp.getInterfaceName() != null;
     }
 
     private static UidRangeParcel[] toUidRangeStableParcels(final @NonNull Set<UidRange> ranges) {
@@ -8243,10 +8241,6 @@
             // above, where the addition of new ranges happens before the removal of old ranges.
             // TODO Fix this window by computing an accurate diff on Set<UidRange>, so the old range
             // to be removed will never overlap with the new range to be added.
-
-            // Null iface given to onVpnUidRangesAdded/Removed is a wildcard to allow apps to
-            // receive packets on all interfaces. This is required to accept incoming traffic in
-            // Lockdown mode by overriding the Lockdown blocking rule.
             if (wasFiltering && !prevRanges.isEmpty()) {
                 mPermissionMonitor.onVpnUidRangesRemoved(oldIface, prevRanges,
                         prevNc.getOwnerUid());
diff --git a/service/src/com/android/server/TestNetworkService.java b/service/src/com/android/server/TestNetworkService.java
index e12190c..1209579 100644
--- a/service/src/com/android/server/TestNetworkService.java
+++ b/service/src/com/android/server/TestNetworkService.java
@@ -50,6 +50,7 @@
 import com.android.net.module.util.NetdUtils;
 import com.android.net.module.util.NetworkStackConstants;
 
+import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.net.Inet4Address;
 import java.net.Inet6Address;
@@ -76,7 +77,11 @@
     @NonNull private final NetworkProvider mNetworkProvider;
 
     // Native method stubs
-    private static native int jniCreateTunTap(boolean isTun, @NonNull String iface);
+    private static native int nativeCreateTunTap(boolean isTun, boolean hasCarrier,
+            @NonNull String iface);
+
+    private static native void nativeSetTunTapCarrierEnabled(@NonNull String iface, int tunFd,
+            boolean enabled);
 
     @VisibleForTesting
     protected TestNetworkService(@NonNull Context context) {
@@ -114,7 +119,7 @@
      * interface.
      */
     @Override
-    public TestNetworkInterface createInterface(boolean isTun, boolean bringUp,
+    public TestNetworkInterface createInterface(boolean isTun, boolean hasCarrier, boolean bringUp,
             LinkAddress[] linkAddrs, @Nullable String iface) {
         enforceTestNetworkPermissions(mContext);
 
@@ -130,8 +135,8 @@
 
         final long token = Binder.clearCallingIdentity();
         try {
-            ParcelFileDescriptor tunIntf =
-                    ParcelFileDescriptor.adoptFd(jniCreateTunTap(isTun, interfaceName));
+            ParcelFileDescriptor tunIntf = ParcelFileDescriptor.adoptFd(
+                    nativeCreateTunTap(isTun, hasCarrier, interfaceName));
             for (LinkAddress addr : linkAddrs) {
                 mNetd.interfaceAddAddress(
                         interfaceName,
@@ -375,4 +380,20 @@
     public static void enforceTestNetworkPermissions(@NonNull Context context) {
         context.enforceCallingOrSelfPermission(PERMISSION_NAME, "TestNetworkService");
     }
+
+    /** Enable / disable TestNetworkInterface carrier */
+    @Override
+    public void setCarrierEnabled(@NonNull TestNetworkInterface iface, boolean enabled) {
+        enforceTestNetworkPermissions(mContext);
+        nativeSetTunTapCarrierEnabled(iface.getInterfaceName(), iface.getFileDescriptor().getFd(),
+                enabled);
+        // Explicitly close fd after use to prevent StrictMode from complaining.
+        // Also, explicitly referencing iface guarantees that the object is not garbage collected
+        // before nativeSetTunTapCarrierEnabled() executes.
+        try {
+            iface.getFileDescriptor().close();
+        } catch (IOException e) {
+            // if the close fails, there is not much that can be done -- move on.
+        }
+    }
 }
diff --git a/service/src/com/android/server/connectivity/PermissionMonitor.java b/service/src/com/android/server/connectivity/PermissionMonitor.java
index e4a2c20..dedeb38 100755
--- a/service/src/com/android/server/connectivity/PermissionMonitor.java
+++ b/service/src/com/android/server/connectivity/PermissionMonitor.java
@@ -23,9 +23,6 @@
 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
-import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -684,8 +681,12 @@
     }
 
     private synchronized void updateLockdownUid(int uid, boolean add) {
-        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)
-                && !hasRestrictedNetworksPermission(uid)) {
+        // Apps that can use restricted networks can always bypass VPNs.
+        if (hasRestrictedNetworksPermission(uid)) {
+            return;
+        }
+
+        if (UidRange.containsUid(mVpnLockdownUidRanges.getSet(), uid)) {
             updateLockdownUidRule(uid, add);
         }
     }
@@ -1079,11 +1080,7 @@
 
     private void updateLockdownUidRule(int uid, boolean add) {
         try {
-            if (add) {
-                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_DENY);
-            } else {
-                mBpfNetMaps.setUidRule(FIREWALL_CHAIN_LOCKDOWN_VPN, uid, FIREWALL_RULE_ALLOW);
-            }
+            mBpfNetMaps.updateUidLockdownRule(uid, add);
         } catch (ServiceSpecificException e) {
             loge("Failed to " + (add ? "add" : "remove") + " Lockdown rule: " + e);
         }
@@ -1259,7 +1256,7 @@
         pw.println("Lockdown filtering rules:");
         pw.increaseIndent();
         for (final UidRange range : mVpnLockdownUidRanges.getSet()) {
-            pw.println("UIDs: " + range.toString());
+            pw.println("UIDs: " + range);
         }
         pw.decreaseIndent();
 
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
index cc07fd1..d0567ae 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideNetworkTestCase.java
@@ -34,8 +34,6 @@
 
 import java.io.FileNotFoundException;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 abstract class HostsideNetworkTestCase extends DeviceTestCase implements IAbiReceiver,
         IBuildReceiver {
@@ -171,18 +169,19 @@
         }
     }
 
-    private static final Pattern UID_PATTERN =
-            Pattern.compile(".*userId=([0-9]+)$", Pattern.MULTILINE);
-
     protected int getUid(String packageName) throws DeviceNotAvailableException {
-        final String output = runCommand("dumpsys package " + packageName);
-        final Matcher matcher = UID_PATTERN.matcher(output);
-        while (matcher.find()) {
-            final String match = matcher.group(1);
-            return Integer.parseInt(match);
+        final int currentUser = getDevice().getCurrentUser();
+        final String uidLines = runCommand(
+                "cmd package list packages -U --user " + currentUser + " " + packageName);
+        for (String uidLine : uidLines.split("\n")) {
+            if (uidLine.startsWith("package:" + packageName + " uid:")) {
+                final String[] uidLineParts = uidLine.split(":");
+                // 3rd entry is package uid
+                return Integer.parseInt(uidLineParts[2].trim());
+            }
         }
-        throw new RuntimeException("Did not find regexp '" + UID_PATTERN + "' on adb output\n"
-                + output);
+        throw new IllegalStateException("Failed to find the test app on the device; pkg="
+                + packageName + ", u=" + currentUser);
     }
 
     protected String runCommand(String command) throws DeviceNotAvailableException {
diff --git a/tests/cts/net/native/src/BpfCompatTest.cpp b/tests/cts/net/native/src/BpfCompatTest.cpp
index 97ecb9e..e52533b 100644
--- a/tests/cts/net/native/src/BpfCompatTest.cpp
+++ b/tests/cts/net/native/src/BpfCompatTest.cpp
@@ -31,8 +31,13 @@
   std::ifstream elfFile(elfPath, std::ios::in | std::ios::binary);
   ASSERT_TRUE(elfFile.is_open());
 
-  EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
-  EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  if (android::modules::sdklevel::IsAtLeastT()) {
+    EXPECT_EQ(116, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(92, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  } else {
+    EXPECT_EQ(48, readSectionUint("size_of_bpf_map_def", elfFile, 0));
+    EXPECT_EQ(28, readSectionUint("size_of_bpf_prog_def", elfFile, 0));
+  }
 }
 
 TEST(BpfTest, bpfStructSizeTestPreT) {
diff --git a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
index 0344604..1b77d5f 100644
--- a/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/cts/net/src/android/net/cts/CaptivePortalTest.kt
@@ -33,7 +33,6 @@
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
 import android.net.Uri
-import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
 import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
@@ -60,6 +59,8 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Assume.assumeFalse
 import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
 import org.junit.runner.RunWith
 import java.util.concurrent.CompletableFuture
 import java.util.concurrent.TimeUnit
@@ -99,34 +100,42 @@
 
     private val server = TestHttpServer("localhost")
 
+    @get:Rule
+    val deviceConfigRule = DeviceConfigRule(retryCountBeforeSIfConfigChanged = 5)
+
+    companion object {
+        @JvmStatic @BeforeClass
+        fun setUpClass() {
+            runAsShell(READ_DEVICE_CONFIG) {
+                // Verify that the test URLs are not normally set on the device, but do not fail if
+                // the test URLs are set to what this test uses (URLs on localhost), in case the
+                // test was interrupted manually and rerun.
+                assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+                assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
+            }
+            NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig()
+        }
+
+        private fun assertEmptyOrLocalhostUrl(urlKey: String) {
+            val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
+            assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
+                    "$urlKey must not be set in production scenarios (current value: $url)")
+        }
+    }
+
     @Before
     fun setUp() {
-        runAsShell(READ_DEVICE_CONFIG) {
-            // Verify that the test URLs are not normally set on the device, but do not fail if the
-            // test URLs are set to what this test uses (URLs on localhost), in case the test was
-            // interrupted manually and rerun.
-            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
-            assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
-        }
-        clearValidationTestUrlsDeviceConfig()
         server.start()
     }
 
     @After
     fun tearDown() {
-        clearValidationTestUrlsDeviceConfig()
         if (pm.hasSystemFeature(FEATURE_WIFI)) {
-            reconnectWifi()
+            deviceConfigRule.runAfterNextCleanup { reconnectWifi() }
         }
         server.stop()
     }
 
-    private fun assertEmptyOrLocalhostUrl(urlKey: String) {
-        val url = DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, urlKey)
-        assertTrue(TextUtils.isEmpty(url) || LOCALHOST_HOSTNAME == Uri.parse(url).host,
-                "$urlKey must not be set in production scenarios (current value: $url)")
-    }
-
     @Test
     fun testCaptivePortalIsNotDefaultNetwork() {
         assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
@@ -154,12 +163,13 @@
         server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
         val headers = mapOf("Location" to makeUrl(TEST_PORTAL_URL_PATH))
         server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT, headers)
-        setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
-        setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
+        setHttpsUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTPS_URL_PATH))
+        setHttpUrlDeviceConfig(deviceConfigRule, makeUrl(TEST_HTTP_URL_PATH))
         Log.d(TAG, "Set portal URLs to $TEST_HTTPS_URL_PATH and $TEST_HTTP_URL_PATH")
         // URL expiration needs to be in the next 10 minutes
         assertTrue(WIFI_CONNECT_TIMEOUT_MS < TimeUnit.MINUTES.toMillis(10))
-        setUrlExpirationDeviceConfig(System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
+        setUrlExpirationDeviceConfig(deviceConfigRule,
+                System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS)
 
         // Wait for a captive portal to be detected on the network
         val wifiNetworkFuture = CompletableFuture<Network>()
@@ -215,4 +225,4 @@
         utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
         utils.ensureWifiConnected()
     }
-}
\ No newline at end of file
+}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 08cf0d7..da2e594 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -249,6 +249,10 @@
     @Rule
     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
 
+    @Rule
+    public final DeviceConfigRule mTestValidationConfigRule = new DeviceConfigRule(
+            5 /* retryCountBeforeSIfConfigChanged */);
+
     private static final String TAG = ConnectivityManagerTest.class.getSimpleName();
 
     public static final int TYPE_MOBILE = ConnectivityManager.TYPE_MOBILE;
@@ -2765,9 +2769,8 @@
             // Accept partial connectivity network should result in a validated network
             expectNetworkHasCapability(network, NET_CAPABILITY_VALIDATED, WIFI_CONNECT_TIMEOUT_MS);
         } finally {
-            resetValidationConfig();
-            // Reconnect wifi to reset the wifi status
-            reconnectWifi();
+            mHttpServer.stop();
+            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
         }
     }
 
@@ -2792,11 +2795,13 @@
             // Reject partial connectivity network should cause the network being torn down
             assertEquals(network, cb.waitForLost());
         } finally {
-            resetValidationConfig();
+            mHttpServer.stop();
             // Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
             // apply here. Thus, turn off wifi first and restart to restore.
-            runShellCommand("svc wifi disable");
-            mCtsNetUtils.ensureWifiConnected();
+            mTestValidationConfigRule.runAfterNextCleanup(() -> {
+                runShellCommand("svc wifi disable");
+                mCtsNetUtils.ensureWifiConnected();
+            });
         }
     }
 
@@ -2832,11 +2837,13 @@
             });
             waitForLost(wifiCb);
         } finally {
-            resetValidationConfig();
+            mHttpServer.stop();
             /// Wifi will not automatically reconnect to the network. ensureWifiDisconnected cannot
             // apply here. Thus, turn off wifi first and restart to restore.
-            runShellCommand("svc wifi disable");
-            mCtsNetUtils.ensureWifiConnected();
+            mTestValidationConfigRule.runAfterNextCleanup(() -> {
+                runShellCommand("svc wifi disable");
+                mCtsNetUtils.ensureWifiConnected();
+            });
         }
     }
 
@@ -2896,9 +2903,8 @@
             wifiCb.assertNoCallbackThat(NO_CALLBACK_TIMEOUT_MS, c -> isValidatedCaps(c));
         } finally {
             resetAvoidBadWifi(previousAvoidBadWifi);
-            resetValidationConfig();
-            // Reconnect wifi to reset the wifi status
-            reconnectWifi();
+            mHttpServer.stop();
+            mTestValidationConfigRule.runAfterNextCleanup(this::reconnectWifi);
         }
     }
 
@@ -2942,11 +2948,6 @@
         return future.get(timeout, TimeUnit.MILLISECONDS);
     }
 
-    private void resetValidationConfig() {
-        NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig();
-        mHttpServer.stop();
-    }
-
     private void prepareHttpServer() throws Exception {
         runAsShell(READ_DEVICE_CONFIG, () -> {
             // Verify that the test URLs are not normally set on the device, but do not fail if the
@@ -3019,9 +3020,11 @@
         mHttpServer.addResponse(new TestHttpServer.Request(
                 TEST_HTTP_URL_PATH, Method.GET, "" /* queryParameters */),
                 httpStatusCode, null /* locationHeader */, "" /* content */);
-        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH));
-        NetworkValidationTestUtil.setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH));
-        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(
+        NetworkValidationTestUtil.setHttpsUrlDeviceConfig(mTestValidationConfigRule,
+                makeUrl(TEST_HTTPS_URL_PATH));
+        NetworkValidationTestUtil.setHttpUrlDeviceConfig(mTestValidationConfigRule,
+                makeUrl(TEST_HTTP_URL_PATH));
+        NetworkValidationTestUtil.setUrlExpirationDeviceConfig(mTestValidationConfigRule,
                 System.currentTimeMillis() + WIFI_CONNECT_TIMEOUT_MS);
     }
 
diff --git a/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
new file mode 100644
index 0000000..d31a4e0
--- /dev/null
+++ b/tests/cts/net/src/android/net/cts/DeviceConfigRule.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.cts
+
+import android.Manifest.permission.READ_DEVICE_CONFIG
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
+import android.provider.DeviceConfig
+import android.util.Log
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.runAsShell
+import com.android.testutils.tryTest
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+private val TAG = DeviceConfigRule::class.simpleName
+
+/**
+ * A [TestRule] that helps set [DeviceConfig] for tests and clean up the test configuration
+ * automatically on teardown.
+ *
+ * The rule can also optionally retry tests when they fail following an external change of
+ * DeviceConfig before S; this typically happens because device config flags are synced while the
+ * test is running, and DisableConfigSyncTargetPreparer is only usable starting from S.
+ *
+ * @param retryCountBeforeSIfConfigChanged if > 0, when the test fails before S, check if
+ *        the configs that were set through this rule were changed, and retry the test
+ *        up to the specified number of times if yes.
+ */
+class DeviceConfigRule @JvmOverloads constructor(
+    val retryCountBeforeSIfConfigChanged: Int = 0
+) : TestRule {
+    // Maps (namespace, key) -> value
+    private val originalConfig = mutableMapOf<Pair<String, String>, String?>()
+    private val usedConfig = mutableMapOf<Pair<String, String>, String?>()
+
+    /**
+     * Actions to be run after cleanup of the config, for the current test only.
+     */
+    private val currentTestCleanupActions = mutableListOf<Runnable>()
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return TestValidationUrlStatement(base, description)
+    }
+
+    private inner class TestValidationUrlStatement(
+        private val base: Statement,
+        private val description: Description
+    ) : Statement() {
+        override fun evaluate() {
+            var retryCount = if (SdkLevel.isAtLeastS()) 1 else retryCountBeforeSIfConfigChanged + 1
+            while (retryCount > 0) {
+                retryCount--
+                tryTest {
+                    base.evaluate()
+                    // Can't use break/return out of a loop here because this is a tryTest lambda,
+                    // so set retryCount to exit instead
+                    retryCount = 0
+                }.catch<Throwable> { e -> // junit AssertionFailedError does not extend Exception
+                    if (retryCount == 0) throw e
+                    usedConfig.forEach { (key, value) ->
+                        val currentValue = runAsShell(READ_DEVICE_CONFIG) {
+                            DeviceConfig.getProperty(key.first, key.second)
+                        }
+                        if (currentValue != value) {
+                            Log.w(TAG, "Test failed with unexpected device config change, retrying")
+                            return@catch
+                        }
+                    }
+                    throw e
+                } cleanupStep {
+                    runAsShell(WRITE_DEVICE_CONFIG) {
+                        originalConfig.forEach { (key, value) ->
+                            DeviceConfig.setProperty(
+                                    key.first, key.second, value, false /* makeDefault */)
+                        }
+                    }
+                } cleanupStep {
+                    originalConfig.clear()
+                    usedConfig.clear()
+                } cleanup {
+                    currentTestCleanupActions.forEach { it.run() }
+                    currentTestCleanupActions.clear()
+                }
+            }
+        }
+    }
+
+    /**
+     * Set a configuration key/value. After the test case ends, it will be restored to the value it
+     * had when this method was first called.
+     */
+    fun setConfig(namespace: String, key: String, value: String?) {
+        runAsShell(READ_DEVICE_CONFIG, WRITE_DEVICE_CONFIG) {
+            val keyPair = Pair(namespace, key)
+            if (!originalConfig.containsKey(keyPair)) {
+                originalConfig[keyPair] = DeviceConfig.getProperty(namespace, key)
+            }
+            usedConfig[keyPair] = value
+            DeviceConfig.setProperty(namespace, key, value, false /* makeDefault */)
+        }
+    }
+
+    /**
+     * Add an action to be run after config cleanup when the current test case ends.
+     */
+    fun runAfterNextCleanup(action: Runnable) {
+        currentTestCleanupActions.add(action)
+    }
+}
diff --git a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
index db24b44..54d6818 100644
--- a/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/EthernetManagerTest.kt
@@ -30,11 +30,14 @@
 import android.net.EthernetManager.STATE_LINK_UP
 import android.net.EthernetManager.TetheredInterfaceCallback
 import android.net.EthernetManager.TetheredInterfaceRequest
+import android.net.EthernetNetworkManagementException
 import android.net.EthernetNetworkSpecifier
+import android.net.EthernetNetworkUpdateRequest
 import android.net.InetAddresses
 import android.net.IpConfiguration
 import android.net.MacAddress
 import android.net.Network
+import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED
 import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
 import android.net.NetworkCapabilities.TRANSPORT_TEST
@@ -44,8 +47,8 @@
 import android.net.cts.EthernetManagerTest.EthernetStateListener.CallbackEntry.InterfaceStateChanged
 import android.os.Build
 import android.os.Handler
-import android.os.HandlerExecutor
 import android.os.Looper
+import android.os.OutcomeReceiver
 import android.os.SystemProperties
 import android.platform.test.annotations.AppModeFull
 import android.util.ArraySet
@@ -53,6 +56,7 @@
 import com.android.net.module.util.ArrayTrackRecord
 import com.android.net.module.util.TrackRecord
 import com.android.testutils.anyNetwork
+import com.android.testutils.ConnectivityModuleTest
 import com.android.testutils.DevSdkIgnoreRule
 import com.android.testutils.DevSdkIgnoreRunner
 import com.android.testutils.RecorderCallback.CallbackEntry.Available
@@ -94,35 +98,42 @@
 @AppModeFull(reason = "Instant apps can't access EthernetManager")
 // EthernetManager is not updatable before T, so tests do not need to be backwards compatible.
 @RunWith(DevSdkIgnoreRunner::class)
+// This test depends on behavior introduced post-T as part of connectivity module updates
+@ConnectivityModuleTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
 class EthernetManagerTest {
 
     private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
     private val em by lazy { context.getSystemService(EthernetManager::class.java) }
     private val cm by lazy { context.getSystemService(ConnectivityManager::class.java) }
+    private val handler by lazy { Handler(Looper.getMainLooper()) }
 
     private val ifaceListener = EthernetStateListener()
     private val createdIfaces = ArrayList<EthernetTestInterface>()
     private val addedListeners = ArrayList<EthernetStateListener>()
-    private val networkRequests = ArrayList<TestableNetworkCallback>()
+    private val registeredCallbacks = ArrayList<TestableNetworkCallback>()
 
     private var tetheredInterfaceRequest: TetheredInterfaceRequest? = null
 
     private class EthernetTestInterface(
         context: Context,
-        private val handler: Handler
+        private val handler: Handler,
+        hasCarrier: Boolean
     ) {
         private val tapInterface: TestNetworkInterface
         private val packetReader: TapPacketReader
         private val raResponder: RouterAdvertisementResponder
-        val interfaceName get() = tapInterface.interfaceName
+        private val tnm: TestNetworkManager
+        val name get() = tapInterface.interfaceName
 
         init {
-            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
-                val tnm = context.getSystemService(TestNetworkManager::class.java)
-                tnm.createTapInterface(false /* bringUp */)
+            tnm = runAsShell(MANAGE_TEST_NETWORKS) {
+                context.getSystemService(TestNetworkManager::class.java)
             }
-            val mtu = 1500
+            tapInterface = runAsShell(MANAGE_TEST_NETWORKS) {
+                tnm.createTapInterface(hasCarrier, false /* bringUp */)
+            }
+            val mtu = tapInterface.mtu
             packetReader = TapPacketReader(handler, tapInterface.fileDescriptor.fileDescriptor, mtu)
             raResponder = RouterAdvertisementResponder(packetReader)
             raResponder.addRouterEntry(MacAddress.fromString("01:23:45:67:89:ab"),
@@ -132,6 +143,12 @@
             raResponder.start()
         }
 
+        fun setCarrierEnabled(enabled: Boolean) {
+            runAsShell(MANAGE_TEST_NETWORKS) {
+                tnm.setCarrierEnabled(tapInterface, enabled)
+            }
+        }
+
         fun destroy() {
             raResponder.stop()
             handler.post({ packetReader.stop() })
@@ -172,7 +189,7 @@
         }
 
         fun expectCallback(iface: EthernetTestInterface, state: Int, role: Int) {
-            expectCallback(createChangeEvent(iface.interfaceName, state, role))
+            expectCallback(createChangeEvent(iface.name, state, role))
         }
 
         fun createChangeEvent(iface: String, state: Int, role: Int) =
@@ -190,7 +207,7 @@
         }
 
         fun eventuallyExpect(iface: EthernetTestInterface, state: Int, role: Int) {
-            eventuallyExpect(iface.interfaceName, state, role)
+            eventuallyExpect(iface.name, state, role)
         }
 
         fun assertNoCallback() {
@@ -227,6 +244,35 @@
         }
     }
 
+    private class EthernetOutcomeReceiver :
+        OutcomeReceiver<String, EthernetNetworkManagementException> {
+        private val result = CompletableFuture<String>()
+
+        override fun onResult(iface: String) {
+            result.complete(iface)
+        }
+
+        override fun onError(e: EthernetNetworkManagementException) {
+            result.completeExceptionally(e)
+        }
+
+        fun expectResult(expected: String) {
+            assertEquals(expected, result.get(TIMEOUT_MS, TimeUnit.MILLISECONDS))
+        }
+
+        fun expectError() {
+            // Assert that the future fails with EthernetNetworkManagementException from the
+            // completeExceptionally() call inside onUnavailable.
+            assertFailsWith(EthernetNetworkManagementException::class) {
+                try {
+                    result.get()
+                } catch (e: ExecutionException) {
+                    throw e.cause!!
+                }
+            }
+        }
+    }
+
     @Before
     fun setUp() {
         setIncludeTestInterfaces(true)
@@ -243,26 +289,28 @@
         for (listener in addedListeners) {
             em.removeInterfaceStateListener(listener)
         }
-        networkRequests.forEach { cm.unregisterNetworkCallback(it) }
+        registeredCallbacks.forEach { cm.unregisterNetworkCallback(it) }
         releaseTetheredInterface()
     }
 
     private fun addInterfaceStateListener(listener: EthernetStateListener) {
         runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS) {
-            em.addInterfaceStateListener(HandlerExecutor(Handler(Looper.getMainLooper())), listener)
+            em.addInterfaceStateListener(handler::post, listener)
         }
         addedListeners.add(listener)
     }
 
-    private fun createInterface(): EthernetTestInterface {
+    private fun createInterface(hasCarrier: Boolean = true): EthernetTestInterface {
         val iface = EthernetTestInterface(
             context,
-            Handler(Looper.getMainLooper())
+            handler,
+            hasCarrier
         ).also { createdIfaces.add(it) }
-        with(ifaceListener) {
-            // when an interface comes up, we should always see a down cb before an up cb.
-            eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
-            expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
+
+        // when an interface comes up, we should always see a down cb before an up cb.
+        ifaceListener.eventuallyExpect(iface, STATE_LINK_DOWN, ROLE_CLIENT)
+        if (hasCarrier) {
+            ifaceListener.expectCallback(iface, STATE_LINK_UP, ROLE_CLIENT)
         }
         return iface
     }
@@ -282,18 +330,20 @@
     private fun requestNetwork(request: NetworkRequest): TestableNetworkCallback {
         return TestableNetworkCallback().also {
             cm.requestNetwork(request, it)
-            networkRequests.add(it)
+            registeredCallbacks.add(it)
         }
     }
 
-    private fun releaseNetwork(cb: TestableNetworkCallback) {
-        cm.unregisterNetworkCallback(cb)
-        networkRequests.remove(cb)
+    private fun registerNetworkListener(request: NetworkRequest): TestableNetworkCallback {
+        return TestableNetworkCallback().also {
+            cm.registerNetworkCallback(request, it)
+            registeredCallbacks.add(it)
+        }
     }
 
     private fun requestTetheredInterface() = TetheredInterfaceListener().also {
         tetheredInterfaceRequest = runAsShell(NETWORK_SETTINGS) {
-            em.requestTetheredInterface(HandlerExecutor(Handler(Looper.getMainLooper())), it)
+            em.requestTetheredInterface(handler::post, it)
         }
     }
 
@@ -304,22 +354,65 @@
         }
     }
 
+    private fun releaseRequest(cb: TestableNetworkCallback) {
+        cm.unregisterNetworkCallback(cb)
+        registeredCallbacks.remove(cb)
+    }
+
+    private fun disableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.disableInterface(iface.name, handler::post, it)
+        }
+    }
+
+    private fun enableInterface(iface: EthernetTestInterface) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.enableInterface(iface.name, handler::post, it)
+        }
+    }
+
+    private fun updateConfiguration(
+        iface: EthernetTestInterface,
+        ipConfig: IpConfiguration? = null,
+        capabilities: NetworkCapabilities? = null
+    ) = EthernetOutcomeReceiver().also {
+        runAsShell(MANAGE_TEST_NETWORKS) {
+            em.updateConfiguration(
+                iface.name,
+                EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(ipConfig)
+                    .setNetworkCapabilities(capabilities).build(),
+                handler::post,
+                it)
+        }
+    }
+
+    // NetworkRequest.Builder does not create a copy of the passed NetworkRequest, so in order to
+    // keep ETH_REQUEST as it is, a defensive copy is created here.
     private fun NetworkRequest.createCopyWithEthernetSpecifier(ifaceName: String) =
         NetworkRequest.Builder(NetworkRequest(ETH_REQUEST))
             .setNetworkSpecifier(EthernetNetworkSpecifier(ifaceName)).build()
 
     // It can take multiple seconds for the network to become available.
     private fun TestableNetworkCallback.expectAvailable() =
-        expectCallback<Available>(anyNetwork(), 5000/*ms timeout*/).network
+        expectCallback<Available>(anyNetwork(), 5000 /* ms timeout */).network
 
     // b/233534110: eventuallyExpect<Lost>() does not advance ReadHead, use
     // eventuallyExpect(Lost::class) instead.
     private fun TestableNetworkCallback.eventuallyExpectLost(n: Network? = null) =
         eventuallyExpect(Lost::class, TIMEOUT_MS) { n?.equals(it.network) ?: true }
 
-    private fun TestableNetworkCallback.assertNotLost(n: Network? = null) =
+    private fun TestableNetworkCallback.assertNeverLost(n: Network? = null) =
         assertNoCallbackThat() { it is Lost && (n?.equals(it.network) ?: true) }
 
+    private fun TestableNetworkCallback.assertNeverAvailable(n: Network? = null) =
+        assertNoCallbackThat() { it is Available && (n?.equals(it.network) ?: true) }
+
+    private fun TestableNetworkCallback.expectCapabilitiesWithInterfaceName(name: String) =
+        expectCapabilitiesThat(anyNetwork()) {
+            it.networkSpecifier == EthernetNetworkSpecifier(name)
+        }
+
     @Test
     fun testCallbacks() {
         // If an interface exists when the callback is registered, it is reported on registration.
@@ -398,11 +491,8 @@
             assertTrue(polledIfaces.add(iface), "Duplicate interface $iface returned")
             assertTrue(ifaces.contains(iface), "Untracked interface $iface returned")
             // If the event's iface was created in the test, additional criteria can be validated.
-            createdIfaces.find { it.interfaceName.equals(iface) }?.let {
-                assertEquals(event,
-                    listener.createChangeEvent(it.interfaceName,
-                                                        STATE_LINK_UP,
-                                                        ROLE_CLIENT))
+            createdIfaces.find { it.name.equals(iface) }?.let {
+                assertEquals(event, listener.createChangeEvent(it.name, STATE_LINK_UP, ROLE_CLIENT))
             }
         }
         // Assert all callbacks are accounted for.
@@ -411,87 +501,75 @@
 
     @Test
     fun testGetInterfaceList() {
-        setIncludeTestInterfaces(true)
-
         // Create two test interfaces and check the return list contains the interface names.
         val iface1 = createInterface()
         val iface2 = createInterface()
         var ifaces = em.getInterfaceList()
         assertTrue(ifaces.size > 0)
-        assertTrue(ifaces.contains(iface1.interfaceName))
-        assertTrue(ifaces.contains(iface2.interfaceName))
+        assertTrue(ifaces.contains(iface1.name))
+        assertTrue(ifaces.contains(iface2.name))
 
         // Remove one existing test interface and check the return list doesn't contain the
         // removed interface name.
         removeInterface(iface1)
         ifaces = em.getInterfaceList()
-        assertFalse(ifaces.contains(iface1.interfaceName))
-        assertTrue(ifaces.contains(iface2.interfaceName))
+        assertFalse(ifaces.contains(iface1.name))
+        assertTrue(ifaces.contains(iface2.name))
 
         removeInterface(iface2)
     }
 
     @Test
     fun testNetworkRequest_withSingleExistingInterface() {
-        setIncludeTestInterfaces(true)
         createInterface()
 
         // install a listener which will later be used to verify the Lost callback
-        val listenerCb = TestableNetworkCallback()
-        cm.registerNetworkCallback(ETH_REQUEST, listenerCb)
-        networkRequests.add(listenerCb)
+        val listenerCb = registerNetworkListener(ETH_REQUEST)
 
         val cb = requestNetwork(ETH_REQUEST)
         val network = cb.expectAvailable()
 
-        cb.assertNotLost()
-        releaseNetwork(cb)
+        cb.assertNeverLost()
+        releaseRequest(cb)
         listenerCb.eventuallyExpectLost(network)
     }
 
     @Test
     fun testNetworkRequest_beforeSingleInterfaceIsUp() {
-        setIncludeTestInterfaces(true)
-
         val cb = requestNetwork(ETH_REQUEST)
 
-        // bring up interface after network has been requested
+        // bring up interface after network has been requested.
+        // Note: there is no guarantee that the NetworkRequest has been processed before the
+        // interface is actually created. That being said, it takes a few seconds between calling
+        // createInterface and the interface actually being properly registered with the ethernet
+        // module, so it is extremely unlikely that the CS handler thread has not run until then.
         val iface = createInterface()
         val network = cb.expectAvailable()
 
         // remove interface before network request has been removed
-        cb.assertNotLost()
+        cb.assertNeverLost()
         removeInterface(iface)
         cb.eventuallyExpectLost()
-
-        releaseNetwork(cb)
     }
 
     @Test
     fun testNetworkRequest_withMultipleInterfaces() {
-        setIncludeTestInterfaces(true)
-
         val iface1 = createInterface()
         val iface2 = createInterface()
 
-        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+        val cb = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
 
         val network = cb.expectAvailable()
-        cb.expectCapabilitiesThat(network) {
-            it.networkSpecifier == EthernetNetworkSpecifier(iface2.interfaceName)
-        }
+        cb.expectCapabilitiesWithInterfaceName(iface2.name)
 
         removeInterface(iface1)
-        cb.assertNotLost()
+        cb.assertNeverLost()
         removeInterface(iface2)
         cb.eventuallyExpectLost()
-
-        releaseNetwork(cb)
     }
 
     @Test
     fun testNetworkRequest_withInterfaceBeingReplaced() {
-        setIncludeTestInterfaces(true)
         val iface1 = createInterface()
 
         val cb = requestNetwork(ETH_REQUEST)
@@ -499,36 +577,67 @@
 
         // create another network and verify the request sticks to the current network
         val iface2 = createInterface()
-        cb.assertNotLost()
+        cb.assertNeverLost()
 
         // remove iface1 and verify the request brings up iface2
         removeInterface(iface1)
         cb.eventuallyExpectLost(network)
         val network2 = cb.expectAvailable()
-
-        releaseNetwork(cb)
     }
 
     @Test
     fun testNetworkRequest_withMultipleInterfacesAndRequests() {
-        setIncludeTestInterfaces(true)
         val iface1 = createInterface()
         val iface2 = createInterface()
 
-        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.interfaceName))
-        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.interfaceName))
+        val cb1 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface1.name))
+        val cb2 = requestNetwork(ETH_REQUEST.createCopyWithEthernetSpecifier(iface2.name))
         val cb3 = requestNetwork(ETH_REQUEST)
 
         cb1.expectAvailable()
+        cb1.expectCapabilitiesWithInterfaceName(iface1.name)
         cb2.expectAvailable()
+        cb2.expectCapabilitiesWithInterfaceName(iface2.name)
+        // this request can be matched by either network.
         cb3.expectAvailable()
 
-        cb1.assertNotLost()
-        cb2.assertNotLost()
-        cb3.assertNotLost()
+        cb1.assertNeverLost()
+        cb2.assertNeverLost()
+        cb3.assertNeverLost()
+    }
 
-        releaseNetwork(cb1)
-        releaseNetwork(cb2)
-        releaseNetwork(cb3)
+    @Test
+    fun testNetworkRequest_ensureProperRefcounting() {
+        // create first request before interface is up / exists; create another request after it has
+        // been created; release one of them and check that the network stays up.
+        val listener = registerNetworkListener(ETH_REQUEST)
+        val cb1 = requestNetwork(ETH_REQUEST)
+
+        val iface = createInterface()
+        val network = cb1.expectAvailable()
+
+        val cb2 = requestNetwork(ETH_REQUEST)
+        cb2.expectAvailable()
+
+        // release the first request; this used to trigger b/197548738
+        releaseRequest(cb1)
+
+        cb2.assertNeverLost()
+        releaseRequest(cb2)
+        listener.eventuallyExpectLost(network)
+    }
+
+    @Test
+    fun testNetworkRequest_forInterfaceWhileTogglingCarrier() {
+        val iface = createInterface(false /* hasCarrier */)
+
+        val cb = requestNetwork(ETH_REQUEST)
+        cb.assertNeverAvailable()
+
+        iface.setCarrierEnabled(true)
+        cb.expectAvailable()
+
+        iface.setCarrierEnabled(false)
+        cb.eventuallyExpectLost()
     }
 }
diff --git a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
index 391d03a..462c8a3 100644
--- a/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
+++ b/tests/cts/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -16,16 +16,11 @@
 
 package android.net.cts
 
-import android.Manifest
+import android.Manifest.permission.WRITE_DEVICE_CONFIG
 import android.net.util.NetworkStackUtils
 import android.provider.DeviceConfig
 import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
-import android.util.Log
 import com.android.testutils.runAsShell
-import com.android.testutils.tryTest
-import java.util.concurrent.CompletableFuture
-import java.util.concurrent.Executor
-import java.util.concurrent.TimeUnit
 
 /**
  * Collection of utility methods for configuring network validation.
@@ -38,9 +33,14 @@
      * Clear the test network validation URLs.
      */
     @JvmStatic fun clearValidationTestUrlsDeviceConfig() {
-        setHttpsUrlDeviceConfig(null)
-        setHttpUrlDeviceConfig(null)
-        setUrlExpirationDeviceConfig(null)
+        runAsShell(WRITE_DEVICE_CONFIG) {
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, null, false)
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, null, false)
+            DeviceConfig.setProperty(NAMESPACE_CONNECTIVITY,
+                    NetworkStackUtils.TEST_URL_EXPIRATION_TIME, null, false)
+        }
     }
 
     /**
@@ -48,71 +48,28 @@
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
      */
-    @JvmStatic fun setHttpsUrlDeviceConfig(url: String?) =
-            setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+    @JvmStatic
+    fun setHttpsUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
 
     /**
      * Set the test validation HTTP URL.
      *
      * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
      */
-    @JvmStatic fun setHttpUrlDeviceConfig(url: String?) =
-            setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+    @JvmStatic
+    fun setHttpUrlDeviceConfig(rule: DeviceConfigRule, url: String?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
 
     /**
      * Set the test validation URL expiration.
      *
      * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
      */
-    @JvmStatic fun setUrlExpirationDeviceConfig(timestamp: Long?) =
-            setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
-
-    private fun setConfig(configKey: String, value: String?): String? {
-        Log.i(TAG, "Setting config \"$configKey\" to \"$value\"")
-        val readWritePermissions = arrayOf(
-                Manifest.permission.READ_DEVICE_CONFIG,
-                Manifest.permission.WRITE_DEVICE_CONFIG)
-
-        val existingValue = runAsShell(*readWritePermissions) {
-            DeviceConfig.getProperty(NAMESPACE_CONNECTIVITY, configKey)
-        }
-        if (existingValue == value) {
-            // Already the correct value. There may be a race if a change is already in flight,
-            // but if multiple threads update the config there is no way to fix that anyway.
-            Log.i(TAG, "\$configKey\" already had value \"$value\"")
-            return value
-        }
-
-        val future = CompletableFuture<String>()
-        val listener = DeviceConfig.OnPropertiesChangedListener {
-            // The listener receives updates for any change to any key, so don't react to
-            // changes that do not affect the relevant key
-            if (!it.keyset.contains(configKey)) return@OnPropertiesChangedListener
-            if (it.getString(configKey, null) == value) {
-                future.complete(value)
-            }
-        }
-
-        return tryTest {
-            runAsShell(*readWritePermissions) {
-                DeviceConfig.addOnPropertiesChangedListener(
-                        NAMESPACE_CONNECTIVITY,
-                        inlineExecutor,
-                        listener)
-                DeviceConfig.setProperty(
-                        NAMESPACE_CONNECTIVITY,
-                        configKey,
-                        value,
-                        false /* makeDefault */)
-                // Don't drop the permission until the config is applied, just in case
-                future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)
-            }.also {
-                Log.i(TAG, "Config \"$configKey\" successfully set to \"$value\"")
-            }
-        } cleanup {
-            DeviceConfig.removeOnPropertiesChangedListener(listener)
-        }
-    }
-
-    private val inlineExecutor get() = Executor { r -> r.run() }
+    @JvmStatic
+    fun setUrlExpirationDeviceConfig(rule: DeviceConfigRule, timestamp: Long?) =
+            rule.setConfig(NAMESPACE_CONNECTIVITY,
+                NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
 }
diff --git a/tests/cts/net/src/android/net/cts/RateLimitTest.java b/tests/cts/net/src/android/net/cts/RateLimitTest.java
index 423f213..28cec1a 100644
--- a/tests/cts/net/src/android/net/cts/RateLimitTest.java
+++ b/tests/cts/net/src/android/net/cts/RateLimitTest.java
@@ -304,7 +304,7 @@
         // If this value is too low, this test might become flaky because of the burst value that
         // allows to send at a higher data rate for a short period of time. The faster the data rate
         // and the longer the test, the less this test will be affected.
-        final long dataLimitInBytesPerSecond = 1_000_000; // 1MB/s
+        final long dataLimitInBytesPerSecond = 2_000_000; // 2MB/s
         long resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(1));
         assertGreaterThan("Failed initial test with rate limit disabled", resultInBytesPerSecond,
                 dataLimitInBytesPerSecond);
@@ -315,9 +315,9 @@
         waitForTcPoliceFilterInstalled(Duration.ofSeconds(1));
 
         resultInBytesPerSecond = runIngressDataRateMeasurement(Duration.ofSeconds(10));
-        // Add 1% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
+        // Add 10% tolerance to reduce test flakiness. Burst size is constant at 128KiB.
         assertLessThan("Failed test with rate limit enabled", resultInBytesPerSecond,
-                (long) (dataLimitInBytesPerSecond * 1.01));
+                (long) (dataLimitInBytesPerSecond * 1.1));
 
         ConnectivitySettingsManager.setIngressRateLimitInBytesPerSecond(mContext, -1);
 
diff --git a/tests/native/connectivity_native_test.cpp b/tests/native/connectivity_native_test.cpp
index 3db5265..29b14ca 100644
--- a/tests/native/connectivity_native_test.cpp
+++ b/tests/native/connectivity_native_test.cpp
@@ -233,54 +233,29 @@
 }
 
 TEST_F(ConnectivityNativeBinderTest, BlockNegativePort) {
-    int retry = 0;
-    ndk::ScopedAStatus status;
-    do {
-        status = mService->blockPortForBind(-1);
-        // TODO: find out why transaction failed is being thrown on the first attempt.
-    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    ndk::ScopedAStatus status = mService->blockPortForBind(-1);
     EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
 }
 
 TEST_F(ConnectivityNativeBinderTest, UnblockNegativePort) {
-    int retry = 0;
-    ndk::ScopedAStatus status;
-    do {
-        status = mService->unblockPortForBind(-1);
-        // TODO: find out why transaction failed is being thrown on the first attempt.
-    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    ndk::ScopedAStatus status = mService->unblockPortForBind(-1);
     EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
 }
 
 TEST_F(ConnectivityNativeBinderTest, BlockMaxPort) {
-    int retry = 0;
-    ndk::ScopedAStatus status;
-    do {
-        status = mService->blockPortForBind(65536);
-        // TODO: find out why transaction failed is being thrown on the first attempt.
-    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    ndk::ScopedAStatus status = mService->blockPortForBind(65536);
     EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
 }
 
 TEST_F(ConnectivityNativeBinderTest, UnblockMaxPort) {
-    int retry = 0;
-    ndk::ScopedAStatus status;
-    do {
-        status = mService->unblockPortForBind(65536);
-        // TODO: find out why transaction failed is being thrown on the first attempt.
-    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    ndk::ScopedAStatus status = mService->unblockPortForBind(65536);
     EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode());
 }
 
 TEST_F(ConnectivityNativeBinderTest, CheckPermission) {
-    int retry = 0;
     int curUid = getuid();
     EXPECT_EQ(0, seteuid(FIRST_APPLICATION_UID + 2000)) << "seteuid failed: " << strerror(errno);
-    ndk::ScopedAStatus status;
-    do {
-        status = mService->blockPortForBind(5555);
-        // TODO: find out why transaction failed is being thrown on the first attempt.
-    } while (status.getExceptionCode() == EX_TRANSACTION_FAILED && retry++ < 5);
+    ndk::ScopedAStatus status = mService->blockPortForBind(5555);
     EXPECT_EQ(EX_SECURITY, status.getExceptionCode());
     EXPECT_EQ(0, seteuid(curUid)) << "seteuid failed: " << strerror(errno);
 }
diff --git a/tests/unit/java/android/net/NetworkStatsRecorderTest.java b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
new file mode 100644
index 0000000..fad11a3
--- /dev/null
+++ b/tests/unit/java/android/net/NetworkStatsRecorderTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *i
+ * 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.net;
+
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.net.NetworkStats;
+import android.os.DropBoxManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.util.FileRotator;
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+
+@RunWith(DevSdkIgnoreRunner.class)
+@SmallTest
+@DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+public final class NetworkStatsRecorderTest {
+    private static final String TAG = NetworkStatsRecorderTest.class.getSimpleName();
+
+    private static final String TEST_PREFIX = "test";
+
+    @Mock private DropBoxManager mDropBox;
+    @Mock private NetworkStats.NonMonotonicObserver mObserver;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    private NetworkStatsRecorder buildRecorder(FileRotator rotator, boolean wipeOnError) {
+        return new NetworkStatsRecorder(rotator, mObserver, mDropBox, TEST_PREFIX,
+                    HOUR_IN_MILLIS, false /* includeTags */, wipeOnError);
+    }
+
+    @Test
+    public void testWipeOnError() throws Exception {
+        final FileRotator rotator = mock(FileRotator.class);
+        final NetworkStatsRecorder wipeOnErrorRecorder = buildRecorder(rotator, true);
+
+        // Assuming that the rotator gets an exception happened when read data.
+        doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+        wipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+        // Verify that the files will be deleted.
+        verify(rotator, times(1)).deleteAll();
+        reset(rotator);
+
+        final NetworkStatsRecorder noWipeOnErrorRecorder = buildRecorder(rotator, false);
+        doThrow(new IOException()).when(rotator).readMatching(any(), anyLong(), anyLong());
+        noWipeOnErrorRecorder.getOrLoadPartialLocked(Long.MIN_VALUE, Long.MAX_VALUE);
+        // Verify that the rotator won't delete files.
+        verify(rotator, never()).deleteAll();
+    }
+}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 7725bb0..03e1cc4 100644
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -53,7 +53,6 @@
 import static android.net.ConnectivityManager.EXTRA_NETWORK_INFO;
 import static android.net.ConnectivityManager.EXTRA_NETWORK_TYPE;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_1;
 import static android.net.ConnectivityManager.FIREWALL_CHAIN_OEM_DENY_2;
@@ -9520,38 +9519,28 @@
 
     @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
     public void testLockdownSetFirewallUidRule() throws Exception {
-        // For ConnectivityService#setAlwaysOnVpnPackage.
-        mServiceContext.setPermission(
-                Manifest.permission.CONTROL_ALWAYS_ON_VPN, PERMISSION_GRANTED);
-        // Needed to call Vpn#setAlwaysOnPackage.
-        mServiceContext.setPermission(Manifest.permission.CONTROL_VPN, PERMISSION_GRANTED);
-        // Needed to call Vpn#isAlwaysOnPackageSupported.
-        mServiceContext.setPermission(NETWORK_SETTINGS, PERMISSION_GRANTED);
-
+        final Set<Range<Integer>> lockdownRange = UidRange.toIntRanges(Set.of(PRIMARY_UIDRANGE));
         // Enable Lockdown
-        final ArrayList<String> allowList = new ArrayList<>();
-        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, ALWAYS_ON_PACKAGE,
-                true /* lockdown */, allowList);
+        mCm.setRequireVpnForUids(true /* requireVpn */, lockdownRange);
         waitForIdle();
 
         // Lockdown rule is set to apps uids
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_DENY));
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
 
         reset(mBpfNetMaps);
 
         // Disable lockdown
-        mVpnManagerService.setAlwaysOnVpnPackage(PRIMARY_USER, null, false /* lockdown */,
-                allowList);
+        mCm.setRequireVpnForUids(false /* requireVPN */, lockdownRange);
         waitForIdle();
 
         // Lockdown rule is removed from apps uids
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP1_UID), eq(FIREWALL_RULE_ALLOW));
-        verify(mBpfNetMaps).setUidRule(
-                eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(APP2_UID), eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps, times(3)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP1_UID, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(APP2_UID, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
 
         // Interface rules are not changed by Lockdown mode enable/disable
         verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
@@ -10532,27 +10521,28 @@
         assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges("tun0"));
     }
 
-    @Test
-    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
-        LinkProperties lp = new LinkProperties();
-        lp.setInterfaceName("tun0");
-        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
-        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+    private void checkInterfaceFilteringRuleWithNullInterface(final LinkProperties lp,
+            final int uid) throws Exception {
         // The uid range needs to cover the test app so the network is visible to it.
         final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
-        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
-        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
+        mMockVpn.establish(lp, uid, vpnRange);
+        assertVpnUidRangesUpdated(true, vpnRange, uid);
 
         if (SdkLevel.isAtLeastT()) {
-            // On T and above, A connected Legacy VPN should have interface rules with null
-            // interface. Null Interface is a wildcard and this accepts traffic from all the
-            // interfaces. There are two expected invocations, one during the VPN initial
+            // On T and above, VPN should have rules for null interface. Null Interface is a
+            // wildcard and this accepts traffic from all the interfaces.
+            // There are two expected invocations, one during the VPN initial
             // connection, one during the VPN LinkProperties update.
             ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
             verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
                     eq(null) /* iface */, uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
-            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            if (uid == VPN_UID) {
+                assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID);
+                assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID);
+            } else {
+                assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
+                assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
+            }
             assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
                     vpnRange);
 
@@ -10561,50 +10551,37 @@
 
             // Disconnected VPN should have interface rules removed
             verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            if (uid == VPN_UID) {
+                assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID);
+            } else {
+                assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
+            }
             assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
         } else {
-            // Before T, Legacy VPN should not have interface rules.
+            // Before T, rules are not configured for null interface.
             verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
         }
     }
 
     @Test
+    public void testLegacyVpnInterfaceFilteringRule() throws Exception {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("tun0");
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+        lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+        // Legacy VPN should have interface filtering with null interface.
+        checkInterfaceFilteringRuleWithNullInterface(lp, Process.SYSTEM_UID);
+    }
+
+    @Test
     public void testLocalIpv4OnlyVpnInterfaceFilteringRule() throws Exception {
         LinkProperties lp = new LinkProperties();
         lp.setInterfaceName("tun0");
         lp.addRoute(new RouteInfo(new IpPrefix("192.0.2.0/24"), null, "tun0"));
         lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), RTN_UNREACHABLE));
-        // The uid range needs to cover the test app so the network is visible to it.
-        final Set<UidRange> vpnRange = Collections.singleton(PRIMARY_UIDRANGE);
-        mMockVpn.establish(lp, Process.SYSTEM_UID, vpnRange);
-        assertVpnUidRangesUpdated(true, vpnRange, Process.SYSTEM_UID);
-
-        if (SdkLevel.isAtLeastT()) {
-            // IPv6 unreachable route should not be misinterpreted as a default route
-            // On T and above, A connected VPN that does not provide a default route should have
-            // interface rules with null interface. Null Interface is a wildcard and this accepts
-            // traffic from all the interfaces. There are two expected invocations, one during the
-            // VPN initial connection, one during the VPN LinkProperties update.
-            ArgumentCaptor<int[]> uidCaptor = ArgumentCaptor.forClass(int[].class);
-            verify(mBpfNetMaps, times(2)).addUidInterfaceRules(
-                    eq(null) /* iface */, uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getAllValues().get(0), APP1_UID, APP2_UID, VPN_UID);
-            assertContainsExactly(uidCaptor.getAllValues().get(1), APP1_UID, APP2_UID, VPN_UID);
-            assertEquals(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */),
-                    vpnRange);
-
-            mMockVpn.disconnect();
-            waitForIdle();
-
-            // Disconnected VPN should have interface rules removed
-            verify(mBpfNetMaps).removeUidInterfaceRules(uidCaptor.capture());
-            assertContainsExactly(uidCaptor.getValue(), APP1_UID, APP2_UID, VPN_UID);
-            assertNull(mService.mPermissionMonitor.getVpnInterfaceUidRanges(null /* iface */));
-        } else {
-            // Before T, VPN with IPv6 unreachable route should not have interface rules.
-            verify(mBpfNetMaps, never()).addUidInterfaceRules(any(), any());
-        }
+        // VPN that does not provide a default route should have interface filtering with null
+        // interface.
+        checkInterfaceFilteringRuleWithNullInterface(lp, VPN_UID);
     }
 
     @Test
diff --git a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
index ecd17ba..354e79a 100644
--- a/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
+++ b/tests/unit/java/com/android/server/connectivity/PermissionMonitorTest.java
@@ -30,9 +30,6 @@
 import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_REQUIRED;
 import static android.content.pm.PackageManager.GET_PERMISSIONS;
 import static android.content.pm.PackageManager.MATCH_ANY_USER;
-import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOCKDOWN_VPN;
-import static android.net.ConnectivityManager.FIREWALL_RULE_ALLOW;
-import static android.net.ConnectivityManager.FIREWALL_RULE_DENY;
 import static android.net.ConnectivitySettingsManager.UIDS_ALLOWED_ON_RESTRICTED_NETWORKS;
 import static android.net.INetd.PERMISSION_INTERNET;
 import static android.net.INetd.PERMISSION_NETWORK;
@@ -698,7 +695,8 @@
         mNetdMonitor.expectNetworkPerm(PERMISSION_SYSTEM, new UserHandle[]{MOCK_USER1},
                 SYSTEM_APPID1);
 
-        final List<PackageInfo> pkgs = List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
+        final List<PackageInfo> pkgs = List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID21,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, SYSTEM_APP_UID21, CHANGE_NETWORK_STATE));
         doReturn(pkgs).when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS),
@@ -764,9 +762,10 @@
                 MOCK_APPID1);
     }
 
-    private void doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
+    private void doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(@Nullable String ifName)
             throws Exception {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
@@ -774,7 +773,7 @@
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         buildAndMockPackageInfoWithPermissions(MOCK_PACKAGE1, MOCK_UID11);
         mPermissionMonitor.startMonitoring();
-        // Every app on user 0 except MOCK_UID12 are under VPN.
+        // Every app on user 0 except MOCK_UID12 is subject to the VPN.
         final Set<UidRange> vpnRange1 = Set.of(
                 new UidRange(0, MOCK_UID12 - 1),
                 new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1));
@@ -811,18 +810,19 @@
 
     @Test
     public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdates() throws Exception {
-        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
+        doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates("tun0");
     }
 
     @Test
     public void testUidFilteringDuringVpnConnectDisconnectAndUidUpdatesWithWildcard()
             throws Exception {
-        doTestuidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
+        doTestUidFilteringDuringVpnConnectDisconnectAndUidUpdates(null /* ifName */);
     }
 
     private void doTestUidFilteringDuringPackageInstallAndUninstall(@Nullable String ifName) throws
             Exception {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
@@ -857,155 +857,149 @@
 
     @Test
     public void testLockdownUidFilteringWithLockdownEnableDisable() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
-        // Every app on user 0 except MOCK_UID12 are under VPN.
-        final UidRange[] vpnRange1 = {
+        // Every app on user 0 except MOCK_UID12 is subject to the VPN.
+        final UidRange[] lockdownRange = {
                 new UidRange(0, MOCK_UID12 - 1),
                 new UidRange(MOCK_UID12 + 1, UserHandle.PER_USER_RANGE - 1)
         };
 
-        // Add Lockdown uid range, expect a rule to be set up for user app MOCK_UID11
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange1);
-        verify(mBpfNetMaps)
-                .setUidRule(
-                        eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange1));
+        // Add Lockdown uid range, expect a rule to be set up for MOCK_UID11 and VPN_UID
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range, expect rules to be torn down
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange1);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(VPN_UID, false /* add */);
         assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
     }
 
     @Test
     public void testLockdownUidFilteringWithLockdownEnableDisableWithMultiAdd() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
-        // MOCK_UID11 is under VPN.
+        // MOCK_UID11 is subject to the VPN.
         final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
-        final UidRange[] vpnRange = {range};
+        final UidRange[] lockdownRange = {range};
 
         // Add Lockdown uid range at 1st time, expect a rule to be set up
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Add Lockdown uid range at 2nd time, expect a rule not to be set up because the uid
         // already has the rule
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
-        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because we added
         // the range 2 times.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(),  anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 2nd time, expect a rule to be torn down because we added
         // twice and we removed twice.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
         assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
     }
 
     @Test
     public void testLockdownUidFilteringWithLockdownEnableDisableWithDuplicates() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
-        // MOCK_UID11 is under VPN.
+        // MOCK_UID11 is subject to the VPN.
         final UidRange range = new UidRange(MOCK_UID11, MOCK_UID11);
-        final UidRange[] vpnRangeDuplicates = {range, range};
-        final UidRange[] vpnRange = {range};
+        final UidRange[] lockdownRangeDuplicates = {range, range};
+        final UidRange[] lockdownRange = {range};
 
         // Add Lockdown uid ranges which contains duplicated uid ranges
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRangeDuplicates);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRangeDuplicates);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 1st time, expect a rule not to be torn down because uid
         // ranges we added contains duplicated uid ranges.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps, never()).setUidRule(anyInt(), anyInt(), anyInt());
-        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(vpnRange));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps, never()).updateUidLockdownRule(anyInt(), anyBoolean());
+        assertEquals(mPermissionMonitor.getVpnLockdownUidRanges(), Set.of(lockdownRange));
 
         reset(mBpfNetMaps);
 
         // Remove Lockdown uid range at 2nd time, expect a rule to be torn down.
-        mPermissionMonitor.updateVpnLockdownUidRanges(false /* false */, vpnRange);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
+        mPermissionMonitor.updateVpnLockdownUidRanges(false /* add */, lockdownRange);
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
         assertTrue(mPermissionMonitor.getVpnLockdownUidRanges().isEmpty());
     }
 
     @Test
     public void testLockdownUidFilteringWithInstallAndUnInstall() {
-        doReturn(List.of(buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
+        doReturn(List.of(
+                buildPackageInfo(SYSTEM_PACKAGE1, SYSTEM_APP_UID11, CHANGE_NETWORK_STATE,
                         NETWORK_STACK, CONNECTIVITY_USE_RESTRICTED_NETWORKS),
                 buildPackageInfo(SYSTEM_PACKAGE2, VPN_UID)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         doReturn(List.of(MOCK_USER1, MOCK_USER2)).when(mUserManager).getUserHandles(eq(true));
 
         mPermissionMonitor.startMonitoring();
-        final UidRange[] vpnRange = {
+        final UidRange[] lockdownRange = {
                 UidRange.createForUser(MOCK_USER1),
                 UidRange.createForUser(MOCK_USER2)
         };
-        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, vpnRange);
+        mPermissionMonitor.updateVpnLockdownUidRanges(true /* add */, lockdownRange);
+
+        reset(mBpfNetMaps);
 
         // Installing package should add Lockdown rules
         addPackageForUsers(new UserHandle[]{MOCK_USER1, MOCK_USER2}, MOCK_PACKAGE1, MOCK_APPID1);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_DENY));
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
-                        eq(FIREWALL_RULE_DENY));
+        verify(mBpfNetMaps, times(2)).updateUidLockdownRule(anyInt(), eq(true) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, true /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID21, true /* add */);
 
         reset(mBpfNetMaps);
 
         // Uninstalling package should remove Lockdown rules
         mPermissionMonitor.onPackageRemoved(MOCK_PACKAGE1, MOCK_UID11);
-        verify(mBpfNetMaps)
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID11),
-                        eq(FIREWALL_RULE_ALLOW));
-        verify(mBpfNetMaps, never())
-                .setUidRule(eq(FIREWALL_CHAIN_LOCKDOWN_VPN), eq(MOCK_UID21),
-                        eq(FIREWALL_RULE_ALLOW));
+        verify(mBpfNetMaps).updateUidLockdownRule(anyInt(), eq(false) /* add */);
+        verify(mBpfNetMaps).updateUidLockdownRule(MOCK_UID11, false /* add */);
     }
 
     // Normal package add/remove operations will trigger multiple intent for uids corresponding to
@@ -1329,7 +1323,8 @@
     public void testOnExternalApplicationsAvailable() throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // and have different uids. There has no permission for both uids.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID12)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
@@ -1387,7 +1382,8 @@
             throws Exception {
         // Initial the permission state. MOCK_PACKAGE1 and MOCK_PACKAGE2 are installed on external
         // storage and shared on MOCK_UID11. There has no permission for MOCK_UID11.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
@@ -1413,7 +1409,8 @@
         // Initial the permission state. MOCK_PACKAGE1 is installed on external storage and
         // MOCK_PACKAGE2 is installed on device. These two packages are shared on MOCK_UID11.
         // MOCK_UID11 has NETWORK and INTERNET permissions.
-        doReturn(List.of(buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
+        doReturn(List.of(
+                buildPackageInfo(MOCK_PACKAGE1, MOCK_UID11),
                 buildPackageInfo(MOCK_PACKAGE2, MOCK_UID11, CHANGE_NETWORK_STATE, INTERNET)))
                 .when(mPackageManager).getInstalledPackagesAsUser(eq(GET_PERMISSIONS), anyInt());
         mPermissionMonitor.startMonitoring();
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index 1f37ff3..8f1d3b8 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -27,7 +27,9 @@
 import static android.net.ConnectivityManager.NetworkCallback;
 import static android.net.INetd.IF_STATE_DOWN;
 import static android.net.INetd.IF_STATE_UP;
+import static android.net.RouteInfo.RTN_UNREACHABLE;
 import static android.net.VpnManager.TYPE_VPN_PLATFORM;
+import static android.net.ipsec.ike.IkeSessionConfiguration.EXTENSION_TYPE_MOBIKE;
 import static android.os.Build.VERSION_CODES.S_V2;
 import static android.os.UserHandle.PER_USER_RANGE;
 
@@ -39,6 +41,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
@@ -59,6 +62,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -83,7 +87,9 @@
 import android.net.InetAddresses;
 import android.net.InterfaceConfigurationParcel;
 import android.net.IpPrefix;
+import android.net.IpSecConfig;
 import android.net.IpSecManager;
+import android.net.IpSecTransform;
 import android.net.IpSecTunnelInterfaceResponse;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -100,7 +106,12 @@
 import android.net.VpnProfileState;
 import android.net.VpnService;
 import android.net.VpnTransportInfo;
+import android.net.ipsec.ike.ChildSessionCallback;
+import android.net.ipsec.ike.ChildSessionConfiguration;
 import android.net.ipsec.ike.IkeSessionCallback;
+import android.net.ipsec.ike.IkeSessionConfiguration;
+import android.net.ipsec.ike.IkeSessionConnectionInfo;
+import android.net.ipsec.ike.IkeTrafficSelector;
 import android.net.ipsec.ike.exceptions.IkeException;
 import android.net.ipsec.ike.exceptions.IkeNetworkLostException;
 import android.net.ipsec.ike.exceptions.IkeNonProtocolException;
@@ -121,6 +132,7 @@
 import android.security.Credentials;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Pair;
 import android.util.Range;
 
 import androidx.test.filters.SmallTest;
@@ -155,6 +167,7 @@
 import java.io.FileWriter;
 import java.io.IOException;
 import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
@@ -165,6 +178,8 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Stream;
 
@@ -198,11 +213,37 @@
     static final Network EGRESS_NETWORK = new Network(101);
     static final String EGRESS_IFACE = "wlan0";
     static final String TEST_VPN_PKG = "com.testvpn.vpn";
+    private static final String TEST_VPN_CLIENT = "2.4.6.8";
     private static final String TEST_VPN_SERVER = "1.2.3.4";
     private static final String TEST_VPN_IDENTITY = "identity";
     private static final byte[] TEST_VPN_PSK = "psk".getBytes();
 
+    private static final int IP4_PREFIX_LEN = 32;
+    private static final int MIN_PORT = 0;
+    private static final int MAX_PORT = 65535;
+
+    private static final InetAddress TEST_VPN_CLIENT_IP =
+            InetAddresses.parseNumericAddress(TEST_VPN_CLIENT);
+    private static final InetAddress TEST_VPN_SERVER_IP =
+            InetAddresses.parseNumericAddress(TEST_VPN_SERVER);
+    private static final InetAddress TEST_VPN_CLIENT_IP_2 =
+            InetAddresses.parseNumericAddress("192.0.2.200");
+    private static final InetAddress TEST_VPN_SERVER_IP_2 =
+            InetAddresses.parseNumericAddress("192.0.2.201");
+    private static final InetAddress TEST_VPN_INTERNAL_IP =
+            InetAddresses.parseNumericAddress("198.51.100.10");
+    private static final InetAddress TEST_VPN_INTERNAL_DNS =
+            InetAddresses.parseNumericAddress("8.8.8.8");
+
+    private static final IkeTrafficSelector IN_TS =
+            new IkeTrafficSelector(MIN_PORT, MAX_PORT, TEST_VPN_INTERNAL_IP, TEST_VPN_INTERNAL_IP);
+    private static final IkeTrafficSelector OUT_TS =
+            new IkeTrafficSelector(MIN_PORT, MAX_PORT,
+                    InetAddresses.parseNumericAddress("0.0.0.0"),
+                    InetAddresses.parseNumericAddress("255.255.255.255"));
+
     private static final Network TEST_NETWORK = new Network(Integer.MAX_VALUE);
+    private static final Network TEST_NETWORK_2 = new Network(Integer.MAX_VALUE - 1);
     private static final String TEST_IFACE_NAME = "TEST_IFACE";
     private static final int TEST_TUNNEL_RESOURCE_ID = 0x2345;
     private static final long TEST_TIMEOUT_MS = 500L;
@@ -234,15 +275,21 @@
     @Mock private AppOpsManager mAppOps;
     @Mock private NotificationManager mNotificationManager;
     @Mock private Vpn.SystemServices mSystemServices;
+    @Mock private Vpn.IkeSessionWrapper mIkeSessionWrapper;
     @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
+    @Mock private NetworkAgent mMockNetworkAgent;
     @Mock private ConnectivityManager mConnectivityManager;
     @Mock private IpSecService mIpSecService;
     @Mock private VpnProfileStore mVpnProfileStore;
+    @Mock private ScheduledThreadPoolExecutor mExecutor;
+    @Mock private ScheduledFuture mScheduledFuture;
     @Mock DeviceIdleInternal mDeviceIdleInternal;
     private final VpnProfile mVpnProfile;
 
     private IpSecManager mIpSecManager;
 
+    private TestDeps mTestDeps;
+
     public VpnTest() throws Exception {
         // Build an actual VPN profile that is capable of being converted to and from an
         // Ikev2VpnProfile
@@ -257,6 +304,7 @@
         MockitoAnnotations.initMocks(this);
 
         mIpSecManager = new IpSecManager(mContext, mIpSecService);
+        mTestDeps = spy(new TestDeps());
 
         when(mContext.getPackageManager()).thenReturn(mPackageManager);
         setMockedPackages(mPackages);
@@ -297,6 +345,28 @@
         // itself, so set the default value of Context#checkCallingOrSelfPermission to
         // PERMISSION_DENIED.
         doReturn(PERMISSION_DENIED).when(mContext).checkCallingOrSelfPermission(any());
+
+        // Set up mIkev2SessionCreator and mExecutor
+        resetIkev2SessionCreator(mIkeSessionWrapper);
+        resetExecutor(mScheduledFuture);
+    }
+
+    private void resetIkev2SessionCreator(Vpn.IkeSessionWrapper ikeSession) {
+        reset(mIkev2SessionCreator);
+        when(mIkev2SessionCreator.createIkeSession(any(), any(), any(), any(), any(), any()))
+                .thenReturn(ikeSession);
+    }
+
+    private void resetExecutor(ScheduledFuture scheduledFuture) {
+        doAnswer(
+                (invocation) -> {
+                    ((Runnable) invocation.getArgument(0)).run();
+                    return null;
+                })
+            .when(mExecutor)
+            .execute(any());
+        when(mExecutor.schedule(
+                any(Runnable.class), anyLong(), any())).thenReturn(mScheduledFuture);
     }
 
     @After
@@ -1344,25 +1414,38 @@
             verify(mConnectivityManager, timeout(TEST_TIMEOUT_MS))
                     .unregisterNetworkCallback(eq(cb));
         } else if (errorType == VpnManager.ERROR_CLASS_RECOVERABLE) {
-            // To prevent spending much time to test the retry function, only retry 2 times here.
             int retryIndex = 0;
-            verify(mIkev2SessionCreator,
-                    timeout(((TestDeps) vpn.mDeps).getNextRetryDelaySeconds(retryIndex++) * 1000
-                            + TEST_TIMEOUT_MS))
-                    .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+            final IkeSessionCallback ikeCb2 = verifyRetryAndGetNewIkeCb(retryIndex++);
 
-            // Capture a new IkeSessionCallback to get the latest token.
-            reset(mIkev2SessionCreator);
-            final IkeSessionCallback ikeCb2 = captor.getValue();
             ikeCb2.onClosedWithException(exception);
-            verify(mIkev2SessionCreator,
-                    timeout(((TestDeps) vpn.mDeps).getNextRetryDelaySeconds(retryIndex++) * 1000
-                            + TEST_TIMEOUT_MS))
-                    .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
-            reset(mIkev2SessionCreator);
+            verifyRetryAndGetNewIkeCb(retryIndex++);
         }
     }
 
+    private IkeSessionCallback verifyRetryAndGetNewIkeCb(int retryIndex) {
+        final ArgumentCaptor<Runnable> runnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+
+        // Verify retry is scheduled
+        final long expectedDelay = mTestDeps.getNextRetryDelaySeconds(retryIndex);
+        verify(mExecutor).schedule(runnableCaptor.capture(), eq(expectedDelay), any());
+
+        // Mock the event of firing the retry task
+        runnableCaptor.getValue().run();
+
+        verify(mIkev2SessionCreator)
+                .createIkeSession(any(), any(), any(), any(), ikeCbCaptor.capture(), any());
+
+        // Forget the mIkev2SessionCreator#createIkeSession call and mExecutor#schedule call
+        // for the next retry verification
+        resetIkev2SessionCreator(mIkeSessionWrapper);
+        resetExecutor(mScheduledFuture);
+
+        return ikeCbCaptor.getValue();
+    }
+
     @Test
     public void testStartPlatformVpnAuthenticationFailed() throws Exception {
         final IkeProtocolException exception = mock(IkeProtocolException.class);
@@ -1491,11 +1574,266 @@
         return vpn;
     }
 
+    private IkeSessionConnectionInfo createIkeConnectInfo() {
+        return new IkeSessionConnectionInfo(TEST_VPN_CLIENT_IP, TEST_VPN_SERVER_IP, TEST_NETWORK);
+    }
+
+    private IkeSessionConnectionInfo createIkeConnectInfo_2() {
+        return new IkeSessionConnectionInfo(
+                TEST_VPN_CLIENT_IP_2, TEST_VPN_SERVER_IP_2, TEST_NETWORK_2);
+    }
+
+    private IkeSessionConfiguration createIkeConfig(
+            IkeSessionConnectionInfo ikeConnectInfo, boolean isMobikeEnabled) {
+        final IkeSessionConfiguration.Builder builder =
+                new IkeSessionConfiguration.Builder(ikeConnectInfo);
+
+        if (isMobikeEnabled) {
+            builder.addIkeExtension(EXTENSION_TYPE_MOBIKE);
+        }
+
+        return builder.build();
+    }
+
+    private ChildSessionConfiguration createChildConfig() {
+        return new ChildSessionConfiguration.Builder(Arrays.asList(IN_TS), Arrays.asList(OUT_TS))
+                .addInternalAddress(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN))
+                .addInternalDnsServer(TEST_VPN_INTERNAL_DNS)
+                .build();
+    }
+
+    private IpSecTransform createIpSecTransform() {
+        return new IpSecTransform(mContext, new IpSecConfig());
+    }
+
+    private void verifyApplyTunnelModeTransforms(int expectedTimes) throws Exception {
+        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_IN),
+                anyInt(), anyString());
+        verify(mIpSecService, times(expectedTimes)).applyTunnelModeTransform(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(IpSecManager.DIRECTION_OUT),
+                anyInt(), anyString());
+    }
+
+    private Pair<IkeSessionCallback, ChildSessionCallback> verifyCreateIkeAndCaptureCbs()
+            throws Exception {
+        final ArgumentCaptor<IkeSessionCallback> ikeCbCaptor =
+                ArgumentCaptor.forClass(IkeSessionCallback.class);
+        final ArgumentCaptor<ChildSessionCallback> childCbCaptor =
+                ArgumentCaptor.forClass(ChildSessionCallback.class);
+
+        verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS)).createIkeSession(
+                any(), any(), any(), any(), ikeCbCaptor.capture(), childCbCaptor.capture());
+
+        return new Pair<>(ikeCbCaptor.getValue(), childCbCaptor.getValue());
+    }
+
+    private static class PlatformVpnSnapshot {
+        public final Vpn vpn;
+        public final NetworkCallback nwCb;
+        public final IkeSessionCallback ikeCb;
+        public final ChildSessionCallback childCb;
+
+        PlatformVpnSnapshot(Vpn vpn, NetworkCallback nwCb,
+                IkeSessionCallback ikeCb, ChildSessionCallback childCb) {
+            this.vpn = vpn;
+            this.nwCb = nwCb;
+            this.ikeCb = ikeCb;
+            this.childCb = childCb;
+        }
+    }
+
+    private PlatformVpnSnapshot verifySetupPlatformVpn(IkeSessionConfiguration ikeConfig)
+            throws Exception {
+        doReturn(mMockNetworkAgent).when(mTestDeps)
+                .newNetworkAgent(
+                        any(), any(), anyString(), any(), any(), any(), any(), any());
+
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OPSTR_ACTIVATE_PLATFORM_VPN);
+        when(mVpnProfileStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG);
+        final NetworkCallback nwCb = triggerOnAvailableAndGetCallback();
+
+        // Mock the setup procedure by firing callbacks
+        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+                verifyCreateIkeAndCaptureCbs();
+        final IkeSessionCallback ikeCb = cbPair.first;
+        final ChildSessionCallback childCb = cbPair.second;
+
+        ikeCb.onOpened(ikeConfig);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+        childCb.onOpened(createChildConfig());
+
+        // Verification VPN setup
+        verifyApplyTunnelModeTransforms(1);
+
+        ArgumentCaptor<LinkProperties> lpCaptor = ArgumentCaptor.forClass(LinkProperties.class);
+        ArgumentCaptor<NetworkCapabilities> ncCaptor =
+                ArgumentCaptor.forClass(NetworkCapabilities.class);
+        verify(mTestDeps).newNetworkAgent(
+                any(), any(), anyString(), ncCaptor.capture(), lpCaptor.capture(),
+                any(), any(), any());
+
+        // Check LinkProperties
+        final LinkProperties lp = lpCaptor.getValue();
+        final List<RouteInfo> expectedRoutes = Arrays.asList(
+                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+                        TEST_IFACE_NAME, RouteInfo.RTN_UNICAST),
+                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+                        TEST_IFACE_NAME, RTN_UNREACHABLE));
+        assertEquals(expectedRoutes, lp.getRoutes());
+
+        // Check internal addresses
+        final List<LinkAddress> expectedAddresses =
+                Arrays.asList(new LinkAddress(TEST_VPN_INTERNAL_IP, IP4_PREFIX_LEN));
+        assertEquals(expectedAddresses, lp.getLinkAddresses());
+
+        // Check internal DNS
+        assertEquals(Arrays.asList(TEST_VPN_INTERNAL_DNS), lp.getDnsServers());
+
+        // Check NetworkCapabilities
+        assertEquals(Arrays.asList(TEST_NETWORK), ncCaptor.getValue().getUnderlyingNetworks());
+
+        return new PlatformVpnSnapshot(vpn, nwCb, ikeCb, childCb);
+    }
+
     @Test
     public void testStartPlatformVpn() throws Exception {
-        startLegacyVpn(createVpn(primaryUser.id), mVpnProfile);
-        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
-        // a subsequent patch.
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    @Test
+    public void testStartPlatformVpnMobility_mobikeEnabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), true /* isMobikeEnabled */));
+
+        // Mock network loss and verify a cleanup task is scheduled
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+        verify(mExecutor).schedule(any(Runnable.class), anyLong(), any());
+
+        // Mock new network comes up and the cleanup task is cancelled
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+        verify(mScheduledFuture).cancel(anyBoolean());
+
+        // Verify MOBIKE is triggered
+        verify(mIkeSessionWrapper).setNetwork(TEST_NETWORK_2);
+
+        // Mock the MOBIKE procedure
+        vpnSnapShot.ikeCb.onIkeSessionConnectionInfoChanged(createIkeConnectInfo_2());
+        vpnSnapShot.childCb.onIpSecTransformsMigrated(
+                createIpSecTransform(), createIpSecTransform());
+
+        verify(mIpSecService).setNetworkForTunnelInterface(
+                eq(TEST_TUNNEL_RESOURCE_ID), eq(TEST_NETWORK_2), anyString());
+
+        // Expect 2 times: one for initial setup and one for MOBIKE
+        verifyApplyTunnelModeTransforms(2);
+
+        // Verify mNetworkCapabilities and mNetworkAgent are updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    @Test
+    public void testStartPlatformVpnReestablishes_mobikeDisabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the first IKE creation to be prepared to capture callbacks of the second
+        // IKE session
+        resetIkev2SessionCreator(mock(Vpn.IkeSessionWrapper.class));
+
+        // Mock network switch
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+        vpnSnapShot.nwCb.onAvailable(TEST_NETWORK_2);
+
+        // Verify the old IKE Session is killed
+        verify(mIkeSessionWrapper).kill();
+
+        // Capture callbacks of the new IKE Session
+        final Pair<IkeSessionCallback, ChildSessionCallback> cbPair =
+                verifyCreateIkeAndCaptureCbs();
+        final IkeSessionCallback ikeCb = cbPair.first;
+        final ChildSessionCallback childCb = cbPair.second;
+
+        // Mock the IKE Session setup
+        ikeCb.onOpened(createIkeConfig(createIkeConnectInfo_2(), false /* isMobikeEnabled */));
+
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_IN);
+        childCb.onIpSecTransformCreated(createIpSecTransform(), IpSecManager.DIRECTION_OUT);
+        childCb.onOpened(createChildConfig());
+
+        // Expect 2 times since there have been two Session setups
+        verifyApplyTunnelModeTransforms(2);
+
+        // Verify mNetworkCapabilities and mNetworkAgent are updated
+        assertEquals(
+                Collections.singletonList(TEST_NETWORK_2),
+                vpnSnapShot.vpn.mNetworkCapabilities.getUnderlyingNetworks());
+        verify(mMockNetworkAgent)
+                .setUnderlyingNetworks(Collections.singletonList(TEST_NETWORK_2));
+
+        vpnSnapShot.vpn.mVpnRunner.exitVpnRunner();
+    }
+
+    private void verifyHandlingNetworkLoss() throws Exception {
+        final ArgumentCaptor<LinkProperties> lpCaptor =
+                ArgumentCaptor.forClass(LinkProperties.class);
+        verify(mMockNetworkAgent).sendLinkProperties(lpCaptor.capture());
+        final LinkProperties lp = lpCaptor.getValue();
+
+        assertNull(lp.getInterfaceName());
+        final List<RouteInfo> expectedRoutes = Arrays.asList(
+                new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null /*gateway*/,
+                        null /*iface*/, RTN_UNREACHABLE),
+                new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/,
+                        null /*iface*/, RTN_UNREACHABLE));
+        assertEquals(expectedRoutes, lp.getRoutes());
+    }
+
+    @Test
+    public void testStartPlatformVpnHandlesNetworkLoss_mobikeEnabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the #sendLinkProperties during first setup.
+        reset(mMockNetworkAgent);
+
+        final ArgumentCaptor<Runnable> runnableCaptor =
+                ArgumentCaptor.forClass(Runnable.class);
+
+        // Mock network loss
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+        // Mock the grace period expires
+        verify(mExecutor).schedule(runnableCaptor.capture(), anyLong(), any());
+        runnableCaptor.getValue().run();
+
+        verifyHandlingNetworkLoss();
+    }
+
+    @Test
+    public void testStartPlatformVpnHandlesNetworkLoss_mobikeDisabled() throws Exception {
+        final PlatformVpnSnapshot vpnSnapShot = verifySetupPlatformVpn(
+                createIkeConfig(createIkeConnectInfo(), false /* isMobikeEnabled */));
+
+        // Forget the #sendLinkProperties during first setup.
+        reset(mMockNetworkAgent);
+
+        // Mock network loss
+        vpnSnapShot.nwCb.onLost(TEST_NETWORK);
+
+        verifyHandlingNetworkLoss();
     }
 
     @Test
@@ -1631,7 +1969,8 @@
         }
     }
 
-    private final class TestDeps extends Vpn.Dependencies {
+    // Make it public and un-final so as to spy it
+    public class TestDeps extends Vpn.Dependencies {
         public final CompletableFuture<String[]> racoonArgs = new CompletableFuture();
         public final CompletableFuture<String[]> mtpdArgs = new CompletableFuture();
         public final File mStateFile;
@@ -1766,10 +2105,16 @@
             return mDeviceIdleInternal;
         }
 
+        @Override
         public long getNextRetryDelaySeconds(int retryCount) {
             // Simply return retryCount as the delay seconds for retrying.
             return retryCount;
         }
+
+        @Override
+        public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor() {
+            return mExecutor;
+        }
     }
 
     /**
@@ -1781,7 +2126,7 @@
         when(mContext.createContextAsUser(eq(UserHandle.of(userId)), anyInt()))
                 .thenReturn(asUserContext);
         final TestLooper testLooper = new TestLooper();
-        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, new TestDeps(), mNetService,
+        final Vpn vpn = new Vpn(testLooper.getLooper(), mContext, mTestDeps, mNetService,
                 mNetd, userId, mVpnProfileStore, mSystemServices, mIkev2SessionCreator);
         verify(mConnectivityManager, times(1)).registerNetworkProvider(argThat(
                 provider -> provider.getName().contains("VpnNetworkProvider")
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
index 4f849d2..2178b33 100644
--- a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -92,6 +92,8 @@
     private Handler mHandler;
     private EthernetNetworkFactory mNetFactory = null;
     private IpClientCallbacks mIpClientCallbacks;
+    private NetworkOfferCallback mNetworkOfferCallback;
+    private NetworkRequest mRequestToKeepNetworkUp;
     @Mock private Context mContext;
     @Mock private Resources mResources;
     @Mock private EthernetNetworkFactory.Dependencies mDeps;
@@ -244,7 +246,9 @@
         ArgumentCaptor<NetworkOfferCallback> captor = ArgumentCaptor.forClass(
                 NetworkOfferCallback.class);
         verify(mNetworkProvider).registerNetworkOffer(any(), any(), any(), captor.capture());
-        captor.getValue().onNetworkNeeded(createDefaultRequest());
+        mRequestToKeepNetworkUp = createDefaultRequest();
+        mNetworkOfferCallback = captor.getValue();
+        mNetworkOfferCallback.onNetworkNeeded(mRequestToKeepNetworkUp);
 
         verifyStart(ipConfig);
         clearInvocations(mDeps);
@@ -625,6 +629,14 @@
     }
 
     @Test
+    public void testUpdateInterfaceAbortsOnNetworkUneededRemovesAllRequests() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetworkOfferCallback.onNetworkUnneeded(mRequestToKeepNetworkUp));
+    }
+
+    @Test
     public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
         initEthernetNetworkFactory();
         final NetworkCapabilities capabilities = createDefaultFilterCaps();
diff --git a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
index 5a4ad87..f9cbb10 100644
--- a/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
+++ b/tests/unit/java/com/android/server/net/NetworkStatsServiceTest.java
@@ -95,13 +95,16 @@
 import android.app.AlarmManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
 import android.database.ContentObserver;
+import android.net.ConnectivityResources;
 import android.net.DataUsageRequest;
 import android.net.INetd;
 import android.net.INetworkStatsSession;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
+import android.net.NetworkIdentity;
 import android.net.NetworkStateSnapshot;
 import android.net.NetworkStats;
 import android.net.NetworkStatsCollection;
@@ -128,6 +131,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 
+import com.android.connectivity.resources.R;
 import com.android.internal.util.FileRotator;
 import com.android.internal.util.test.BroadcastInterceptingContext;
 import com.android.net.module.util.IBpfMap;
@@ -165,6 +169,7 @@
 import java.time.temporal.ChronoUnit;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -247,6 +252,8 @@
     private @Mock PersistentInt mImportLegacyAttemptsCounter;
     private @Mock PersistentInt mImportLegacySuccessesCounter;
     private @Mock PersistentInt mImportLegacyFallbacksCounter;
+    private @Mock Resources mResources;
+    private Boolean mIsDebuggable;
 
     private class MockContext extends BroadcastInterceptingContext {
         private final Context mBaseContext;
@@ -307,6 +314,12 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
+
+        // Setup mock resources.
+        final Context mockResContext = mock(Context.class);
+        doReturn(mResources).when(mockResContext).getResources();
+        ConnectivityResources.setResourcesContextForTest(mockResContext);
+
         final Context context = InstrumentationRegistry.getContext();
         mServiceContext = new MockContext(context);
         when(mLocationPermissionChecker.checkCallersLocationPermission(
@@ -462,6 +475,11 @@
             public IBpfMap<UidStatsMapKey, StatsMapValue> getAppUidStatsMap() {
                 return mAppUidStatsMap;
             }
+
+            @Override
+            public boolean isDebuggable() {
+                return mIsDebuggable == Boolean.TRUE;
+            }
         };
     }
 
@@ -1898,19 +1916,127 @@
         //  will decrease the retry counter by 1.
     }
 
+    @Test
+    public void testDataMigration_differentFromFallback() throws Exception {
+        assertStatsFilesExist(false);
+        expectDefaultSettings();
+
+        NetworkStateSnapshot[] states = new NetworkStateSnapshot[]{buildWifiState()};
+
+        mService.notifyNetworkStatus(NETWORKS_WIFI, states, getActiveIface(states),
+                new UnderlyingNetworkInfo[0]);
+
+        // modify some number on wifi, and trigger poll event
+        incrementCurrentTime(HOUR_IN_MILLIS);
+        expectNetworkStatsSummary(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, 1024L, 8L, 2048L, 16L));
+        expectNetworkStatsUidDetail(new NetworkStats(getElapsedRealtime(), 1)
+                .insertEntry(TEST_IFACE, UID_BLUE, SET_DEFAULT, TAG_NONE, 128L, 1L, 128L, 1L, 0L));
+        forcePollAndWaitForIdle();
+        // Simulate shutdown to force persisting data
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(true);
+
+        // Move the files to the legacy directory to simulate an import from old data
+        for (File f : mStatsDir.listFiles()) {
+            Files.move(f.toPath(), mLegacyStatsDir.toPath().resolve(f.getName()));
+        }
+        assertStatsFilesExist(false);
+
+        // Prepare some unexpected data.
+        final NetworkIdentity testWifiIdent = new NetworkIdentity.Builder().setType(TYPE_WIFI)
+                .setWifiNetworkKey(TEST_WIFI_NETWORK_KEY).build();
+        final NetworkStatsCollection.Key unexpectedUidAllkey = new NetworkStatsCollection.Key(
+                Set.of(testWifiIdent), UID_ALL, SET_DEFAULT, 0);
+        final NetworkStatsCollection.Key unexpectedUidBluekey = new NetworkStatsCollection.Key(
+                Set.of(testWifiIdent), UID_BLUE, SET_DEFAULT, 0);
+        final NetworkStatsHistory unexpectedHistory = new NetworkStatsHistory
+                .Builder(965L /* bucketDuration */, 1)
+                .addEntry(new NetworkStatsHistory.Entry(TEST_START, 3L, 55L, 4L, 31L, 10L, 5L))
+                .build();
+
+        // Simulate the platform stats collection somehow is different from what is read from
+        // the fallback method. The service should read them as is. This usually happens when an
+        // OEM has changed the implementation of NetworkStatsDataMigrationUtils inside the platform.
+        final NetworkStatsCollection summaryCollection =
+                getLegacyCollection(PREFIX_XT, false /* includeTags */);
+        summaryCollection.recordHistory(unexpectedUidAllkey, unexpectedHistory);
+        final NetworkStatsCollection uidCollection =
+                getLegacyCollection(PREFIX_UID, false /* includeTags */);
+        uidCollection.recordHistory(unexpectedUidBluekey, unexpectedHistory);
+        mPlatformNetworkStatsCollection.put(PREFIX_DEV, summaryCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_XT, summaryCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_UID, uidCollection);
+        mPlatformNetworkStatsCollection.put(PREFIX_UID_TAG,
+                getLegacyCollection(PREFIX_UID_TAG, true /* includeTags */));
+
+        // Mock zero usage and boot through serviceReady(), verify there is no imported data.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+        assertStatsFilesExist(false);
+
+        // Set the flag and reboot, verify the imported data is not there until next boot.
+        mStoreFilesInApexData = true;
+        mImportLegacyTargetAttempts = 3;
+        mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
+        assertStatsFilesExist(false);
+
+        // Boot through systemReady() again.
+        expectDefaultSettings();
+        expectNetworkStatsUidDetail(buildEmptyStats());
+        expectSystemReady();
+        mService.systemReady();
+
+        // Verify the result read from public API matches the result returned from the importer.
+        assertNetworkTotal(sTemplateWifi, 1024L + 55L, 8L + 4L, 2048L + 31L, 16L + 10L, 0 + 5);
+        assertUidTotal(sTemplateWifi, UID_BLUE,
+                128L + 55L, 1L + 4L, 128L + 31L, 1L + 10L, 0 + 5);
+        assertStatsFilesExist(true);
+        verify(mImportLegacyAttemptsCounter).set(3);
+        verify(mImportLegacySuccessesCounter).set(1);
+    }
+
+    @Test
+    public void testShouldRunComparison() {
+        for (Boolean isDebuggable : Set.of(Boolean.TRUE, Boolean.FALSE)) {
+            mIsDebuggable = isDebuggable;
+            // Verify return false regardless of the device is debuggable.
+            doReturn(0).when(mResources)
+                    .getInteger(R.integer.config_netstats_validate_import);
+            assertShouldRunComparison(false, isDebuggable);
+            // Verify return true regardless of the device is debuggable.
+            doReturn(1).when(mResources)
+                    .getInteger(R.integer.config_netstats_validate_import);
+            assertShouldRunComparison(true, isDebuggable);
+            // Verify return true iff the device is debuggable.
+            for (int testValue : Set.of(-1, 2)) {
+                doReturn(testValue).when(mResources)
+                        .getInteger(R.integer.config_netstats_validate_import);
+                assertShouldRunComparison(isDebuggable, isDebuggable);
+            }
+        }
+    }
+
+    private void assertShouldRunComparison(boolean expected, boolean isDebuggable) {
+        assertEquals("shouldRunComparison (debuggable=" + isDebuggable + "): ",
+                expected, mService.shouldRunComparison());
+    }
+
     private NetworkStatsRecorder makeTestRecorder(File directory, String prefix, Config config,
-            boolean includeTags) {
+            boolean includeTags, boolean wipeOnError) {
         final NetworkStats.NonMonotonicObserver observer =
                 mock(NetworkStats.NonMonotonicObserver.class);
         final DropBoxManager dropBox = mock(DropBoxManager.class);
         return new NetworkStatsRecorder(new FileRotator(
                 directory, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
-                observer, dropBox, prefix, config.bucketDuration, includeTags);
+                observer, dropBox, prefix, config.bucketDuration, includeTags, wipeOnError);
     }
 
     private NetworkStatsCollection getLegacyCollection(String prefix, boolean includeTags) {
         final NetworkStatsRecorder recorder = makeTestRecorder(mLegacyStatsDir, prefix,
-                mSettings.getDevConfig(), includeTags);
+                mSettings.getDevConfig(), includeTags, false);
         return recorder.getOrLoadCompleteLocked();
     }
 
diff --git a/tools/Android.bp b/tools/Android.bp
new file mode 100644
index 0000000..1fa93bb
--- /dev/null
+++ b/tools/Android.bp
@@ -0,0 +1,91 @@
+//
+// Copyright (C) 2022 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 {
+    // See: http://go/android-license-faq
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+// Build tool used to generate jarjar rules for all classes in a jar, except those that are
+// API, UnsupportedAppUsage or otherwise excluded.
+python_binary_host {
+    name: "jarjar-rules-generator",
+    srcs: [
+        "gen_jarjar.py",
+    ],
+    main: "gen_jarjar.py",
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+    visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+genrule_defaults {
+    name: "jarjar-rules-combine-defaults",
+    // Concat files with a line break in the middle
+    cmd: "for src in $(in); do cat $${src}; echo; done > $(out)",
+    defaults_visibility: ["//packages/modules/Connectivity:__subpackages__"],
+}
+
+java_library {
+    name: "jarjar-rules-generator-testjavalib",
+    srcs: ["testdata/java/**/*.java"],
+    visibility: ["//visibility:private"],
+}
+
+// TODO(b/233723405) - Remove this workaround.
+// Temporary work around of b/233723405. Using the module_lib stub directly
+// in the test causes it to sometimes get the dex jar and sometimes get the
+// classes jar due to b/233111644. Statically including it here instead
+// ensures that it will always get the classes jar.
+java_library {
+    name: "framework-connectivity.stubs.module_lib-for-test",
+    visibility: ["//visibility:private"],
+    static_libs: [
+        "framework-connectivity.stubs.module_lib",
+    ],
+    // Not strictly necessary but specified as this MUST not have generate
+    // a dex jar as that will break the tests.
+    compile_dex: false,
+}
+
+python_test_host {
+    name: "jarjar-rules-generator-test",
+    srcs: [
+        "gen_jarjar.py",
+        "gen_jarjar_test.py",
+    ],
+    data: [
+        "testdata/test-jarjar-excludes.txt",
+        "testdata/test-unsupportedappusage.txt",
+        ":framework-connectivity.stubs.module_lib-for-test",
+        ":jarjar-rules-generator-testjavalib",
+    ],
+    main: "gen_jarjar_test.py",
+    version: {
+        py2: {
+            enabled: false,
+        },
+        py3: {
+            enabled: true,
+        },
+    },
+}
diff --git a/tools/gen_jarjar.py b/tools/gen_jarjar.py
new file mode 100755
index 0000000..4c2cf54
--- /dev/null
+++ b/tools/gen_jarjar.py
@@ -0,0 +1,133 @@
+#
+# Copyright (C) 2022 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 script generates jarjar rule files to add a jarjar prefix to all classes, except those
+that are API, unsupported API or otherwise excluded."""
+
+import argparse
+import io
+import re
+import subprocess
+from xml import sax
+from xml.sax.handler import ContentHandler
+from zipfile import ZipFile
+
+
+def parse_arguments(argv):
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        '--jars', nargs='+',
+        help='Path to pre-jarjar JAR. Can be followed by multiple space-separated paths.')
+    parser.add_argument(
+        '--prefix', required=True,
+        help='Package prefix to use for jarjared classes, '
+             'for example "com.android.connectivity" (does not end with a dot).')
+    parser.add_argument(
+        '--output', required=True, help='Path to output jarjar rules file.')
+    parser.add_argument(
+        '--apistubs', nargs='*', default=[],
+        help='Path to API stubs jar. Classes that are API will not be jarjared. Can be followed by '
+             'multiple space-separated paths.')
+    parser.add_argument(
+        '--unsupportedapi', nargs='*', default=[],
+        help='Path to UnsupportedAppUsage hidden API .txt lists. '
+             'Classes that have UnsupportedAppUsage API will not be jarjared. Can be followed by '
+             'multiple space-separated paths.')
+    parser.add_argument(
+        '--excludes', nargs='*', default=[],
+        help='Path to files listing classes that should not be jarjared. Can be followed by '
+             'multiple space-separated paths. '
+             'Each file should contain one full-match regex per line. Empty lines or lines '
+             'starting with "#" are ignored.')
+    return parser.parse_args(argv)
+
+
+def _list_toplevel_jar_classes(jar):
+    """List all classes in a .class .jar file that are not inner classes."""
+    return {_get_toplevel_class(c) for c in _list_jar_classes(jar)}
+
+def _list_jar_classes(jar):
+    with ZipFile(jar, 'r') as zip:
+        files = zip.namelist()
+        assert 'classes.dex' not in files, f'Jar file {jar} is dexed, ' \
+                                           'expected an intermediate zip of .class files'
+        class_len = len('.class')
+        return [f.replace('/', '.')[:-class_len] for f in files
+                if f.endswith('.class') and not f.endswith('/package-info.class')]
+
+
+def _list_hiddenapi_classes(txt_file):
+    out = set()
+    with open(txt_file, 'r') as f:
+        for line in f:
+            if not line.strip():
+                continue
+            assert line.startswith('L') and ';' in line, f'Class name not recognized: {line}'
+            clazz = line.replace('/', '.').split(';')[0][1:]
+            out.add(_get_toplevel_class(clazz))
+    return out
+
+
+def _get_toplevel_class(clazz):
+    """Return the name of the toplevel (not an inner class) enclosing class of the given class."""
+    if '$' not in clazz:
+        return clazz
+    return clazz.split('$')[0]
+
+
+def _get_excludes(path):
+    out = []
+    with open(path, 'r') as f:
+        for line in f:
+            stripped = line.strip()
+            if not stripped or stripped.startswith('#'):
+                continue
+            out.append(re.compile(stripped))
+    return out
+
+
+def make_jarjar_rules(args):
+    excluded_classes = set()
+    for apistubs_file in args.apistubs:
+        excluded_classes.update(_list_toplevel_jar_classes(apistubs_file))
+
+    for unsupportedapi_file in args.unsupportedapi:
+        excluded_classes.update(_list_hiddenapi_classes(unsupportedapi_file))
+
+    exclude_regexes = []
+    for exclude_file in args.excludes:
+        exclude_regexes.extend(_get_excludes(exclude_file))
+
+    with open(args.output, 'w') as outfile:
+        for jar in args.jars:
+            jar_classes = _list_jar_classes(jar)
+            jar_classes.sort()
+            for clazz in jar_classes:
+                if (_get_toplevel_class(clazz) not in excluded_classes and
+                        not any(r.fullmatch(clazz) for r in exclude_regexes)):
+                    outfile.write(f'rule {clazz} {args.prefix}.@0\n')
+                    # Also include jarjar rules for unit tests of the class, so the package matches
+                    outfile.write(f'rule {clazz}Test {args.prefix}.@0\n')
+                    outfile.write(f'rule {clazz}Test$* {args.prefix}.@0\n')
+
+
+def _main():
+    # Pass in None to use argv
+    args = parse_arguments(None)
+    make_jarjar_rules(args)
+
+
+if __name__ == '__main__':
+    _main()
diff --git a/tools/gen_jarjar_test.py b/tools/gen_jarjar_test.py
new file mode 100644
index 0000000..8d8e82b
--- /dev/null
+++ b/tools/gen_jarjar_test.py
@@ -0,0 +1,57 @@
+#  Copyright (C) 2022 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.
+#
+#  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.
+
+import gen_jarjar
+import unittest
+
+
+class TestGenJarjar(unittest.TestCase):
+    def test_gen_rules(self):
+        args = gen_jarjar.parse_arguments([
+            "--jars", "jarjar-rules-generator-testjavalib.jar",
+            "--prefix", "jarjar.prefix",
+            "--output", "test-output-rules.txt",
+            "--apistubs", "framework-connectivity.stubs.module_lib.jar",
+            "--unsupportedapi", "testdata/test-unsupportedappusage.txt",
+            "--excludes", "testdata/test-jarjar-excludes.txt",
+        ])
+        gen_jarjar.make_jarjar_rules(args)
+
+        with open(args.output) as out:
+            lines = out.readlines()
+
+        self.assertListEqual([
+            'rule test.utils.TestUtilClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClassTest$* jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClass jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest jarjar.prefix.@0\n',
+            'rule test.utils.TestUtilClass$TestInnerClassTest$* jarjar.prefix.@0\n'], lines)
+
+
+if __name__ == '__main__':
+    # Need verbosity=2 for the test results parser to find results
+    unittest.main(verbosity=2)
diff --git a/tools/testdata/java/android/net/LinkProperties.java b/tools/testdata/java/android/net/LinkProperties.java
new file mode 100644
index 0000000..bdca377
--- /dev/null
+++ b/tools/testdata/java/android/net/LinkProperties.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Test class with a name matching a public API.
+ */
+public class LinkProperties {
+}
diff --git a/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
new file mode 100644
index 0000000..7e3bee1
--- /dev/null
+++ b/tools/testdata/java/test/jarjarexcluded/JarjarExcludedClass.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 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 test.jarjarexcluded;
+
+/**
+ * Test class that is excluded from jarjar.
+ */
+public class JarjarExcludedClass {
+}
diff --git a/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
new file mode 100644
index 0000000..9d32296
--- /dev/null
+++ b/tools/testdata/java/test/unsupportedappusage/TestUnsupportedAppUsageClass.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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 test.unsupportedappusage;
+
+public class TestUnsupportedAppUsageClass {
+    public void testMethod() {}
+}
diff --git a/tools/testdata/java/test/utils/TestUtilClass.java b/tools/testdata/java/test/utils/TestUtilClass.java
new file mode 100644
index 0000000..2162e45
--- /dev/null
+++ b/tools/testdata/java/test/utils/TestUtilClass.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 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 test.utils;
+
+/**
+ * Sample class to test jarjar rules.
+ */
+public class TestUtilClass {
+    public static class TestInnerClass {}
+}
diff --git a/tools/testdata/test-jarjar-excludes.txt b/tools/testdata/test-jarjar-excludes.txt
new file mode 100644
index 0000000..35d97a2
--- /dev/null
+++ b/tools/testdata/test-jarjar-excludes.txt
@@ -0,0 +1,3 @@
+# Test file for excluded classes
+test\.jarj.rexcluded\.JarjarExcludedCla.s
+test\.jarjarexcluded\.JarjarExcludedClass\$TestInnerCl.ss
diff --git a/tools/testdata/test-unsupportedappusage.txt b/tools/testdata/test-unsupportedappusage.txt
new file mode 100644
index 0000000..331eff9
--- /dev/null
+++ b/tools/testdata/test-unsupportedappusage.txt
@@ -0,0 +1 @@
+Ltest/unsupportedappusage/TestUnsupportedAppUsageClass;->testMethod()V
\ No newline at end of file