Merge "Add icon drawable and Util class"
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
new file mode 100644
index 0000000..f27899f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
@@ -0,0 +1,141 @@
+/*
+ * 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.testing;
+
+import com.android.server.nearby.util.ArrayUtils;
+import com.android.server.nearby.util.Hex;
+
+/**
+ * Test class to provide example to unit test.
+ */
+public class FastPairTestData {
+    private static final byte[] FAST_PAIR_RECORD_BIG_ENDIAN =
+            Hex.stringToBytes("02011E020AF006162CFEAABBCC");
+
+    /**
+     * A Fast Pair frame, Note: The service UUID is FE2C, but in the
+     * packet it's 2CFE, since the core Bluetooth data types are little-endian.
+     *
+     * <p>However, the model ID is big-endian (multi-byte values in our spec are now big-endian, aka
+     * network byte order).
+     *
+     * @see {http://go/fast-pair-service-data}
+     */
+    public static byte[] getFastPairRecord() {
+        return FAST_PAIR_RECORD_BIG_ENDIAN;
+    }
+
+    /** A Fast Pair frame, with a shared account key. */
+    public static final byte[] FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD =
+            Hex.stringToBytes("02011E020AF00C162CFE007011223344556677");
+
+    /** Model ID in {@link #getFastPairRecord()}. */
+    public static final byte[] FAST_PAIR_MODEL_ID = Hex.stringToBytes("AABBCC");
+
+    /** @see #getFastPairRecord() */
+    public static byte[] newFastPairRecord(byte header, byte[] modelId) {
+        return newFastPairRecord(
+                modelId.length == 3 ? modelId : ArrayUtils.concatByteArrays(new byte[] {header},
+                        modelId));
+    }
+
+    /** @see #getFastPairRecord() */
+    public static byte[] newFastPairRecord(byte[] serviceData) {
+        int length = /* length of type and service UUID = */ 3 + serviceData.length;
+        return Hex.stringToBytes(
+                String.format("02011E020AF0%02X162CFE%s", length,
+                        Hex.bytesToStringUppercase(serviceData)));
+    }
+
+    // This is an example of advertising data with AD types
+    public static byte[] adv_1 = {
+            0x02, // Length of this Data
+            0x01, // <<Flags>>
+            0x01, // LE Limited Discoverable Mode
+            0x0A, // Length of this Data
+            0x09, // <<Complete local name>>
+            'P', 'e', 'd', 'o', 'm', 'e', 't', 'e', 'r'
+    };
+
+    // This is an example of advertising data with positive TX Power
+    // Level.
+    public static byte[] adv_2 = {
+            0x02, // Length of this Data
+            0x0a, // <<TX Power Level>>
+            127 // Level = 127
+    };
+
+    // Example data including a service data block
+    public static byte[] sd1 = {
+            0x02, // Length of this Data
+            0x01, // <<Flags>>
+            0x04, // BR/EDR Not Supported.
+            0x03, // Length of this Data
+            0x02, // <<Incomplete List of 16-bit Service UUIDs>>
+            0x04,
+            0x18, // TX Power Service UUID
+            0x1e, // Length of this Data
+            (byte) 0x16, // <<Service Specific Data>>
+            // Service UUID
+            (byte) 0xe0,
+            0x00,
+            // gBeacon Header
+            0x15,
+            // Running time ENCRYPT
+            (byte) 0xd2,
+            0x77,
+            0x01,
+            0x00,
+            // Scan Freq ENCRYPT
+            0x32,
+            0x05,
+            // Time in slow mode
+            0x00,
+            0x00,
+            // Time in fast mode
+            0x7f,
+            0x17,
+            // Subset of UID
+            0x56,
+            0x00,
+            // ID Mask
+            (byte) 0xd4,
+            0x7c,
+            0x18,
+            // RFU (reserved)
+            0x00,
+            // GUID = decimal 1297482358
+            0x76,
+            0x02,
+            0x56,
+            0x4d,
+            0x00,
+            // Ranging Payload Header
+            0x24,
+            // MAC of scanning address
+            (byte) 0xa4,
+            (byte) 0xbb,
+            // NORM RX RSSI -67dBm
+            (byte) 0xb0,
+            // NORM TX POWER -77dBm, so actual TX POWER = -36dBm
+            (byte) 0xb3,
+            // Note based on the values aboves PATH LOSS = (-36) - (-67) = 31dBm
+            // Below zero padding added to test it is handled correctly
+            0x00
+    };
+
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
index 40fefaf..03484a0 100644
--- a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
@@ -56,7 +56,7 @@
             sInstance = new FastPairDataProvider(context);
         }
         if (sInstance.mProxyFastPairDataProvider == null) {
-            Log.wtf(TAG, "no proxy fast pair data provider found");
+            Log.w(TAG, "no proxy fast pair data provider found");
         } else {
             sInstance.mProxyFastPairDataProvider.register();
         }
diff --git a/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
new file mode 100644
index 0000000..599843c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/ArrayUtils.java
@@ -0,0 +1,48 @@
+/*
+ * 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 java.util.Arrays;
+
+/**
+ * ArrayUtils class that help manipulate array.
+ */
+public class ArrayUtils {
+    /** Concatenate N arrays of bytes into a single array. */
+    public static byte[] concatByteArrays(byte[]... arrays) {
+        // Degenerate case - no input provided.
+        if (arrays.length == 0) {
+            return new byte[0];
+        }
+
+        // Compute the total size.
+        int totalSize = 0;
+        for (int i = 0; i < arrays.length; i++) {
+            totalSize += arrays[i].length;
+        }
+
+        // Copy the arrays into the new array.
+        byte[] result = Arrays.copyOf(arrays[0], totalSize);
+        int pos = arrays[0].length;
+        for (int i = 1; i < arrays.length; i++) {
+            byte[] current = arrays[i];
+            System.arraycopy(current, 0, result, pos, current.length);
+            pos += current.length;
+        }
+        return result;
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/Hex.java b/nearby/service/java/com/android/server/nearby/util/Hex.java
index 446204e..1d1d855 100644
--- a/nearby/service/java/com/android/server/nearby/util/Hex.java
+++ b/nearby/service/java/com/android/server/nearby/util/Hex.java
@@ -20,6 +20,11 @@
  * Hex class that contains hex related functions.
  */
 public class Hex {
+
+    private static final char[] HEX_UPPERCASE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+    };
+
     private static final char[] HEX_LOWERCASE = {
             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
     };
@@ -37,4 +42,41 @@
         }
         return new String(hexChars);
     }
+
+    /**
+     * Encodes the byte array to string.
+     */
+    public static String bytesToStringUppercase(byte[] bytes) {
+        return bytesToStringUppercase(bytes, false /* zeroTerminated */);
+    }
+
+    /** Encodes a byte array as a hexadecimal representation of bytes. */
+    public static String bytesToStringUppercase(byte[] bytes, boolean zeroTerminated) {
+        int length = bytes.length;
+        StringBuilder out = new StringBuilder(length * 2);
+        for (int i = 0; i < length; i++) {
+            if (zeroTerminated && i == length - 1 && (bytes[i] & 0xff) == 0) {
+                break;
+            }
+            out.append(HEX_UPPERCASE[(bytes[i] & 0xf0) >>> 4]);
+            out.append(HEX_UPPERCASE[bytes[i] & 0x0f]);
+        }
+        return out.toString();
+    }
+    /**
+     * Converts string to byte array.
+     */
+    public static byte[] stringToBytes(String hex) throws IllegalArgumentException {
+        int length = hex.length();
+        if (length % 2 != 0) {
+            throw new IllegalArgumentException("Hex string has odd number of characters");
+        }
+        byte[] out = new byte[length / 2];
+        for (int i = 0; i < length; i += 2) {
+            // Byte.parseByte() doesn't work here because it expects a hex value in -128, 127, and
+            // our hex values are in 0, 255.
+            out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16);
+        }
+        return out;
+    }
 }
diff --git a/nearby/tests/cts/fastpair/Android.bp b/nearby/tests/cts/fastpair/Android.bp
index 56dc682..9cbd7d0 100644
--- a/nearby/tests/cts/fastpair/Android.bp
+++ b/nearby/tests/cts/fastpair/Android.bp
@@ -39,5 +39,6 @@
     ],
     platform_apis: true,
     sdk_version: "module_current",
-    min_sdk_version: "32",
+    min_sdk_version: "30",
+    target_sdk_version: "32",
 }
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index 19a4a40..aacb6d8 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -25,6 +25,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -38,6 +39,7 @@
     private static final byte[] VALUE = new byte[]{1, 2, 3, 4};
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         CredentialElement element = new CredentialElement(KEY, VALUE);
 
@@ -46,6 +48,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         CredentialElement element = new CredentialElement(KEY, VALUE);
 
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
index eb03a0d..ec6e89a 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -39,6 +40,7 @@
     private static final byte[] VALUE = new byte[]{1, 1, 1, 1};
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         DataElement dataElement = new DataElement(KEY, VALUE);
 
@@ -47,6 +49,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         DataElement dataElement = new DataElement(KEY, VALUE);
 
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index f32ef12..938eab2 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -35,6 +35,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -66,6 +67,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void test_startAndStopScan() {
         ScanRequest scanRequest = new ScanRequest.Builder()
                 .setScanType(ScanRequest.SCAN_TYPE_FAST_PAIR)
@@ -92,6 +94,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testStartStopBroadcast() {
         PrivateCredential credential = new PrivateCredential.Builder(SECRETE_ID, AUTHENTICITY_KEY)
                 .setIdentityType(IDENTITY_TYPE_PRIVATE)
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index a37cc67..67d5aa4 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -30,6 +30,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -74,6 +75,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         PresenceBroadcastRequest broadcastRequest = mBuilder.build();
 
@@ -90,6 +92,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         PresenceBroadcastRequest broadcastRequest = mBuilder.build();
 
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index a1b282d..e0d9200 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -26,6 +26,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -52,6 +53,7 @@
     private static final long DISCOVERY_MILLIS = 100L;
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         PresenceDevice device = new PresenceDevice.Builder()
                 .setDeviceType(DEVICE_TYPE)
@@ -83,6 +85,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         PresenceDevice device = new PresenceDevice.Builder()
                 .setDeviceId(DEVICE_ID)
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index 308be9e..6b7c43b 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -29,6 +29,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,6 +64,7 @@
             .addExtendedProperty(new DataElement(KEY, VALUE));
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         PresenceScanFilter filter = mBuilder.build();
 
@@ -75,6 +77,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         PresenceScanFilter filter = mBuilder.build();
 
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index 5242999..1cd6d9c 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -28,6 +28,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -60,6 +61,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         PrivateCredential credential = mBuilder.build();
 
@@ -76,6 +78,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         PrivateCredential credential = mBuilder.build();
 
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index f750951..7756669 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -29,6 +29,7 @@
 
 import androidx.annotation.RequiresApi;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -63,6 +64,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testBuilder() {
         PublicCredential credential = mBuilder.build();
 
@@ -78,6 +80,7 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 32, codeName = "T")
     public void testWriteParcel() {
         PublicCredential credential = mBuilder.build();
 
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 9b5b14c..55ec645 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -43,6 +43,7 @@
         "platform-test-annotations",
         "service-nearby",
         "truth-prebuilt",
+        "Robolectric_all-target",
     ],
     test_suites: [
         "general-tests",
diff --git a/nearby/tests/unit/AndroidTest.xml b/nearby/tests/unit/AndroidTest.xml
index 9124a07..fdf665d 100644
--- a/nearby/tests/unit/AndroidTest.xml
+++ b/nearby/tests/unit/AndroidTest.xml
@@ -22,7 +22,7 @@
     <option name="test-suite-tag" value="apct" />
     <option name="test-tag" value="NearbyUnitTests" />
     <option name="config-descriptor:metadata" key="mainline-param"
-            value="com.google.android.tethering.apex" />
+            value="com.google.android.tethering.next.apex" />
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="android.nearby.test" />
         <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
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
new file mode 100644
index 0000000..1d3653b
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
@@ -0,0 +1,476 @@
+/*
+ * 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 org.junit.Assert.fail;
+
+import android.bluetooth.BluetoothDevice;
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.nearby.common.ble.testing.FastPairTestData;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@RunWith(AndroidJUnit4.class)
+public class BleFilterTest {
+
+
+    public static final ParcelUuid EDDYSTONE_SERVICE_DATA_PARCELUUID =
+            ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+
+    private ParcelUuid mServiceDataUuid;
+    private BleSighting mBleSighting;
+    private BleFilter.Builder mFilterBuilder;
+
+    @Before
+    public void setUp() throws Exception {
+        // This is the service data UUID in TestData.sd1.
+        // Can't be static because of Robolectric.
+        mServiceDataUuid = ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB");
+
+        byte[] bleRecordBytes =
+                new byte[]{
+                        0x02,
+                        0x01,
+                        0x1a, // advertising flags
+                        0x05,
+                        0x02,
+                        0x0b,
+                        0x11,
+                        0x0a,
+                        0x11, // 16 bit service uuids
+                        0x04,
+                        0x09,
+                        0x50,
+                        0x65,
+                        0x64, // setName
+                        0x02,
+                        0x0A,
+                        (byte) 0xec, // tx power level
+                        0x05,
+                        0x16,
+                        0x0b,
+                        0x11,
+                        0x50,
+                        0x64, // service data
+                        0x05,
+                        (byte) 0xff,
+                        (byte) 0xe0,
+                        0x00,
+                        0x02,
+                        0x15, // manufacturer specific data
+                        0x03,
+                        0x50,
+                        0x01,
+                        0x02, // an unknown data type won't cause trouble
+                };
+
+        mBleSighting = new BleSighting(null /* device */, bleRecordBytes,
+                -10, 1397545200000000L);
+        mFilterBuilder = new BleFilter.Builder();
+    }
+
+    @Test
+    public void setNameFilter() {
+        BleFilter filter = mFilterBuilder.setDeviceName("Ped").build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        filter = mFilterBuilder.setDeviceName("Pem").build();
+        assertThat(filter.matches(mBleSighting)).isFalse();
+    }
+
+    @Test
+    public void setServiceUuidFilter() {
+        BleFilter filter =
+                mFilterBuilder.setServiceUuid(
+                        ParcelUuid.fromString("0000110A-0000-1000-8000-00805F9B34FB"))
+                        .build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        filter =
+                mFilterBuilder.setServiceUuid(
+                        ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"))
+                        .build();
+        assertThat(filter.matches(mBleSighting)).isFalse();
+
+        filter =
+                mFilterBuilder
+                        .setServiceUuid(
+                                ParcelUuid.fromString("0000110C-0000-1000-8000-00805F9B34FB"),
+                                ParcelUuid.fromString("FFFFFFF0-FFFF-FFFF-FFFF-FFFFFFFFFFFF"))
+                        .build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+    }
+
+    @Test
+    public void setServiceDataFilter() {
+        byte[] setServiceData = new byte[]{0x50, 0x64};
+        ParcelUuid serviceDataUuid = ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB");
+        BleFilter filter = mFilterBuilder.setServiceData(serviceDataUuid, setServiceData).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        byte[] emptyData = new byte[0];
+        filter = mFilterBuilder.setServiceData(serviceDataUuid, emptyData).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        byte[] prefixData = new byte[]{0x50};
+        filter = mFilterBuilder.setServiceData(serviceDataUuid, prefixData).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        byte[] nonMatchData = new byte[]{0x51, 0x64};
+        byte[] mask = new byte[]{(byte) 0x00, (byte) 0xFF};
+        filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData, mask).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        filter = mFilterBuilder.setServiceData(serviceDataUuid, nonMatchData).build();
+        assertThat(filter.matches(mBleSighting)).isFalse();
+    }
+
+    @Test
+    public void manufacturerSpecificData() {
+        byte[] setManufacturerData = new byte[]{0x02, 0x15};
+        int manufacturerId = 0xE0;
+        BleFilter filter =
+                mFilterBuilder.setManufacturerData(manufacturerId, setManufacturerData).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        byte[] emptyData = new byte[0];
+        filter = mFilterBuilder.setManufacturerData(manufacturerId, emptyData).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        byte[] prefixData = new byte[]{0x02};
+        filter = mFilterBuilder.setManufacturerData(manufacturerId, prefixData).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        // Data and mask are nullable. Check that we still match when they're null.
+        filter = mFilterBuilder.setManufacturerData(manufacturerId,
+                null /* data */).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+        filter = mFilterBuilder.setManufacturerData(manufacturerId,
+                null /* data */, null /* mask */).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+
+        // Test data mask
+        byte[] nonMatchData = new byte[]{0x02, 0x14};
+        filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData).build();
+        assertThat(filter.matches(mBleSighting)).isFalse();
+        byte[] mask = new byte[]{(byte) 0xFF, (byte) 0x00};
+        filter = mFilterBuilder.setManufacturerData(manufacturerId, nonMatchData, mask).build();
+        assertThat(filter.matches(mBleSighting)).isTrue();
+    }
+
+    @Test
+    public void manufacturerDataNotInBleRecord() {
+        byte[] bleRecord = FastPairTestData.adv_2;
+        // Verify manufacturer with no data
+        byte[] data = {(byte) 0xe0, (byte) 0x00};
+        BleFilter filter = mFilterBuilder.setManufacturerData(0x00e0, data).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test
+    public void manufacturerDataMaskNotInBleRecord() {
+        byte[] bleRecord = FastPairTestData.adv_2;
+
+        // Verify matching partial manufacturer with data and mask
+        byte[] data = {(byte) 0x15};
+        byte[] mask = {(byte) 0xff};
+
+        BleFilter filter = mFilterBuilder
+                .setManufacturerData(0x00e0, data, mask).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+
+    @Test
+    public void serviceData() throws Exception {
+        byte[] bleRecord = FastPairTestData.sd1;
+        byte[] serviceData = {(byte) 0x15};
+
+        // Verify manufacturer 2-byte UUID with no data
+        BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
+        assertMatches(filter, null, 0, bleRecord);
+    }
+
+    @Test
+    public void serviceDataNoMatch() {
+        byte[] bleRecord = FastPairTestData.sd1;
+        byte[] serviceData = {(byte) 0xe1, (byte) 0x00};
+
+        // Verify manufacturer 2-byte UUID with no data
+        BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test
+    public void serviceDataMask() {
+        byte[] bleRecord = FastPairTestData.sd1;
+        BleFilter filter;
+
+        // Verify matching partial manufacturer with data and mask
+        byte[] serviceData1 = {(byte) 0x15};
+        byte[] mask1 = {(byte) 0xff};
+        filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData1, mask1).build();
+        assertMatches(filter, null, 0, bleRecord);
+    }
+
+    @Test
+    public void serviceDataMaskNoMatch() {
+        byte[] bleRecord = FastPairTestData.sd1;
+        BleFilter filter;
+
+        // Verify non-matching partial manufacturer with data and mask
+        byte[] serviceData2 = {(byte) 0xe0, (byte) 0x00, (byte) 0x10};
+        byte[] mask2 = {(byte) 0xff, (byte) 0xff, (byte) 0xff};
+        filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData2, mask2).build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void serviceDataMaskWithDifferentLength() {
+        // Different lengths for data and mask.
+        byte[] serviceData = {(byte) 0xe0, (byte) 0x00, (byte) 0x10};
+        byte[] mask = {(byte) 0xff, (byte) 0xff};
+
+        //expected.expect(IllegalArgumentException.class);
+
+        mFilterBuilder.setServiceData(mServiceDataUuid, serviceData, mask).build();
+    }
+
+
+    @Test
+    public void deviceNameTest() {
+        // Verify the name filter matches
+        byte[] bleRecord = FastPairTestData.adv_1;
+        BleFilter filter = mFilterBuilder.setDeviceName("Pedometer").build();
+        assertMatches(filter, null, 0, bleRecord);
+    }
+
+    @Test
+    public void deviceNameNoMatch() {
+        // Verify the name filter does not match
+        byte[] bleRecord = FastPairTestData.adv_1;
+        BleFilter filter = mFilterBuilder.setDeviceName("Foo").build();
+        assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+    }
+
+    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 assertMatches(
+            BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecordBytes) {
+
+        // Device match.
+        if (filter.getDeviceAddress() != null
+                && (device == null || !filter.getDeviceAddress().equals(device.getAddress()))) {
+            fail("Filter specified a device address ("
+                    + filter.getDeviceAddress()
+                    + ") which doesn't match the actual value: ["
+                    + (device == null ? "null device" : device.getAddress())
+                    + "]");
+        }
+
+        // BLE record is null but there exist filters on it.
+        BleRecord bleRecord = BleRecord.parseFromBytes(bleRecordBytes);
+        if (bleRecord == null
+                && (filter.getDeviceName() != null
+                || filter.getServiceUuid() != null
+                || filter.getManufacturerData() != null
+                || filter.getServiceData() != null)) {
+            fail(
+                    "The bleRecordBytes given parsed to a null bleRecord, but the filter"
+                            + "has a non-null field which depends on the scan record");
+        }
+
+        // Local name match.
+        if (filter.getDeviceName() != null
+                && !filter.getDeviceName().equals(bleRecord.getDeviceName())) {
+            fail(
+                    "The filter's device name ("
+                            + filter.getDeviceName()
+                            + ") doesn't match the scan record device name ("
+                            + bleRecord.getDeviceName()
+                            + ")");
+        }
+
+        // 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");
+        }
+
+        // Service data match
+        if (filter.getServiceDataUuid() != null
+                && !BleFilter.matchesPartialData(
+                filter.getServiceData(),
+                filter.getServiceDataMask(),
+                bleRecord.getServiceData(filter.getServiceDataUuid()))) {
+            fail(
+                    "The filter's service data doesn't match what's in the scan record.\n"
+                            + "Service data: "
+                            + byteString(filter.getServiceData())
+                            + "\n"
+                            + "Service data UUID: "
+                            + filter.getServiceDataUuid().toString()
+                            + "\n"
+                            + "Service data mask: "
+                            + byteString(filter.getServiceDataMask())
+                            + "\n"
+                            + "Scan record service data: "
+                            + byteString(bleRecord.getServiceData(filter.getServiceDataUuid()))
+                            + "\n"
+                            + "Scan record data map:\n"
+                            + byteString(bleRecord.getServiceData()));
+        }
+
+        // Manufacturer data match.
+        if (filter.getManufacturerId() >= 0
+                && !BleFilter.matchesPartialData(
+                filter.getManufacturerData(),
+                filter.getManufacturerDataMask(),
+                bleRecord.getManufacturerSpecificData(filter.getManufacturerId()))) {
+            fail(
+                    "The filter's manufacturer data doesn't match what's in the scan record.\n"
+                            + "Manufacturer ID: "
+                            + filter.getManufacturerId()
+                            + "\n"
+                            + "Manufacturer data: "
+                            + byteString(filter.getManufacturerData())
+                            + "\n"
+                            + "Manufacturer data mask: "
+                            + byteString(filter.getManufacturerDataMask())
+                            + "\n"
+                            + "Scan record manufacturer-specific data: "
+                            + byteString(bleRecord.getManufacturerSpecificData(
+                            filter.getManufacturerId()))
+                            + "\n"
+                            + "Manufacturer data array:\n"
+                            + byteString(bleRecord.getManufacturerSpecificData()));
+        }
+
+        // All filters match.
+        assertThat(
+                matches(filter, device, rssi, bleRecordBytes)).isTrue();
+    }
+
+
+    private static String byteString(byte[] bytes) {
+        if (bytes == null) {
+            return "[null]";
+        } else {
+            final char[] hexArray = "0123456789ABCDEF".toCharArray();
+            char[] hexChars = new char[bytes.length * 2];
+            for (int i = 0; i < bytes.length; i++) {
+                int v = bytes[i] & 0xFF;
+                hexChars[i * 2] = hexArray[v >>> 4];
+                hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+            }
+            return new String(hexChars);
+        }
+    }
+
+    // Ref to beacon.decode.AppleBeaconDecoder.getFilterData
+    private static byte[] getFilterData(ParcelUuid uuid) {
+        byte[] data = new byte[18];
+        data[0] = (byte) 0x02;
+        data[1] = (byte) 0x15;
+        // Check if UUID is needed in data
+        if (uuid != null) {
+            // Convert UUID to array in big endian order
+            byte[] uuidBytes = uuidToByteArray(uuid);
+            for (int i = 0; i < 16; i++) {
+                // Adding uuid bytes in big-endian order to match iBeacon format
+                data[i + 2] = uuidBytes[i];
+            }
+        }
+        return data;
+    }
+
+    // Ref to beacon.decode.AppleBeaconDecoder.uuidToByteArray
+    private static byte[] uuidToByteArray(ParcelUuid uuid) {
+        ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
+        bb.putLong(uuid.getUuid().getMostSignificantBits());
+        bb.putLong(uuid.getUuid().getLeastSignificantBits());
+        return bb.array();
+    }
+
+    private static boolean matchesServiceUuids(
+            ParcelUuid uuid, ParcelUuid parcelUuidMask, List<ParcelUuid> uuids) {
+        if (uuid == null) {
+            return true;
+        }
+
+        for (ParcelUuid parcelUuid : uuids) {
+            UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
+            if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // Check if the uuid pattern matches the particular service uuid.
+    private static boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) {
+        if (mask == null) {
+            return uuid.equals(data);
+        }
+        if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits())
+                != (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) {
+            return false;
+        }
+        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 f4aff1d..5da98e2 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
@@ -1,4 +1,20 @@
 /*
+ * 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.
+ */
+
+/*
  * Copyright (C) 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
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..d356d8e
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.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.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.bluetooth.BluetoothDevice;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.shadows.ShadowBluetoothDevice;
+
+import java.util.concurrent.TimeUnit;
+
+
+@RunWith(AndroidJUnit4.class)
+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 =
+            ShadowBluetoothDevice.newInstance("00:11:22:33:44:55");
+    private final BluetoothDevice mBluetoothDevice2 =
+            ShadowBluetoothDevice.newInstance("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);
+    }
+
+
+    /** 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
new file mode 100644
index 0000000..1ad04f8
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.decode;
+
+import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_MODEL_ID;
+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 androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.nearby.common.ble.BleRecord;
+import com.android.server.nearby.util.Hex;
+
+import com.google.common.primitives.Bytes;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@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";
+    // Bits 3-6 are model ID length bits = 0b0100 = 4
+    private static final byte PADDED_LONG_MODEL_ID_HEADER = 0b00001000;
+    private static final String TRIMMED_LONG_MODEL_ID = "001111";
+    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 String BLOOM_FILTER = "112233445566";
+    private static final byte BLOOM_FILTER_SALT_HEADER = 0b00010001;
+    private static final String BLOOM_FILTER_SALT = "01";
+    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;
+
+
+    @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);
+        assertThat(
+                mDecoder.getBeaconIdBytes(
+                        newBleRecord(fastPairServiceData.createServiceData())))
+                .isEqualTo(Hex.stringToBytes(TRIMMED_LONG_MODEL_ID));
+    }
+
+    @Test
+    public void getBloomFilter() {
+        FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
+                MODEL_ID);
+        fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+        fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+        assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+                .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+    }
+
+    @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));
+    }
+
+    private static BleRecord newBleRecord(byte[] serviceDataBytes) {
+        return parseFromBytes(newFastPairRecord(serviceDataBytes));
+    }
+    class FastPairServiceData {
+        private Byte mHeader;
+        private String mModelId;
+        List<Byte> mExtraFieldHeaders = new ArrayList<>();
+        List<String> mExtraFields = new ArrayList<>();
+
+        FastPairServiceData(Byte header, String modelId) {
+            this.mHeader = header;
+            this.mModelId = modelId;
+        }
+        private byte[] createServiceData() {
+            if (mExtraFieldHeaders.size() != mExtraFields.size()) {
+                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));
+            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]);
+            }
+            return serviceData;
+        }
+    }
+
+
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
new file mode 100644
index 0000000..2926015
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RangingUtilsTest {
+    // relative error to be used in comparing doubles
+    private static final double DELTA = 1e-5;
+
+    @Test
+    public void distanceFromRssi_getCorrectValue() {
+        // Distance expected to be 1.0 meters based on an RSSI/TxPower of -41dBm
+        // Using params: int rssi (dBm), int calibratedTxPower (dBm)
+        double distance = RangingUtils.distanceFromRssi(-41, -41);
+        assertThat(distance).isWithin(DELTA).of(1.0);
+
+        double distance2 = RangingUtils.distanceFromRssi(-70, -50);
+        assertThat(distance2).isWithin(DELTA).of(10.0);
+
+        // testing that the double values are not casted to integers
+        double distance3 = RangingUtils.distanceFromRssi(-67, -77);
+        assertThat(distance3).isWithin(DELTA).of(0.31622776601683794);
+
+        double distance4 = RangingUtils.distanceFromRssi(-50, -70);
+        assertThat(distance4).isWithin(DELTA).of(0.1);
+    }
+
+    @Test
+    public void testRssiFromDistance() {
+        // RSSI expected at 1 meter based on the calibrated tx field of -41dBm
+        // Using params: distance (m), int calibratedTxPower (dBm),
+        int rssi = RangingUtils.rssiFromDistance(1.0, -41);
+
+        assertThat(rssi).isEqualTo(-41);
+    }
+}