Merge "Add unit tests to com.android.server.nearby.fastpair.cache" into tm-mainline-prod
diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
index 237a645..819936d 100644
--- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
+++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java
@@ -26,8 +26,8 @@
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL;
 import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL;
 import static android.net.TetheringManager.TETHERING_ETHERNET;
-import static android.net.TetheringTester.RemoteResponder;
-import static android.net.TetheringTester.isIcmpv6Type;
+import static android.net.TetheringTester.isExpectedIcmpv6Packet;
+import static android.net.TetheringTester.isExpectedUdpPacket;
 import static android.system.OsConstants.IPPROTO_IP;
 import static android.system.OsConstants.IPPROTO_IPV6;
 import static android.system.OsConstants.IPPROTO_UDP;
@@ -83,10 +83,7 @@
 import com.android.net.module.util.bpf.Tether4Value;
 import com.android.net.module.util.bpf.TetherStatsKey;
 import com.android.net.module.util.bpf.TetherStatsValue;
-import com.android.net.module.util.structs.EthernetHeader;
-import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
-import com.android.net.module.util.structs.UdpHeader;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
@@ -333,7 +330,7 @@
         final long deadline = SystemClock.uptimeMillis() + timeoutMs;
         do {
             byte[] pkt = reader.popPacket(timeoutMs);
-            if (isIcmpv6Type(pkt, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return;
+            if (isExpectedIcmpv6Packet(pkt, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return;
 
             timeoutMs = deadline - SystemClock.uptimeMillis();
         } while (timeoutMs > 0);
@@ -793,61 +790,28 @@
 
     @Test
     public void testIcmpv6Echo() throws Exception {
-        assumeFalse(mEm.isAvailable());
-
-        // MyTetheringEventCallback currently only support await first available upstream. Tethering
-        // may select internet network as upstream if test network is not available and not be
-        // preferred yet. Create test upstream network before enable tethering.
-        mUpstreamTracker = createTestUpstream(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
-                toList(TEST_IP4_DNS, TEST_IP6_DNS));
-
-        mDownstreamIface = createTestInterface();
-        mEm.setIncludeTestInterfaces(true);
-
-        final String iface = mTetheredInterfaceRequester.getInterface();
-        assertEquals("TetheredInterfaceCallback for unexpected interface",
-                mDownstreamIface.getInterfaceName(), iface);
-
-        mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
-                mUpstreamTracker.getNetwork());
-        assertEquals("onUpstreamChanged for unexpected network", mUpstreamTracker.getNetwork(),
-                mTetheringEventCallback.awaitUpstreamChanged());
-
-        mDownstreamReader = makePacketReader(mDownstreamIface);
-        mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
-
-        runPing6Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader));
+        runPing6Test(initTetheringTester(toList(TEST_IP4_ADDR, TEST_IP6_ADDR),
+                toList(TEST_IP4_DNS, TEST_IP6_DNS)));
     }
 
-    private void runPing6Test(TetheringTester tester, RemoteResponder remote) throws Exception {
-        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
-        // TetheringTester test ipv6 tethering connectivity before testing ipv6.
-        // TODO: move to a common place to avoid that every IPv6 test needs to call this function.
-        tester.waitForIpv6TetherConnectivityVerified();
-
+    private void runPing6Test(TetheringTester tester) throws Exception {
         TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString("1:2:3:4:5:6"),
                 true /* hasIpv6 */);
         Inet6Address remoteIp6Addr = (Inet6Address) parseNumericAddress("2400:222:222::222");
         ByteBuffer request = Ipv6Utils.buildEchoRequestPacket(tethered.macAddr,
                 tethered.routerMacAddr, tethered.ipv6Addr, remoteIp6Addr);
-        tester.sendPacket(request);
-
-        final byte[] echoRequest = remote.getNextMatchedPacket((p) -> {
+        tester.verifyUpload(request, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
 
-            return isIcmpv6Type(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
+            return isExpectedIcmpv6Packet(p, false /* hasEth */, ICMPV6_ECHO_REQUEST_TYPE);
         });
-        assertNotNull("No icmpv6 echo request in upstream", echoRequest);
 
         ByteBuffer reply = Ipv6Utils.buildEchoReplyPacket(remoteIp6Addr, tethered.ipv6Addr);
-        remote.sendPacket(reply);
-
-        final byte[] echoReply = tester.getNextMatchedPacket((p) -> {
+        tester.verifyDownload(reply, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
 
-            return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
+            return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ECHO_REPLY_TYPE);
         });
-        assertNotNull("No icmpv6 echo reply in downstream", echoReply);
     }
 
     // Test network topology:
@@ -879,28 +843,6 @@
     private static final ByteBuffer PAYLOAD3 =
             ByteBuffer.wrap(new byte[] { (byte) 0x9a, (byte) 0xbc });
 
-    private boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEther,
-            boolean isIpv4, @NonNull final ByteBuffer payload) {
-        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
-
-        if (hasEther) {
-            if (Struct.parse(EthernetHeader.class, buf) == null) return false;
-        }
-
-        if (isIpv4) {
-            if (Struct.parse(Ipv4Header.class, buf) == null) return false;
-        } else {
-            if (Struct.parse(Ipv6Header.class, buf) == null) return false;
-        }
-
-        if (Struct.parse(UdpHeader.class, buf) == null) return false;
-
-        if (buf.remaining() != payload.limit()) return false;
-
-        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
-                payload.array());
-    }
-
     @NonNull
     private ByteBuffer buildUdpPacket(
             @Nullable final MacAddress srcMac, @Nullable final MacAddress dstMac,
@@ -961,10 +903,15 @@
                 dstPort, payload);
     }
 
-    // TODO: remove this verification once upstream connected notification race is fixed.
-    // See #runUdp4Test.
-    private boolean isIpv4TetherConnectivityVerified(TetheringTester tester,
-            RemoteResponder remote, 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 */,
@@ -972,19 +919,22 @@
 
         // Send a UDP packet from client and check the packet can be found on upstream interface.
         for (int i = 0; i < TETHER_REACHABILITY_ATTEMPTS; i++) {
-            tester.sendPacket(probePacket);
-            byte[] expectedPacket = remote.getNextMatchedPacket(p -> {
+            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, RemoteResponder remote, boolean usingBpf)
-            throws Exception {
+    private void runUdp4Test(TetheringTester tester, boolean usingBpf) throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
                 "1:2:3:4:5:6"), false /* hasIpv6 */);
 
@@ -994,14 +944,14 @@
         // 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, remote, tethered));
+        probeV4TetheringConnectivity(tester, tethered, false /* is4To6 */);
 
         // Send a UDP packet in original direction.
         final ByteBuffer originalPacket = buildUdpPacket(tethered.macAddr,
                 tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                 REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 PAYLOAD /* payload */);
-        tester.verifyUpload(remote, originalPacket, p -> {
+        tester.verifyUpload(originalPacket, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
         });
@@ -1011,7 +961,7 @@
         final ByteBuffer replyPacket = buildUdpPacket(REMOTE_IP4_ADDR /* srcIp */,
                 publicIp4Addr /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
                 PAYLOAD2 /* payload */);
-        remote.verifyDownload(tester, replyPacket, p -> {
+        tester.verifyDownload(replyPacket, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
         });
@@ -1030,7 +980,7 @@
                     tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                     REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */,
                     REMOTE_PORT /* dstPort */, PAYLOAD3 /* payload */);
-            tester.verifyUpload(remote, originalPacket2, p -> {
+            tester.verifyUpload(originalPacket2, p -> {
                 Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
                 return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD3);
             });
@@ -1066,7 +1016,7 @@
 
             // Send packets on original direction.
             for (int i = 0; i < TX_UDP_PACKET_COUNT; i++) {
-                tester.verifyUpload(remote, originalPacket, p -> {
+                tester.verifyUpload(originalPacket, p -> {
                     Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
                     return isExpectedUdpPacket(p, false /* hasEther */, true /* isIpv4 */, PAYLOAD);
                 });
@@ -1074,7 +1024,7 @@
 
             // Send packets on reply direction.
             for (int i = 0; i < RX_UDP_PACKET_COUNT; i++) {
-                remote.verifyDownload(tester, replyPacket, p -> {
+                tester.verifyDownload(replyPacket, p -> {
                     Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
                     return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
                 });
@@ -1101,8 +1051,8 @@
         }
     }
 
-    void initializeTethering(List<LinkAddress> upstreamAddresses, List<InetAddress> upstreamDnses)
-            throws Exception {
+    private TetheringTester initTetheringTester(List<LinkAddress> upstreamAddresses,
+            List<InetAddress> upstreamDnses) throws Exception {
         assumeFalse(mEm.isAvailable());
 
         // MyTetheringEventCallback currently only support await first available upstream. Tethering
@@ -1113,9 +1063,9 @@
         mDownstreamIface = createTestInterface();
         mEm.setIncludeTestInterfaces(true);
 
-        final String iface = mTetheredInterfaceRequester.getInterface();
+        // Make sure EtherentTracker use "mDownstreamIface" as server mode interface.
         assertEquals("TetheredInterfaceCallback for unexpected interface",
-                mDownstreamIface.getInterfaceName(), iface);
+                mDownstreamIface.getInterfaceName(), mTetheredInterfaceRequester.getInterface());
 
         mTetheringEventCallback = enableEthernetTethering(mDownstreamIface.getInterfaceName(),
                 mUpstreamTracker.getNetwork());
@@ -1124,13 +1074,22 @@
 
         mDownstreamReader = makePacketReader(mDownstreamIface);
         mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
+
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
+        // sure tethering already have ipv6 connectivity before testing.
+        if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
+            waitForRouterAdvertisement(mDownstreamReader, mDownstreamIface.getInterfaceName(),
+                    WAIT_RA_TIMEOUT_MS);
+        }
+
+        return new TetheringTester(mDownstreamReader, mUpstreamReader);
     }
 
     @Test
     @IgnoreAfter(Build.VERSION_CODES.R)
     public void testTetherUdpV4UpToR() throws Exception {
-        initializeTethering(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)),
                 false /* usingBpf */);
     }
 
@@ -1165,15 +1124,13 @@
     @Test
     @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testTetherUdpV4AfterR() throws Exception {
-        initializeTethering(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS));
         final String kernelVersion = VintfRuntimeInfo.getKernelVersion();
         boolean usingBpf = isUdpOffloadSupportedByKernel(kernelVersion);
         if (!usingBpf) {
             Log.i(TAG, "testTetherUdpV4AfterR will skip BPF offload test for kernel "
                     + kernelVersion);
         }
-        runUdp4Test(new TetheringTester(mDownstreamReader), new RemoteResponder(mUpstreamReader),
-                usingBpf);
+        runUdp4Test(initTetheringTester(toList(TEST_IP4_ADDR), toList(TEST_IP4_DNS)), usingBpf);
     }
 
     @Nullable
@@ -1232,33 +1189,16 @@
         return null;
     }
 
-    @Nullable
-    private Inet6Address getClatIpv6Address(TetheringTester tester,
-            RemoteResponder remote, 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);
-
+    @NonNull
+    private Inet6Address getClatIpv6Address(TetheringTester tester, TetheredDevice tethered)
+            throws Exception {
         // 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++) {
-            tester.sendPacket(probePacket);
-            expectedPacket = remote.getNextMatchedPacket(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:
@@ -1275,19 +1215,12 @@
     // sending out an IPv4 packet and extracting the source address from CLAT translated IPv6
     // packet.
     //
-    private void runClatUdpTest(TetheringTester tester, RemoteResponder remote)
-            throws Exception {
-        // Currently tethering don't have API to tell when ipv6 tethering is available. Thus, let
-        // TetheringTester test ipv6 tethering connectivity before testing ipv6.
-        // TODO: move to a common place to avoid that every IPv6 test needs to call this function.
-        tester.waitForIpv6TetherConnectivityVerified();
-
+    private void runClatUdpTest(TetheringTester tester) throws Exception {
         final TetheredDevice tethered = tester.createTetheredDevice(MacAddress.fromString(
                 "1:2:3:4:5:6"), true /* hasIpv6 */);
 
         // Get CLAT IPv6 address.
-        final Inet6Address clatAddr6 = getClatIpv6Address(tester, remote, tethered);
-        assertNotNull(clatAddr6);
+        final Inet6Address clatAddr6 = getClatIpv6Address(tester, tethered);
 
         // Send an IPv4 UDP packet in original direction.
         // IPv4 packet -- CLAT translation --> IPv6 packet
@@ -1295,7 +1228,7 @@
                 tethered.routerMacAddr, tethered.ipv4Addr /* srcIp */,
                 REMOTE_IP4_ADDR /* dstIp */, LOCAL_PORT /* srcPort */, REMOTE_PORT /* dstPort */,
                 PAYLOAD /* payload */);
-        tester.verifyUpload(remote, originalPacket, p -> {
+        tester.verifyUpload(originalPacket, p -> {
             Log.d(TAG, "Packet in upstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, false /* hasEther */, false /* isIpv4 */, PAYLOAD);
         });
@@ -1305,7 +1238,7 @@
         final ByteBuffer replyPacket = buildUdpPacket(REMOTE_NAT64_ADDR /* srcIp */,
                 clatAddr6 /* dstIp */, REMOTE_PORT /* srcPort */, LOCAL_PORT /* dstPort */,
                 PAYLOAD2 /* payload */);
-        remote.verifyDownload(tester, replyPacket, p -> {
+        tester.verifyDownload(replyPacket, p -> {
             Log.d(TAG, "Packet in downstream: " + dumpHexString(p));
             return isExpectedUdpPacket(p, true /* hasEther */, true /* isIpv4 */, PAYLOAD2);
         });
@@ -1317,9 +1250,7 @@
     @IgnoreUpTo(Build.VERSION_CODES.R)
     public void testTetherClatUdp() throws Exception {
         // CLAT only starts on IPv6 only network.
-        initializeTethering(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS));
-        runClatUdpTest(new TetheringTester(mDownstreamReader),
-                new RemoteResponder(mUpstreamReader));
+        runClatUdpTest(initTetheringTester(toList(TEST_IP6_ADDR), toList(TEST_IP6_DNS)));
     }
 
     private <T> List<T> toList(T... array) {
diff --git a/Tethering/tests/integration/src/android/net/TetheringTester.java b/Tethering/tests/integration/src/android/net/TetheringTester.java
index 458680a..4d90d39 100644
--- a/Tethering/tests/integration/src/android/net/TetheringTester.java
+++ b/Tethering/tests/integration/src/android/net/TetheringTester.java
@@ -18,11 +18,13 @@
 
 import static android.net.InetAddresses.parseNumericAddress;
 import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.IPPROTO_UDP;
 
 import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
 import static com.android.net.module.util.NetworkStackConstants.ARP_REQUEST;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_ADDR_LEN;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
+import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV4;
 import static com.android.net.module.util.NetworkStackConstants.ETHER_TYPE_IPV6;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_PIO;
 import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ND_OPTION_SLLA;
@@ -42,17 +44,20 @@
 import android.util.ArrayMap;
 import android.util.Log;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.android.net.module.util.Ipv6Utils;
 import com.android.net.module.util.Struct;
 import com.android.net.module.util.structs.EthernetHeader;
 import com.android.net.module.util.structs.Icmpv6Header;
+import com.android.net.module.util.structs.Ipv4Header;
 import com.android.net.module.util.structs.Ipv6Header;
 import com.android.net.module.util.structs.LlaOption;
 import com.android.net.module.util.structs.NsHeader;
 import com.android.net.module.util.structs.PrefixInformationOption;
 import com.android.net.module.util.structs.RaHeader;
+import com.android.net.module.util.structs.UdpHeader;
 import com.android.networkstack.arp.ArpPacket;
 import com.android.testutils.TapPacketReader;
 
@@ -61,6 +66,7 @@
 import java.net.InetAddress;
 import java.nio.ByteBuffer;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Random;
 import java.util.concurrent.TimeoutException;
@@ -88,11 +94,17 @@
 
     private final ArrayMap<MacAddress, TetheredDevice> mTetheredDevices;
     private final TapPacketReader mDownstreamReader;
+    private final TapPacketReader mUpstreamReader;
 
     public TetheringTester(TapPacketReader downstream) {
+        this(downstream, null);
+    }
+
+    public TetheringTester(TapPacketReader downstream, TapPacketReader upstream) {
         if (downstream == null) fail("Downstream reader could not be NULL");
 
         mDownstreamReader = downstream;
+        mUpstreamReader = upstream;
         mTetheredDevices = new ArrayMap<>();
     }
 
@@ -170,7 +182,7 @@
     }
 
     private DhcpPacket getNextDhcpPacket() throws Exception {
-        final byte[] packet = getNextMatchedPacket((p) -> {
+        final byte[] packet = getDownloadPacket((p) -> {
             // Test whether this is DHCP packet.
             try {
                 DhcpPacket.decodeFullPacket(p, p.length, DhcpPacket.ENCAP_L2);
@@ -213,7 +225,7 @@
                     tethered.ipv4Addr.getAddress() /* sender IP */,
                     (short) ARP_REPLY);
             try {
-                sendPacket(arpReply);
+                sendUploadPacket(arpReply);
             } catch (Exception e) {
                 fail("Failed to reply ARP for " + tethered.ipv4Addr);
             }
@@ -227,9 +239,9 @@
                 tetherMac.toByteArray() /* srcMac */, routerIp.getAddress() /* target IP */,
                 new byte[ETHER_ADDR_LEN] /* target HW address */,
                 tetherIp.getAddress() /* sender IP */, (short) ARP_REQUEST);
-        sendPacket(arpProbe);
+        sendUploadPacket(arpProbe);
 
-        final byte[] packet = getNextMatchedPacket((p) -> {
+        final byte[] packet = getDownloadPacket((p) -> {
             final ArpPacket arpPacket = parseArpPacket(p);
             if (arpPacket == null || arpPacket.opCode != ARP_REPLY) return false;
             return arpPacket.targetIp.equals(tetherIp);
@@ -245,25 +257,11 @@
         return null;
     }
 
-    public void waitForIpv6TetherConnectivityVerified() throws Exception {
-        Log.d(TAG, "Waiting RA multicast");
-
-        // Wait for RA multicast message from router to confirm that the IPv6 tethering
-        // connectivity is ready. We don't extract the router mac address from RA because
-        // we get the router mac address from IPv4 ARP packet. See #getRouterMacAddressFromArp.
-        for (int i = 0; i < READ_RA_ATTEMPTS; i++) {
-            final byte[] raPacket = getNextMatchedPacket((p) -> {
-                return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
-            });
-            if (raPacket != null) return;
-        }
-
-        fail("Could not get RA multicast packet after " + READ_RA_ATTEMPTS + " attempts");
-    }
-
     private List<PrefixInformationOption> getRaPrefixOptions(byte[] packet) {
         ByteBuffer buf = ByteBuffer.wrap(packet);
-        if (!isIcmpv6Type(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) return null;
+        if (!isExpectedIcmpv6Packet(buf, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT)) {
+            fail("Parsing RA packet fail");
+        }
 
         Struct.parse(RaHeader.class, buf);
         final ArrayList<PrefixInformationOption> pioList = new ArrayList<>();
@@ -290,13 +288,10 @@
     private Inet6Address runSlaac(MacAddress srcMac, MacAddress dstMac) throws Exception {
         sendRsPacket(srcMac, dstMac);
 
-        final byte[] raPacket = getNextMatchedPacket((p) -> {
-            return isIcmpv6Type(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
-        });
+        final byte[] raPacket = verifyPacketNotNull("Receive RA fail", getDownloadPacket(p -> {
+            return isExpectedIcmpv6Packet(p, true /* hasEth */, ICMPV6_ROUTER_ADVERTISEMENT);
+        }));
 
-        if (raPacket == null) {
-            fail("Could not get ra for prefix options");
-        }
         final List<PrefixInformationOption> options = getRaPrefixOptions(raPacket);
 
         for (PrefixInformationOption pio : options) {
@@ -312,7 +307,7 @@
             }
         }
 
-        fail("Could not get ipv6 address");
+        fail("No available ipv6 prefix");
         return null;
     }
 
@@ -322,7 +317,7 @@
         ByteBuffer rs = Ipv6Utils.buildRsPacket(srcMac, dstMac, (Inet6Address) LINK_LOCAL,
                 IPV6_ADDR_ALL_NODES_MULTICAST, slla);
 
-        sendPacket(rs);
+        sendUploadPacket(rs);
     }
 
     private void maybeReplyNa(byte[] packet) {
@@ -347,7 +342,7 @@
             ByteBuffer ns = Ipv6Utils.buildNaPacket(tethered.macAddr, tethered.routerMacAddr,
                     nsHdr.target, ipv6Hdr.srcIp, flags, nsHdr.target, tlla);
             try {
-                sendPacket(ns);
+                sendUploadPacket(ns);
             } catch (Exception e) {
                 fail("Failed to reply NA for " + tethered.ipv6Addr);
             }
@@ -356,23 +351,18 @@
         }
     }
 
-    public static boolean isIcmpv6Type(byte[] packet, boolean hasEth, int type) {
+    public static boolean isExpectedIcmpv6Packet(byte[] packet, boolean hasEth, int type) {
         final ByteBuffer buf = ByteBuffer.wrap(packet);
-        return isIcmpv6Type(buf, hasEth, type);
+        return isExpectedIcmpv6Packet(buf, hasEth, type);
     }
 
-    private static boolean isIcmpv6Type(ByteBuffer buf, boolean hasEth, int type) {
+    private static boolean isExpectedIcmpv6Packet(ByteBuffer buf, boolean hasEth, int type) {
         try {
-            if (hasEth) {
-                final EthernetHeader ethHdr = Struct.parse(EthernetHeader.class, buf);
-                if (ethHdr.etherType != ETHER_TYPE_IPV6) return false;
-            }
+            if (hasEth && !hasExpectedEtherHeader(buf, false /* isIpv4 */)) return false;
 
-            final Ipv6Header ipv6Hdr = Struct.parse(Ipv6Header.class, buf);
-            if (ipv6Hdr.nextHeader != (byte) IPPROTO_ICMPV6) return false;
+            if (!hasExpectedIpHeader(buf, false /* isIpv4 */, IPPROTO_ICMPV6)) return false;
 
-            final Icmpv6Header icmpv6Hdr = Struct.parse(Icmpv6Header.class, buf);
-            return icmpv6Hdr.type == (short) type;
+            return Struct.parse(Icmpv6Header.class, buf).type == (short) type;
         } catch (Exception e) {
             // Parsing packet fail means it is not icmpv6 packet.
         }
@@ -380,11 +370,53 @@
         return false;
     }
 
-    public void sendPacket(ByteBuffer packet) throws Exception {
+    private static boolean hasExpectedEtherHeader(@NonNull final ByteBuffer buf, boolean isIpv4)
+            throws Exception {
+        final int expected = isIpv4 ? ETHER_TYPE_IPV4 : ETHER_TYPE_IPV6;
+
+        return Struct.parse(EthernetHeader.class, buf).etherType == expected;
+    }
+
+    private static boolean hasExpectedIpHeader(@NonNull final ByteBuffer buf, boolean isIpv4,
+            int ipProto) throws Exception {
+        if (isIpv4) {
+            return Struct.parse(Ipv4Header.class, buf).protocol == (byte) ipProto;
+        } else {
+            return Struct.parse(Ipv6Header.class, buf).nextHeader == (byte) ipProto;
+        }
+    }
+
+    public static boolean isExpectedUdpPacket(@NonNull final byte[] rawPacket, boolean hasEth,
+            boolean isIpv4, @NonNull final ByteBuffer payload) {
+        final ByteBuffer buf = ByteBuffer.wrap(rawPacket);
+        try {
+            if (hasEth && !hasExpectedEtherHeader(buf, isIpv4)) return false;
+
+            if (!hasExpectedIpHeader(buf, isIpv4, IPPROTO_UDP)) return false;
+
+            if (Struct.parse(UdpHeader.class, buf) == null) return false;
+        } catch (Exception e) {
+            // Parsing packet fail means it is not udp packet.
+            return false;
+        }
+
+        if (buf.remaining() != payload.limit()) return false;
+
+        return Arrays.equals(Arrays.copyOfRange(buf.array(), buf.position(), buf.limit()),
+                payload.array());
+    }
+
+    private void sendUploadPacket(ByteBuffer packet) throws Exception {
         mDownstreamReader.sendResponse(packet);
     }
 
-    public byte[] getNextMatchedPacket(Predicate<byte[]> filter) {
+    private void sendDownloadPacket(ByteBuffer packet) throws Exception {
+        assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+        mUpstreamReader.sendResponse(packet);
+    }
+
+    private byte[] getDownloadPacket(Predicate<byte[]> filter) {
         byte[] packet;
         while ((packet = mDownstreamReader.poll(PACKET_READ_TIMEOUT_MS)) != null) {
             if (filter.test(packet)) return packet;
@@ -396,30 +428,34 @@
         return null;
     }
 
-    public void verifyUpload(final RemoteResponder dst, final ByteBuffer packet,
-            final Predicate<byte[]> filter) throws Exception {
-        sendPacket(packet);
-        assertNotNull("Upload fail", dst.getNextMatchedPacket(filter));
+    private byte[] getUploadPacket(Predicate<byte[]> filter) {
+        assertNotNull("Can't deal with upstream interface in local only mode", mUpstreamReader);
+
+        return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
     }
 
-    public static class RemoteResponder {
-        final TapPacketReader mUpstreamReader;
-        public RemoteResponder(TapPacketReader reader) {
-            mUpstreamReader = reader;
-        }
+    private @NonNull byte[] verifyPacketNotNull(String message, @Nullable byte[] packet) {
+        assertNotNull(message, packet);
 
-        public void sendPacket(ByteBuffer packet) throws Exception {
-            mUpstreamReader.sendResponse(packet);
-        }
+        return packet;
+    }
 
-        public byte[] getNextMatchedPacket(Predicate<byte[]> filter) throws Exception {
-            return mUpstreamReader.poll(PACKET_READ_TIMEOUT_MS, filter);
-        }
+    public byte[] testUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        sendUploadPacket(packet);
 
-        public void verifyDownload(final TetheringTester dst, final ByteBuffer packet,
-                final Predicate<byte[]> filter) throws Exception {
-            sendPacket(packet);
-            assertNotNull("Download fail", dst.getNextMatchedPacket(filter));
-        }
+        return getUploadPacket(filter);
+    }
+
+    public byte[] verifyUpload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        return verifyPacketNotNull("Upload fail", testUpload(packet, filter));
+    }
+
+    public byte[] verifyDownload(final ByteBuffer packet, final Predicate<byte[]> filter)
+            throws Exception {
+        sendDownloadPacket(packet);
+
+        return verifyPacketNotNull("Download fail", getDownloadPacket(filter));
     }
 }
diff --git a/bpf_progs/Android.bp b/bpf_progs/Android.bp
index 23af3e3..ef2a6e7 100644
--- a/bpf_progs/Android.bp
+++ b/bpf_progs/Android.bp
@@ -39,9 +39,6 @@
         "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",
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index cb1714c..2711bef 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -52,25 +52,30 @@
 #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_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0060)
+
+// 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_UGM(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, AID_ROOT, AID_NET_BW_ACCT, 0460)
+
+// 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)
+
+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_RW_NETD(configuration_map, HASH, uint32_t, uint32_t, CONFIGURATION_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)
 
 static __always_inline int is_system_uid(uint32_t uid) {
     return (uid <= MAX_SYSTEM_UID) && (uid >= MIN_SYSTEM_UID);
diff --git a/framework/Android.bp b/framework/Android.bp
index d7de439..24d8cca 100644
--- a/framework/Android.bp
+++ b/framework/Android.bp
@@ -64,7 +64,6 @@
         ":framework-connectivity-sources",
         ":net-utils-framework-common-srcs",
         ":framework-connectivity-api-shared-srcs",
-        ":framework-connectivity-javastream-protos",
     ],
     aidl: {
         generate_get_transaction_name: true,
@@ -90,6 +89,7 @@
         "modules-utils-backgroundthread",
         "modules-utils-build",
         "modules-utils-preconditions",
+        "framework-connectivity-javastream-protos",
     ],
     libs: [
         "app-compat-annotations",
@@ -197,28 +197,16 @@
     visibility: ["//frameworks/base"],
 }
 
-gensrcs {
+java_library {
     name: "framework-connectivity-javastream-protos",
-    depfile: true,
-
-    tools: [
-        "aprotoc",
-        "protoc-gen-javastream",
-        "soong_zip",
+    proto: {
+        type: "stream",
+    },
+    srcs: [":framework-connectivity-protos"],
+    installable: false,
+    sdk_version: "module_current",
+    min_sdk_version: "30",
+    apex_available: [
+        "com.android.tethering",
     ],
-
-    cmd: "mkdir -p $(genDir)/$(in) " +
-        "&& $(location aprotoc) " +
-        "  --plugin=$(location protoc-gen-javastream) " +
-        "  --dependency_out=$(depfile) " +
-        "  --javastream_out=$(genDir)/$(in) " +
-        "  -Iexternal/protobuf/src " +
-        "  -I . " +
-        "  $(in) " +
-        "&& $(location soong_zip) -jar -o $(out) -C $(genDir)/$(in) -D $(genDir)/$(in)",
-
-    srcs: [
-        ":framework-connectivity-protos",
-    ],
-    output_extension: "srcjar",
 }
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index ddac19d..4ce6add 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -51,6 +51,9 @@
     field public static final int BLOCKED_REASON_RESTRICTED_MODE = 8; // 0x8
     field public static final int FIREWALL_CHAIN_DOZABLE = 1; // 0x1
     field public static final int FIREWALL_CHAIN_LOW_POWER_STANDBY = 5; // 0x5
+    field public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7; // 0x7
+    field public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8; // 0x8
+    field public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9; // 0x9
     field public static final int FIREWALL_CHAIN_POWERSAVE = 3; // 0x3
     field public static final int FIREWALL_CHAIN_RESTRICTED = 4; // 0x4
     field public static final int FIREWALL_CHAIN_STANDBY = 2; // 0x2
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index db1d7e9..a3d3896 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -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/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 5769b92..1b0578f 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -997,6 +997,7 @@
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
      */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_1 = 7;
 
     /**
@@ -1004,6 +1005,7 @@
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
      */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_2 = 8;
 
     /**
@@ -1011,6 +1013,7 @@
      * Denylist of apps that will not have network access due to OEM-specific restrictions.
      * @hide
      */
+    @SystemApi(client = MODULE_LIBRARIES)
     public static final int FIREWALL_CHAIN_OEM_DENY_3 = 9;
 
     /** @hide */
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 2c50c73..5917470 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -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;
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/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/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/service/java/com/android/server/nearby/util/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
deleted file mode 100644
index 6021ff6..0000000
--- a/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * 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 android.annotation.Nullable;
-import android.bluetooth.le.ScanRecord;
-import android.os.ParcelUuid;
-import android.util.SparseArray;
-
-import com.android.server.nearby.common.ble.BleFilter;
-import com.android.server.nearby.common.ble.BleRecord;
-
-import java.util.Arrays;
-
-/**
- * Parses Fast Pair information out of {@link BleRecord}s.
- *
- * <p>There are 2 different packet formats that are supported, which is used can be determined by
- * packet length:
- *
- * <p>For 3-byte packets, the full packet is the model ID.
- *
- * <p>For all other packets, the first byte is the header, followed by the model ID, followed by
- * zero or more extra fields. Each field has its own header byte followed by the field value. The
- * packet header is formatted as 0bVVVLLLLR (V = version, L = model ID length, R = reserved) and
- * each extra field header is 0bLLLLTTTT (L = field length, T = field type).
- */
-public class FastPairDecoder {
-
-    private static final int FIELD_TYPE_BLOOM_FILTER = 0;
-    private static final int FIELD_TYPE_BLOOM_FILTER_SALT = 1;
-    private static final int FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION = 2;
-    private static final int FIELD_TYPE_BATTERY = 3;
-    private static final int FIELD_TYPE_BATTERY_NO_NOTIFICATION = 4;
-    public static final int FIELD_TYPE_CONNECTION_STATE = 5;
-    private static final int FIELD_TYPE_RANDOM_RESOLVABLE_DATA = 6;
-
-
-    /** FE2C is the 16-bit Service UUID. The rest is the base UUID. See BluetoothUuid (hidden). */
-    private static final ParcelUuid FAST_PAIR_SERVICE_PARCEL_UUID =
-            ParcelUuid.fromString("0000FE2C-0000-1000-8000-00805F9B34FB");
-
-    /** The filter you use to scan for Fast Pair BLE advertisements. */
-    public static final BleFilter FILTER =
-            new BleFilter.Builder().setServiceData(FAST_PAIR_SERVICE_PARCEL_UUID,
-                    new byte[0]).build();
-
-    // NOTE: Ensure that all bitmasks are always ints, not bytes so that bitshifting works correctly
-    // without needing worry about signing errors.
-    private static final int HEADER_VERSION_BITMASK = 0b11100000;
-    private static final int HEADER_LENGTH_BITMASK = 0b00011110;
-    private static final int HEADER_VERSION_OFFSET = 5;
-    private static final int HEADER_LENGTH_OFFSET = 1;
-
-    private static final int EXTRA_FIELD_LENGTH_BITMASK = 0b11110000;
-    private static final int EXTRA_FIELD_TYPE_BITMASK = 0b00001111;
-    private static final int EXTRA_FIELD_LENGTH_OFFSET = 4;
-    private static final int EXTRA_FIELD_TYPE_OFFSET = 0;
-
-    private static final int MIN_ID_LENGTH = 3;
-    private static final int MAX_ID_LENGTH = 14;
-    private static final int HEADER_INDEX = 0;
-    private static final int HEADER_LENGTH = 1;
-    private static final int FIELD_HEADER_LENGTH = 1;
-
-    // Not using java.util.IllegalFormatException because it is unchecked.
-    private static class IllegalFormatException extends Exception {
-        private IllegalFormatException(String message) {
-            super(message);
-        }
-    }
-
-    /**
-     * Gets model id data from broadcast
-     */
-    @Nullable
-    public static byte[] getModelId(@Nullable byte[] serviceData) {
-        if (serviceData == null) {
-            return null;
-        }
-
-        if (serviceData.length >= MIN_ID_LENGTH) {
-            if (serviceData.length == MIN_ID_LENGTH) {
-                // If the length == 3, all bytes are the ID. See flag docs for more about
-                // endianness.
-                return serviceData;
-            } else {
-                // Otherwise, the first byte is a header which contains the length of the big-endian
-                // model ID that follows. The model ID will be trimmed if it contains leading zeros.
-                int idIndex = 1;
-                int end = idIndex + getIdLength(serviceData);
-                while (serviceData[idIndex] == 0 && end - idIndex > MIN_ID_LENGTH) {
-                    idIndex++;
-                }
-                return Arrays.copyOfRange(serviceData, idIndex, end);
-            }
-        }
-        return null;
-    }
-
-    /** Gets the FastPair service data array if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getServiceDataArray(BleRecord bleRecord) {
-        return bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
-    }
-
-    /** Gets the FastPair service data array if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getServiceDataArray(ScanRecord scanRecord) {
-        return scanRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
-    }
-
-    /** Gets the bloom filter from the extra fields if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getBloomFilter(@Nullable byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER);
-    }
-
-    /** Gets the bloom filter salt from the extra fields if available, otherwise returns null. */
-    @Nullable
-    public static byte[] getBloomFilterSalt(byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_SALT);
-    }
-
-    /**
-     * Gets the suppress notification with bloom filter from the extra fields if available,
-     * otherwise returns null.
-     */
-    @Nullable
-    public static byte[] getBloomFilterNoNotification(@Nullable byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
-    }
-
-    /**
-     * Get random resolvableData
-     */
-    @Nullable
-    public static byte[] getRandomResolvableData(byte[] serviceData) {
-        return getExtraField(serviceData, FIELD_TYPE_RANDOM_RESOLVABLE_DATA);
-    }
-
-    @Nullable
-    private static byte[] getExtraField(@Nullable byte[] serviceData, int fieldId) {
-        if (serviceData == null || serviceData.length < HEADER_INDEX + HEADER_LENGTH) {
-            return null;
-        }
-        try {
-            return getExtraFields(serviceData).get(fieldId);
-        } catch (IllegalFormatException e) {
-            return null;
-        }
-    }
-
-    /** Gets extra field data at the end of the packet, defined by the extra field header. */
-    private static SparseArray<byte[]> getExtraFields(byte[] serviceData)
-            throws IllegalFormatException {
-        SparseArray<byte[]> extraFields = new SparseArray<>();
-        if (getVersion(serviceData) != 0) {
-            return extraFields;
-        }
-        int headerIndex = getFirstExtraFieldHeaderIndex(serviceData);
-        while (headerIndex < serviceData.length) {
-            int length = getExtraFieldLength(serviceData, headerIndex);
-            int index = headerIndex + FIELD_HEADER_LENGTH;
-            int type = getExtraFieldType(serviceData, headerIndex);
-            int end = index + length;
-            if (extraFields.get(type) == null) {
-                if (end <= serviceData.length) {
-                    extraFields.put(type, Arrays.copyOfRange(serviceData, index, end));
-                } else {
-                    throw new IllegalFormatException(
-                            "Invalid length, " + end + " is longer than service data size "
-                                    + serviceData.length);
-                }
-            }
-            headerIndex = end;
-        }
-        return extraFields;
-    }
-
-    /** Checks whether or not a valid ID is included in the service data packet. */
-    public static boolean hasBeaconIdBytes(BleRecord bleRecord) {
-        byte[] serviceData = bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
-        return checkModelId(serviceData);
-    }
-
-    /** Check whether byte array is FastPair model id or not. */
-    public static boolean checkModelId(@Nullable byte[] scanResult) {
-        return scanResult != null
-                // The 3-byte format has no header byte (all bytes are the ID).
-                && (scanResult.length == MIN_ID_LENGTH
-                // Header byte exists. We support only format version 0. (A different version
-                // indicates
-                // a breaking change in the format.)
-                || (scanResult.length > MIN_ID_LENGTH
-                && getVersion(scanResult) == 0
-                && isIdLengthValid(scanResult)));
-    }
-
-    /** Checks whether or not bloom filter is included in the service data packet. */
-    public static boolean hasBloomFilter(BleRecord bleRecord) {
-        return (getBloomFilter(getServiceDataArray(bleRecord)) != null
-                || getBloomFilterNoNotification(getServiceDataArray(bleRecord)) != null);
-    }
-
-    /** Checks whether or not bloom filter is included in the service data packet. */
-    public static boolean hasBloomFilter(ScanRecord scanRecord) {
-        return (getBloomFilter(getServiceDataArray(scanRecord)) != null
-                || getBloomFilterNoNotification(getServiceDataArray(scanRecord)) != null);
-    }
-
-    private static int getVersion(byte[] serviceData) {
-        return serviceData.length == MIN_ID_LENGTH
-                ? 0
-                : (serviceData[HEADER_INDEX] & HEADER_VERSION_BITMASK) >> HEADER_VERSION_OFFSET;
-    }
-
-    private static int getIdLength(byte[] serviceData) {
-        return serviceData.length == MIN_ID_LENGTH
-                ? MIN_ID_LENGTH
-                : (serviceData[HEADER_INDEX] & HEADER_LENGTH_BITMASK) >> HEADER_LENGTH_OFFSET;
-    }
-
-    private static int getFirstExtraFieldHeaderIndex(byte[] serviceData) {
-        return HEADER_INDEX + HEADER_LENGTH + getIdLength(serviceData);
-    }
-
-    private static int getExtraFieldLength(byte[] serviceData, int extraFieldIndex) {
-        return (serviceData[extraFieldIndex] & EXTRA_FIELD_LENGTH_BITMASK)
-                >> EXTRA_FIELD_LENGTH_OFFSET;
-    }
-
-    private static int getExtraFieldType(byte[] serviceData, int extraFieldIndex) {
-        return (serviceData[extraFieldIndex] & EXTRA_FIELD_TYPE_BITMASK) >> EXTRA_FIELD_TYPE_OFFSET;
-    }
-
-    private static boolean isIdLengthValid(byte[] serviceData) {
-        int idLength = getIdLength(serviceData);
-        return MIN_ID_LENGTH <= idLength
-                && idLength <= MAX_ID_LENGTH
-                && idLength + HEADER_LENGTH <= serviceData.length;
-    }
-}
-
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/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/com/android/server/nearby/common/ble/BleFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
index 1d3653b..b8ada31 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,10 @@
 
 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.ParcelUuid;
 import android.util.SparseArray;
 
@@ -44,6 +47,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 +309,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 +353,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 +382,193 @@
         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);
+    }
+
     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 +608,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 +684,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 +790,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 +839,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..aa52e26 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
@@ -238,7 +238,6 @@
         BleRecord record = BleRecord.parseFromBytes(BEACON);
         BleRecord record2 = BleRecord.parseFromBytes(SAME_BEACON);
 
-
         assertThat(record).isEqualTo(record2);
 
         // Different items.
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..05dab09
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.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.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 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);
+    }
+
+    /** 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/FastPairDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
index 1ad04f8..ca3239a 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,90 +58,428 @@
     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 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 fastPairServiceData =
+                new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
         fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
         fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
-        assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+        assertThat(
+                FastPairDecoder.getBloomFilter(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 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);
-        assertThat(
-                FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
-                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
-    }
-
-    @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));
-    }
-
-    @Test
     public void getBloomFilterNoNotification() {
         FastPairServiceData fastPairServiceData =
                 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<>();
@@ -147,23 +494,21 @@
                 throw new RuntimeException("Number of headers and extra fields must match.");
             }
             byte[] serviceData =
-                    Bytes.concat(
-                            mHeader == null ? new byte[0] : new byte[] {mHeader},
-                            mModelId == null ? new byte[0] : Hex.stringToBytes(mModelId));
+                Bytes.concat(
+                    mHeader == null ? new byte[0] : new byte[] {mHeader},
+                    mModelId == null ? new byte[0] : Hex.stringToBytes(mModelId));
             for (int i = 0; i < mExtraFieldHeaders.size(); i++) {
                 serviceData =
-                        Bytes.concat(
-                                serviceData,
-                                mExtraFieldHeaders.get(i) != null
-                                        ? new byte[] {mExtraFieldHeaders.get(i)}
-                                        : new byte[0],
-                                mExtraFields.get(i) != null
-                                        ? Hex.stringToBytes(mExtraFields.get(i))
-                                        : new byte[0]);
+                    Bytes.concat(
+                        serviceData,
+                        mExtraFieldHeaders.get(i) != null
+                            ? new byte[] {mExtraFieldHeaders.get(i)}
+                            : new byte[0],
+                        mExtraFields.get(i) != null
+                            ? Hex.stringToBytes(mExtraFields.get(i))
+                            : new byte[0]);
             }
             return serviceData;
         }
     }
-
-
 }
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..4fbf042
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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 com.google.common.truth.Truth.assertWithMessage;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+/**
+ * Unit-tests for the {@link BloomFilter} class.
+ */
+public abstract class BloomFilterTest extends TestCase {
+    private static final int BYTE_ARRAY_LENGTH = 100;
+
+    private final BloomFilter mBloomFilter =
+            new BloomFilter(new byte[BYTE_ARRAY_LENGTH], newHasher());
+
+    public abstract BloomFilter.Hasher newHasher();
+
+    @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;
+    }
+
+}
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..fb35092
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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;
+
+public class FastPairBloomFilterHasherTest  extends BloomFilterTest{
+
+    @Override
+    public BloomFilter.Hasher newHasher() {
+        return new FastPairBloomFilterHasher();
+    }
+
+    @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/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..ef37323
--- /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();
+        mBluetoothGatt.disconnect();
+        verify(mBluetoothGatt).disconnect();
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
+    public void testClose_callsWrapped() {
+        doNothing().when(mBluetoothGatt).close();
+        mBluetoothGatt.close();
+        verify(mBluetoothGatt).close();
+    }
+}
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..a3978ff
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.graphics.Bitmap;
+
+import org.junit.Test;
+
+public class IconUtilsTest {
+    private static final int MIN_ICON_SIZE = 16;
+    private static final int DESIRED_ICON_SIZE = 32;
+    @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();
+    }
+}
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/netd/BpfHandler.cpp b/netd/BpfHandler.cpp
index 42d0de5..5ae8ab6 100644
--- a/netd/BpfHandler.cpp
+++ b/netd/BpfHandler.cpp
@@ -199,6 +199,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..7e3b94d 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,7 +62,7 @@
 
     BpfMap<uint64_t, UidTagValue> mCookieTagMap;
     BpfMap<StatsKey, StatsValue> mStatsMapA;
-    BpfMap<StatsKey, StatsValue> mStatsMapB;
+    BpfMapRO<StatsKey, StatsValue> mStatsMapB;
     BpfMap<uint32_t, uint32_t> mConfigurationMap;
     BpfMap<uint32_t, uint8_t> mUidPermissionMap;
 
diff --git a/netd/BpfHandlerTest.cpp b/netd/BpfHandlerTest.cpp
index 1bd222d..c0f7e45 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
@@ -65,7 +65,7 @@
         mFakeConfigurationMap.resetMap(BPF_MAP_TYPE_HASH, 1);
         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;
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/native/TrafficController.cpp b/service/native/TrafficController.cpp
index d05e6fa..bce3bb5 100644
--- a/service/native/TrafficController.cpp
+++ b/service/native/TrafficController.cpp
@@ -488,8 +488,6 @@
               oldConfigure.error().message().c_str());
         return -oldConfigure.error().code();
     }
-    Status res;
-    BpfConfig newConfiguration;
     uint32_t match;
     switch (chain) {
         case DOZABLE:
@@ -519,9 +517,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 2eabc26..f84a910 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"
@@ -262,37 +262,6 @@
         EXPECT_TRUE(mTc.mPrivilegedUser.empty());
     }
 
-    void addPrivilegedUid(uid_t uid) {
-        std::vector privilegedUid = {uid};
-        mTc.setPermissionForUids(INetd::PERMISSION_UPDATE_DEVICE_STATS, privilegedUid);
-    }
-
-    void removePrivilegedUid(uid_t uid) {
-        std::vector privilegedUid = {uid};
-        mTc.setPermissionForUids(INetd::PERMISSION_NONE, privilegedUid);
-    }
-
-    void expectFakeStatsUnchanged(uint64_t cookie, uint32_t tag, uint32_t uid,
-                                  StatsKey tagStatsMapKey) {
-        Result<UidTagValue> cookieMapResult = mFakeCookieTagMap.readValue(cookie);
-        EXPECT_RESULT_OK(cookieMapResult);
-        EXPECT_EQ(uid, cookieMapResult.value().uid);
-        EXPECT_EQ(tag, cookieMapResult.value().tag);
-        Result<StatsValue> statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
-        EXPECT_RESULT_OK(statsMapResult);
-        EXPECT_EQ((uint64_t)RXPACKETS, statsMapResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)RXBYTES, statsMapResult.value().rxBytes);
-        tagStatsMapKey.tag = 0;
-        statsMapResult = mFakeStatsMapA.readValue(tagStatsMapKey);
-        EXPECT_RESULT_OK(statsMapResult);
-        EXPECT_EQ((uint64_t)RXPACKETS, statsMapResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)RXBYTES, statsMapResult.value().rxBytes);
-        auto appStatsResult = mFakeAppUidStatsMap.readValue(uid);
-        EXPECT_RESULT_OK(appStatsResult);
-        EXPECT_EQ((uint64_t)RXPACKETS, appStatsResult.value().rxPackets);
-        EXPECT_EQ((uint64_t)RXBYTES, appStatsResult.value().rxBytes);
-    }
-
     Status updateUidOwnerMaps(const std::vector<uint32_t>& appUids,
                               UidOwnerMatchType matchType, TrafficController::IptOp op) {
         Status ret(0);
diff --git a/service/src/com/android/server/connectivity/ClatCoordinator.java b/service/src/com/android/server/connectivity/ClatCoordinator.java
index ea89be6..498cf63 100644
--- a/service/src/com/android/server/connectivity/ClatCoordinator.java
+++ b/service/src/com/android/server/connectivity/ClatCoordinator.java
@@ -596,7 +596,6 @@
         try {
             mNetd.interfaceSetEnableIPv6(tunIface, false /* enabled */);
         } catch (RemoteException | ServiceSpecificException e) {
-            maybeCleanUp(tunFd, readSock6, writeSock6);
             Log.e(TAG, "Disable IPv6 on " + tunIface + " failed: " + e);
         }
 
diff --git a/tests/common/java/android/net/LinkPropertiesTest.java b/tests/common/java/android/net/LinkPropertiesTest.java
index 581ee22..9ed2bb3 100644
--- a/tests/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/common/java/android/net/LinkPropertiesTest.java
@@ -20,7 +20,6 @@
 import static android.net.RouteInfo.RTN_UNICAST;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.ParcelUtils.assertParcelingIsLossless;
 import static com.android.testutils.ParcelUtils.parcelingRoundTrip;
 
@@ -53,6 +52,7 @@
 
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.RuleChain;
 import org.junit.runner.RunWith;
 
 import java.net.Inet4Address;
@@ -68,11 +68,13 @@
 @SmallTest
 @ConnectivityModuleTest
 public class LinkPropertiesTest {
+    // Use a RuleChain to explicitly specify the order of rules. DevSdkIgnoreRule must run before
+    // PlatformCompatChange rule, because otherwise tests with that should be skipped when targeting
+    // target SDK 33 will still attempt to override compat changes (which on user builds will crash)
+    // before being skipped.
     @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
-    @Rule
-    public final PlatformCompatChangeRule compatChangeRule = new PlatformCompatChangeRule();
+    public final RuleChain chain = RuleChain.outerRule(
+            new DevSdkIgnoreRule()).around(new PlatformCompatChangeRule());
 
     private static final InetAddress ADDRV4 = address("75.208.6.1");
     private static final InetAddress ADDRV6 = address("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
@@ -1262,7 +1264,8 @@
         assertFalse(lp.hasIpv4UnreachableDefaultRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.S_V2)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testHasExcludeRoute() {
         LinkProperties lp = new LinkProperties();
@@ -1274,7 +1277,8 @@
         assertTrue(lp.hasExcludeRoute());
     }
 
-    @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testRouteAddWithSameKey() throws Exception {
         LinkProperties lp = new LinkProperties();
@@ -1291,7 +1295,8 @@
         assertEquals(2, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(SC_V2)
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @EnableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesEnabled() {
         final LinkProperties lp = new LinkProperties();
@@ -1307,8 +1312,8 @@
         assertEquals(3, lp.getRoutes().size());
     }
 
-    @Test @IgnoreUpTo(SC_V2)
-    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden on T or above")
+    @Test @IgnoreUpTo(Build.VERSION_CODES.R)
+    @CtsNetTestCasesMaxTargetSdk31(reason = "Compat change cannot be overridden when targeting T+")
     @DisableCompatChanges({LinkProperties.EXCLUDED_ROUTES})
     public void testExcludedRoutesDisabled() {
         final LinkProperties lp = new LinkProperties();
diff --git a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
index b65fb6b..9a1fa42 100644
--- a/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
+++ b/tests/cts/hostside/src/com/android/cts/net/HostsideLinkPropertiesGatingTests.java
@@ -22,6 +22,9 @@
 
 /**
  * Tests for the {@link android.net.LinkProperties#EXCLUDED_ROUTES} compatibility change.
+ *
+ * TODO: see if we can delete this cumbersome host test by moving the coverage to CtsNetTestCases
+ * and CtsNetTestCasesMaxTargetSdk31.
  */
 public class HostsideLinkPropertiesGatingTests extends CompatChangeGatingTestCase {
     private static final String TEST_APK = "CtsHostsideNetworkTestsApp3.apk";
@@ -45,8 +48,19 @@
         runDeviceCompatTest("testExcludedRoutesChangeDisabled");
     }
 
-    public void testExcludedRoutesChangeDisabledByOverride() throws Exception {
+    public void testExcludedRoutesChangeDisabledByOverrideOnDebugBuild() throws Exception {
+        // Must install APK even when skipping test, because tearDown expects uninstall to succeed.
         installPackage(TEST_APK, true);
+
+        // This test uses an app with a target SDK where the compat change is on by default.
+        // Because user builds do not allow overriding compat changes, only run this test on debug
+        // builds. This seems better than deleting this test and not running it anywhere because we
+        // could in the future run this test on userdebug builds in presubmit.
+        //
+        // We cannot use assumeXyz here because CompatChangeGatingTestCase ultimately inherits from
+        // junit.framework.TestCase, which does not understand assumption failures.
+        if ("user".equals(getDevice().getProperty("ro.build.type"))) return;
+
         runDeviceCompatTestWithChangeDisabled("testExcludedRoutesChangeDisabled");
     }
 
diff --git a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
index 6dfadc7..bd1b74a 100644
--- a/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
+++ b/tests/cts/tethering/src/android/tethering/cts/TetheringManagerTest.java
@@ -26,13 +26,8 @@
 import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN;
 import static android.net.TetheringManager.TETHER_ERROR_NO_CHANGE_TETHERING_PERMISSION;
 import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR;
-import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED;
 import static android.net.cts.util.CtsTetheringUtils.isAnyIfaceMatch;
 
-import static com.android.networkstack.apishim.ConstantsShim.KEY_CARRIER_SUPPORTS_TETHERING_BOOL;
-import static com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
-import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -73,19 +68,14 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.TestCarrierConfigReceiver;
-
 import org.junit.After;
 import org.junit.Before;
-import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -94,9 +84,6 @@
 @RunWith(AndroidJUnit4.class)
 public class TetheringManagerTest {
 
-    @Rule
-    public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
-
     private Context mContext;
 
     private ConnectivityManager mCm;
@@ -405,7 +392,7 @@
         // Override carrier config to ignore entitlement check.
         final PersistableBundle bundle = new PersistableBundle();
         bundle.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, false);
-        overrideCarrierConfig(bundle, CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL);
+        overrideCarrierConfig(bundle);
 
         // Verify that requestLatestTetheringEntitlementResult() can get entitlement
         // result TETHER_ERROR_NO_ERROR due to provisioning bypassed.
@@ -413,112 +400,14 @@
                 TETHERING_WIFI, false, c -> c.run(), listener), TETHER_ERROR_NO_ERROR);
 
         // Reset carrier config.
-        overrideCarrierConfig(null, "");
+        overrideCarrierConfig(null);
     }
 
-    @Test
-    @IgnoreUpTo(SC_V2)
-    public void testEnableTethering_carrierUnsupported_noTetheringActive() throws Exception {
-        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
-
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        boolean previousWifiEnabledState = false;
-        try {
-            tetherEventCallback.assumeWifiTetheringSupported(mContext);
-            // Avoid device connected to Wifi network.
-            previousWifiEnabledState = ensureCurrentNetworkIsCellular();
-            final PersistableBundle bundle = new PersistableBundle();
-            bundle.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
-            // Override carrier config to make carrier not support.
-            overrideCarrierConfig(bundle, KEY_CARRIER_SUPPORTS_TETHERING_BOOL);
-
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
-
-            mCtsTetheringUtils.expectSoftApDisabled();
-            tetherEventCallback.expectNoTetheringActive();
-        } finally {
-            overrideCarrierConfig(null, "");
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-            if (previousWifiEnabledState) {
-                mCtsNetUtils.connectToWifi();
-            }
-        }
-    }
-
-    @Test
-    @IgnoreUpTo(SC_V2)
-    public void testEnableTethering_carrierUnsupportByConfigChange_noTetheringActive()
-            throws Exception {
-        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
-
-        final TestTetheringEventCallback tetherEventCallback =
-                mCtsTetheringUtils.registerTetheringEventCallback();
-        boolean previousWifiEnabledState = false;
-        try {
-            tetherEventCallback.assumeWifiTetheringSupported(mContext);
-            // Avoid device connected to Wifi network.
-            previousWifiEnabledState = ensureCurrentNetworkIsCellular();
-            mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
-
-            final PersistableBundle bundle = new PersistableBundle();
-            bundle.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
-            // Override carrier config to make carrier not support.
-            overrideCarrierConfig(bundle, KEY_CARRIER_SUPPORTS_TETHERING_BOOL);
-
-            mCtsTetheringUtils.expectSoftApDisabled();
-            tetherEventCallback.expectNoTetheringActive();
-        } finally {
-            overrideCarrierConfig(null, "");
-            mCtsTetheringUtils.unregisterTetheringEventCallback(tetherEventCallback);
-            if (previousWifiEnabledState) {
-                mCtsNetUtils.connectToWifi();
-            }
-        }
-    }
-
-    @Test
-    @IgnoreUpTo(SC_V2)
-    public void testRequestLatestEntitlementResult_carrierUnsupported() throws Exception {
-        assumeTrue(mTM.isTetheringSupported());
-        assumeTrue(mPm.hasSystemFeature(FEATURE_TELEPHONY));
-
-        final PersistableBundle bundle = new PersistableBundle();
-        bundle.putBoolean(KEY_CARRIER_SUPPORTS_TETHERING_BOOL, false);
-        try {
-            // Override carrier config to make carrier not support.
-            overrideCarrierConfig(bundle, KEY_CARRIER_SUPPORTS_TETHERING_BOOL);
-
-            // Verify that requestLatestTetheringEntitlementResult() can get entitlement
-            // result TETHER_ERROR_PROVISIONING_FAILED due to carrier unsupported
-            assertEntitlementResult(listener -> mTM.requestLatestTetheringEntitlementResult(
-                    TETHERING_WIFI,
-                    false,
-                    c -> c.run(),
-                    listener),
-                    TETHER_ERROR_PROVISIONING_FAILED);
-        } finally {
-            // Reset carrier config.
-            overrideCarrierConfig(null, "");
-        }
-    }
-
-    private void overrideCarrierConfig(PersistableBundle bundle, String configName)
-            throws Exception {
-        final int timeoutMs = 5_000;
-        final int currentSubId = SubscriptionManager.getDefaultSubscriptionId();
-        TestCarrierConfigReceiver configListener =
-                new TestCarrierConfigReceiver(mContext, currentSubId, timeoutMs, bundle,
-                        (configs) -> {
-                            if (bundle == null) {
-                                // This is to restore carrier config and means no need to do match.
-                                return true;
-                            }
-                            boolean requestConfigValue = bundle.getBoolean(configName);
-                            boolean receiveConfigValue = configs.getBoolean(configName);
-                            return Objects.equals(receiveConfigValue, requestConfigValue);
-                        });
-        configListener.overrideCarrierConfigForTest();
+    private void overrideCarrierConfig(PersistableBundle bundle) {
+        final CarrierConfigManager configManager = (CarrierConfigManager) mContext
+                .getSystemService(Context.CARRIER_CONFIG_SERVICE);
+        final int subId = SubscriptionManager.getDefaultSubscriptionId();
+        configManager.overrideConfig(subId, bundle);
     }
 
     @Test
@@ -532,8 +421,30 @@
         try {
             tetherEventCallback.assumeWifiTetheringSupported(mContext);
             tetherEventCallback.expectNoTetheringActive();
-            // Avoid device connected to Wifi network.
-            previousWifiEnabledState = ensureCurrentNetworkIsCellular();
+
+            previousWifiEnabledState = mWm.isWifiEnabled();
+            if (previousWifiEnabledState) {
+                mCtsNetUtils.ensureWifiDisconnected(null);
+            }
+
+            final TestNetworkCallback networkCallback = new TestNetworkCallback();
+            Network activeNetwork = null;
+            try {
+                mCm.registerDefaultNetworkCallback(networkCallback);
+                activeNetwork = networkCallback.waitForAvailable();
+            } finally {
+                mCm.unregisterNetworkCallback(networkCallback);
+            }
+
+            assertNotNull("No active network. Please ensure the device has working mobile data.",
+                    activeNetwork);
+            final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
+
+            // If active nework is ETHERNET, tethering may not use cell network as upstream.
+            assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
+
+            assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
+
             mCtsTetheringUtils.startWifiTethering(tetherEventCallback);
 
             final TelephonyManager telephonyManager = (TelephonyManager) mContext.getSystemService(
@@ -553,36 +464,4 @@
             }
         }
     }
-
-    /**
-     * Make sure current network is cellular data.
-     * @return true Previous Wifi state is enabled, false is disabled.
-     */
-    private boolean ensureCurrentNetworkIsCellular() throws Exception {
-        final boolean previousWifiEnabledState = mWm.isWifiEnabled();
-        if (previousWifiEnabledState) {
-            mCtsNetUtils.ensureWifiDisconnected(null);
-        }
-
-        final TestNetworkCallback networkCallback = new TestNetworkCallback();
-        Network activeNetwork = null;
-        try {
-            mCm.registerDefaultNetworkCallback(networkCallback);
-            activeNetwork = networkCallback.waitForAvailable();
-        } finally {
-            mCm.unregisterNetworkCallback(networkCallback);
-        }
-
-        assertNotNull("No active network. Please ensure the device has working mobile data.",
-                activeNetwork);
-
-        final NetworkCapabilities activeNetCap = mCm.getNetworkCapabilities(activeNetwork);
-
-        // If active nework is ETHERNET, tethering may not use cell network as upstream.
-        assumeFalse(activeNetCap.hasTransport(TRANSPORT_ETHERNET));
-
-        assertTrue(activeNetCap.hasTransport(TRANSPORT_CELLULAR));
-
-        return previousWifiEnabledState;
-    }
 }
diff --git a/tests/unit/java/com/android/server/connectivity/VpnTest.java b/tests/unit/java/com/android/server/connectivity/VpnTest.java
index eb35469..cdfa190 100644
--- a/tests/unit/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/unit/java/com/android/server/connectivity/VpnTest.java
@@ -1315,7 +1315,7 @@
                 config -> Arrays.asList(config.flags).contains(flag)));
     }
 
-    private void setupPlatformVpnWithSpecificExceptionAndItsErrorCode(IkeException exception,
+    private void doTestPlatformVpnWithException(IkeException exception,
             String category, int errorType, int errorCode) throws Exception {
         final ArgumentCaptor<IkeSessionCallback> captor =
                 ArgumentCaptor.forClass(IkeSessionCallback.class);
@@ -1333,6 +1333,7 @@
         // state
         verify(mIkev2SessionCreator, timeout(TEST_TIMEOUT_MS))
                 .createIkeSession(any(), any(), any(), any(), captor.capture(), any());
+        reset(mIkev2SessionCreator);
         final IkeSessionCallback ikeCb = captor.getValue();
         ikeCb.onClosedWithException(exception);
 
@@ -1342,6 +1343,23 @@
         if (errorType == VpnManager.ERROR_CLASS_NOT_RECOVERABLE) {
             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());
+
+            // 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);
         }
     }
 
@@ -1350,7 +1368,7 @@
         final IkeProtocolException exception = mock(IkeProtocolException.class);
         final int errorCode = IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED;
         when(exception.getErrorType()).thenReturn(errorCode);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_NOT_RECOVERABLE,
                 errorCode);
     }
@@ -1360,7 +1378,7 @@
         final IkeProtocolException exception = mock(IkeProtocolException.class);
         final int errorCode = IkeProtocolException.ERROR_TYPE_TEMPORARY_FAILURE;
         when(exception.getErrorType()).thenReturn(errorCode);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_IKE_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE, errorCode);
     }
 
@@ -1370,7 +1388,7 @@
         final UnknownHostException unknownHostException = new UnknownHostException();
         final int errorCode = VpnManager.ERROR_CODE_NETWORK_UNKNOWN_HOST;
         when(exception.getCause()).thenReturn(unknownHostException);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 errorCode);
     }
@@ -1382,7 +1400,7 @@
                 new IkeTimeoutException("IkeTimeoutException");
         final int errorCode = VpnManager.ERROR_CODE_NETWORK_PROTOCOL_TIMEOUT;
         when(exception.getCause()).thenReturn(ikeTimeoutException);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 errorCode);
     }
@@ -1391,7 +1409,7 @@
     public void testStartPlatformVpnFailedWithIkeNetworkLostException() throws Exception {
         final IkeNetworkLostException exception = new IkeNetworkLostException(
                 new Network(100));
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 VpnManager.ERROR_CODE_NETWORK_LOST);
     }
@@ -1402,7 +1420,7 @@
         final IOException ioException = new IOException();
         final int errorCode = VpnManager.ERROR_CODE_NETWORK_IO;
         when(exception.getCause()).thenReturn(ioException);
-        setupPlatformVpnWithSpecificExceptionAndItsErrorCode(exception,
+        doTestPlatformVpnWithException(exception,
                 VpnManager.CATEGORY_EVENT_NETWORK_ERROR, VpnManager.ERROR_CLASS_RECOVERABLE,
                 errorCode);
     }
@@ -1694,6 +1712,11 @@
         public DeviceIdleInternal getDeviceIdleInternal() {
             return mDeviceIdleInternal;
         }
+
+        public long getNextRetryDelaySeconds(int retryCount) {
+            // Simply return retryCount as the delay seconds for retrying.
+            return retryCount;
+        }
     }
 
     /**