Add Fast Pair decoder

Test: to reduce the refactor scope did not add test will add in the
follow up.
Bug: 202335820

Change-Id: I2925d4bce9abd3d554a894be331796af4ae857e9
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
new file mode 100644
index 0000000..23d5170
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
@@ -0,0 +1,746 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * Criteria for filtering BLE devices. A {@link BleFilter} allows clients to restrict BLE devices to
+ * only those that are of interest to them.
+ *
+ *
+ * <p>Current filtering on the following fields are supported:
+ * <li>Service UUIDs which identify the bluetooth gatt services running on the device.
+ * <li>Name of remote Bluetooth LE device.
+ * <li>Mac address of the remote device.
+ * <li>Service data which is the data associated with a service.
+ * <li>Manufacturer specific data which is the data associated with a particular manufacturer.
+ *
+ * @see BleSighting
+ */
+public final class BleFilter implements Parcelable {
+
+    @Nullable
+    private String mDeviceName;
+
+    @Nullable
+    private String mDeviceAddress;
+
+    @Nullable
+    private ParcelUuid mServiceUuid;
+
+    @Nullable
+    private ParcelUuid mServiceUuidMask;
+
+    @Nullable
+    private ParcelUuid mServiceDataUuid;
+
+    @Nullable
+    private byte[] mServiceData;
+
+    @Nullable
+    private byte[] mServiceDataMask;
+
+    private int mManufacturerId;
+
+    @Nullable
+    private byte[] mManufacturerData;
+
+    @Nullable
+    private byte[] mManufacturerDataMask;
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    BleFilter() {
+    }
+
+    BleFilter(
+            @Nullable String deviceName,
+            @Nullable String deviceAddress,
+            @Nullable ParcelUuid serviceUuid,
+            @Nullable ParcelUuid serviceUuidMask,
+            @Nullable ParcelUuid serviceDataUuid,
+            @Nullable byte[] serviceData,
+            @Nullable byte[] serviceDataMask,
+            int manufacturerId,
+            @Nullable byte[] manufacturerData,
+            @Nullable byte[] manufacturerDataMask) {
+        this.mDeviceName = deviceName;
+        this.mDeviceAddress = deviceAddress;
+        this.mServiceUuid = serviceUuid;
+        this.mServiceUuidMask = serviceUuidMask;
+        this.mServiceDataUuid = serviceDataUuid;
+        this.mServiceData = serviceData;
+        this.mServiceDataMask = serviceDataMask;
+        this.mManufacturerId = manufacturerId;
+        this.mManufacturerData = manufacturerData;
+        this.mManufacturerDataMask = manufacturerDataMask;
+    }
+
+    public static final Parcelable.Creator<BleFilter> CREATOR = new Creator<BleFilter>() {
+        @Override
+        public BleFilter createFromParcel(Parcel source) {
+            BleFilter nBleFilter = new BleFilter();
+            nBleFilter.mDeviceName = source.readString();
+            nBleFilter.mDeviceAddress = source.readString();
+            nBleFilter.mManufacturerId = source.readInt();
+            nBleFilter.mManufacturerData = source.marshall();
+            nBleFilter.mManufacturerDataMask = source.marshall();
+            nBleFilter.mServiceDataUuid = source.readParcelable(null);
+            nBleFilter.mServiceData = source.marshall();
+            nBleFilter.mServiceDataMask = source.marshall();
+            nBleFilter.mServiceUuid = source.readParcelable(null);
+            nBleFilter.mServiceUuidMask = source.readParcelable(null);
+            return nBleFilter;
+        }
+
+        @Override
+        public BleFilter[] newArray(int size) {
+            return new BleFilter[size];
+        }
+    };
+
+
+    /** Returns the filter set on the device name field of Bluetooth advertisement data. */
+    @Nullable
+    public String getDeviceName() {
+        return mDeviceName;
+    }
+
+    /** Returns the filter set on the service uuid. */
+    @Nullable
+    public ParcelUuid getServiceUuid() {
+        return mServiceUuid;
+    }
+
+    /** Returns the mask for the service uuid. */
+    @Nullable
+    public ParcelUuid getServiceUuidMask() {
+        return mServiceUuidMask;
+    }
+
+    /** Returns the filter set on the device address. */
+    @Nullable
+    public String getDeviceAddress() {
+        return mDeviceAddress;
+    }
+
+    /** Returns the filter set on the service data. */
+    @Nullable
+    public byte[] getServiceData() {
+        return mServiceData;
+    }
+
+    /** Returns the mask for the service data. */
+    @Nullable
+    public byte[] getServiceDataMask() {
+        return mServiceDataMask;
+    }
+
+    /** Returns the filter set on the service data uuid. */
+    @Nullable
+    public ParcelUuid getServiceDataUuid() {
+        return mServiceDataUuid;
+    }
+
+    /** Returns the manufacturer id. -1 if the manufacturer filter is not set. */
+    public int getManufacturerId() {
+        return mManufacturerId;
+    }
+
+    /** Returns the filter set on the manufacturer data. */
+    @Nullable
+    public byte[] getManufacturerData() {
+        return mManufacturerData;
+    }
+
+    /** Returns the mask for the manufacturer data. */
+    @Nullable
+    public byte[] getManufacturerDataMask() {
+        return mManufacturerDataMask;
+    }
+
+    /**
+     * Check if the filter matches a {@code BleSighting}. A BLE sighting is considered as a match if
+     * it matches all the field filters.
+     */
+    public boolean matches(@Nullable BleSighting bleSighting) {
+        if (bleSighting == null) {
+            return false;
+        }
+        BluetoothDevice device = bleSighting.getDevice();
+        // Device match.
+        if (mDeviceAddress != null && (device == null || !mDeviceAddress.equals(
+                device.getAddress()))) {
+            return false;
+        }
+
+        BleRecord bleRecord = bleSighting.getBleRecord();
+
+        // Scan record is null but there exist filters on it.
+        if (bleRecord == null
+                && (mDeviceName != null
+                || mServiceUuid != null
+                || mManufacturerData != null
+                || mServiceData != null)) {
+            return false;
+        }
+
+        // Local name match.
+        if (mDeviceName != null && !mDeviceName.equals(bleRecord.getDeviceName())) {
+            return false;
+        }
+
+        // UUID match.
+        if (mServiceUuid != null
+                && !matchesServiceUuids(mServiceUuid, mServiceUuidMask,
+                bleRecord.getServiceUuids())) {
+            return false;
+        }
+
+        // Service data match
+        if (mServiceDataUuid != null
+                && !matchesPartialData(
+                mServiceData, mServiceDataMask, bleRecord.getServiceData(mServiceDataUuid))) {
+            return false;
+        }
+
+        // Manufacturer data match.
+        if (mManufacturerId >= 0
+                && !matchesPartialData(
+                mManufacturerData,
+                mManufacturerDataMask,
+                bleRecord.getManufacturerSpecificData(mManufacturerId))) {
+            return false;
+        }
+
+        // All filters match.
+        return true;
+    }
+
+    /**
+     * Determines if the characteristics of this filter are a superset of the characteristics of the
+     * given filter.
+     */
+    public boolean isSuperset(@Nullable BleFilter bleFilter) {
+        if (bleFilter == null) {
+            return false;
+        }
+
+        if (equals(bleFilter)) {
+            return true;
+        }
+
+        // Verify device address matches.
+        if (mDeviceAddress != null && !mDeviceAddress.equals(bleFilter.getDeviceAddress())) {
+            return false;
+        }
+
+        // Verify device name matches.
+        if (mDeviceName != null && !mDeviceName.equals(bleFilter.getDeviceName())) {
+            return false;
+        }
+
+        // Verify UUID is a superset.
+        if (mServiceUuid != null
+                && !serviceUuidIsSuperset(
+                mServiceUuid,
+                mServiceUuidMask,
+                bleFilter.getServiceUuid(),
+                bleFilter.getServiceUuidMask())) {
+            return false;
+        }
+
+        // Verify service data is a superset.
+        if (mServiceDataUuid != null
+                && (!mServiceDataUuid.equals(bleFilter.getServiceDataUuid())
+                || !partialDataIsSuperset(
+                mServiceData,
+                mServiceDataMask,
+                bleFilter.getServiceData(),
+                bleFilter.getServiceDataMask()))) {
+            return false;
+        }
+
+        // Verify manufacturer data is a superset.
+        if (mManufacturerId >= 0
+                && (mManufacturerId != bleFilter.getManufacturerId()
+                || !partialDataIsSuperset(
+                mManufacturerData,
+                mManufacturerDataMask,
+                bleFilter.getManufacturerData(),
+                bleFilter.getManufacturerDataMask()))) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Determines if the first uuid and mask are a superset of the second uuid and mask. */
+    private static boolean serviceUuidIsSuperset(
+            @Nullable ParcelUuid uuid1,
+            @Nullable ParcelUuid uuidMask1,
+            @Nullable ParcelUuid uuid2,
+            @Nullable ParcelUuid uuidMask2) {
+        // First uuid1 is null so it can match any service UUID.
+        if (uuid1 == null) {
+            return true;
+        }
+
+        // uuid2 is a superset of uuid1, but not the other way around.
+        if (uuid2 == null) {
+            return false;
+        }
+
+        // Without a mask, the uuids must match.
+        if (uuidMask1 == null) {
+            return uuid1.equals(uuid2);
+        }
+
+        // Mask2 should be at least as specific as mask1.
+        if (uuidMask2 != null) {
+            long uuid1MostSig = uuidMask1.getUuid().getMostSignificantBits();
+            long uuid1LeastSig = uuidMask1.getUuid().getLeastSignificantBits();
+            long uuid2MostSig = uuidMask2.getUuid().getMostSignificantBits();
+            long uuid2LeastSig = uuidMask2.getUuid().getLeastSignificantBits();
+            if (((uuid1MostSig & uuid2MostSig) != uuid1MostSig)
+                    || ((uuid1LeastSig & uuid2LeastSig) != uuid1LeastSig)) {
+                return false;
+            }
+        }
+
+        if (!matchesServiceUuids(uuid1, uuidMask1, Arrays.asList(uuid2))) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Determines if the first data and mask are the superset of the second data and mask. */
+    private static boolean partialDataIsSuperset(
+            @Nullable byte[] data1,
+            @Nullable byte[] dataMask1,
+            @Nullable byte[] data2,
+            @Nullable byte[] dataMask2) {
+        if (Arrays.equals(data1, data2) && Arrays.equals(dataMask1, dataMask2)) {
+            return true;
+        }
+
+        if (data1 == null) {
+            return true;
+        }
+
+        if (data2 == null) {
+            return false;
+        }
+
+        // Mask2 should be at least as specific as mask1.
+        if (dataMask1 != null && dataMask2 != null) {
+            for (int i = 0, j = 0; i < dataMask1.length && j < dataMask2.length; i++, j++) {
+                if ((dataMask1[i] & dataMask2[j]) != dataMask1[i]) {
+                    return false;
+                }
+            }
+        }
+
+        if (!matchesPartialData(data1, dataMask1, data2)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Check if the uuid pattern is contained in a list of parcel uuids. */
+    private static boolean matchesServiceUuids(
+            @Nullable ParcelUuid uuid, @Nullable ParcelUuid parcelUuidMask,
+            List<ParcelUuid> uuids) {
+        if (uuid == null) {
+            // No service uuid filter has been set, so there's a match.
+            return true;
+        }
+
+        UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid();
+        for (ParcelUuid parcelUuid : uuids) {
+            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, @Nullable 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()));
+    }
+
+    /**
+     * Check whether the data pattern matches the parsed data. Assumes that {@code data} and {@code
+     * dataMask} have the same length.
+     */
+    /* package */
+    static boolean matchesPartialData(
+            @Nullable byte[] data, @Nullable byte[] dataMask, @Nullable byte[] parsedData) {
+        if (data == null || parsedData == null || parsedData.length < data.length) {
+            return false;
+        }
+        if (dataMask == null) {
+            for (int i = 0; i < data.length; ++i) {
+                if (parsedData[i] != data[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        for (int i = 0; i < data.length; ++i) {
+            if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "BleFilter [deviceName="
+                + mDeviceName
+                + ", deviceAddress="
+                + mDeviceAddress
+                + ", uuid="
+                + mServiceUuid
+                + ", uuidMask="
+                + mServiceUuidMask
+                + ", serviceDataUuid="
+                + mServiceDataUuid
+                + ", serviceData="
+                + Arrays.toString(mServiceData)
+                + ", serviceDataMask="
+                + Arrays.toString(mServiceDataMask)
+                + ", manufacturerId="
+                + mManufacturerId
+                + ", manufacturerData="
+                + Arrays.toString(mManufacturerData)
+                + ", manufacturerDataMask="
+                + Arrays.toString(mManufacturerDataMask)
+                + "]";
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mDeviceName);
+        out.writeString(mDeviceAddress);
+        out.writeInt(mManufacturerId);
+        out.writeByteArray(mManufacturerData);
+        out.writeByteArray(mManufacturerDataMask);
+        out.writeParcelable(mServiceDataUuid, flags);
+        out.writeByteArray(mServiceData);
+        out.writeByteArray(mServiceDataMask);
+        out.writeParcelable(mServiceUuid, flags);
+        out.writeParcelable(mServiceUuidMask, flags);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mDeviceName,
+                mDeviceAddress,
+                mManufacturerId,
+                Arrays.hashCode(mManufacturerData),
+                Arrays.hashCode(mManufacturerDataMask),
+                mServiceDataUuid,
+                Arrays.hashCode(mServiceData),
+                Arrays.hashCode(mServiceDataMask),
+                mServiceUuid,
+                mServiceUuidMask);
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        BleFilter other = (BleFilter) obj;
+        return mDeviceName.equals(other.mDeviceName)
+                && mDeviceAddress.equals(other.mDeviceAddress)
+                && mManufacturerId == other.mManufacturerId
+                && Arrays.equals(mManufacturerData, other.mManufacturerData)
+                && Arrays.equals(mManufacturerDataMask, other.mManufacturerDataMask)
+                && mServiceDataUuid.equals(other.mServiceDataUuid)
+                && Arrays.equals(mServiceData, other.mServiceData)
+                && Arrays.equals(mServiceDataMask, other.mServiceDataMask)
+                && mServiceUuid.equals(other.mServiceUuid)
+                && mServiceUuidMask.equals(other.mServiceUuidMask);
+    }
+
+    /** Builder class for {@link BleFilter}. */
+    public static final class Builder {
+
+        private String mDeviceName;
+        private String mDeviceAddress;
+
+        @Nullable
+        private ParcelUuid mServiceUuid;
+        @Nullable
+        private ParcelUuid mUuidMask;
+
+        private ParcelUuid mServiceDataUuid;
+        @Nullable
+        private byte[] mServiceData;
+        @Nullable
+        private byte[] mServiceDataMask;
+
+        private int mManufacturerId = -1;
+        private byte[] mManufacturerData;
+        @Nullable
+        private byte[] mManufacturerDataMask;
+
+        /** Set filter on device name. */
+        public Builder setDeviceName(String deviceName) {
+            this.mDeviceName = deviceName;
+            return this;
+        }
+
+        /**
+         * Set filter on device address.
+         *
+         * @param deviceAddress The device Bluetooth address for the filter. It needs to be in the
+         *                      format of "01:02:03:AB:CD:EF". The device address can be validated
+         *                      using {@link
+         *                      BluetoothAdapter#checkBluetoothAddress}.
+         * @throws IllegalArgumentException If the {@code deviceAddress} is invalid.
+         */
+        public Builder setDeviceAddress(String deviceAddress) {
+            if (!BluetoothAdapter.checkBluetoothAddress(deviceAddress)) {
+                throw new IllegalArgumentException("invalid device address " + deviceAddress);
+            }
+            this.mDeviceAddress = deviceAddress;
+            return this;
+        }
+
+        /** Set filter on service uuid. */
+        public Builder setServiceUuid(@Nullable ParcelUuid serviceUuid) {
+            this.mServiceUuid = serviceUuid;
+            mUuidMask = null; // clear uuid mask
+            return this;
+        }
+
+        /**
+         * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the {@code
+         * serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the bit in
+         * {@code serviceUuid}, and 0 to ignore that bit.
+         *
+         * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but {@code
+         *                                  uuidMask}
+         *                                  is not {@code null}.
+         */
+        public Builder setServiceUuid(@Nullable ParcelUuid serviceUuid,
+                @Nullable ParcelUuid uuidMask) {
+            if (uuidMask != null && serviceUuid == null) {
+                throw new IllegalArgumentException("uuid is null while uuidMask is not null!");
+            }
+            this.mServiceUuid = serviceUuid;
+            this.mUuidMask = uuidMask;
+            return this;
+        }
+
+        /**
+         * Set filtering on service data.
+         */
+        public Builder setServiceData(ParcelUuid serviceDataUuid, @Nullable byte[] serviceData) {
+            this.mServiceDataUuid = serviceDataUuid;
+            this.mServiceData = serviceData;
+            mServiceDataMask = null; // clear service data mask
+            return this;
+        }
+
+        /**
+         * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to
+         * match
+         * the one in service data, otherwise set it to 0 to ignore that bit.
+         *
+         * <p>The {@code serviceDataMask} must have the same length of the {@code serviceData}.
+         *
+         * @throws IllegalArgumentException If {@code serviceDataMask} is {@code null} while {@code
+         *                                  serviceData} is not or {@code serviceDataMask} and
+         *                                  {@code serviceData} has different
+         *                                  length.
+         */
+        public Builder setServiceData(
+                ParcelUuid serviceDataUuid,
+                @Nullable byte[] serviceData,
+                @Nullable byte[] serviceDataMask) {
+            if (serviceDataMask != null) {
+                if (serviceData == null) {
+                    throw new IllegalArgumentException(
+                            "serviceData is null while serviceDataMask is not null");
+                }
+                // Since the serviceDataMask is a bit mask for serviceData, the lengths of the two
+                // byte array need to be the same.
+                if (serviceData.length != serviceDataMask.length) {
+                    throw new IllegalArgumentException(
+                            "size mismatch for service data and service data mask");
+                }
+            }
+            this.mServiceDataUuid = serviceDataUuid;
+            this.mServiceData = serviceData;
+            this.mServiceDataMask = serviceDataMask;
+            return this;
+        }
+
+        /**
+         * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id.
+         *
+         * <p>Note the first two bytes of the {@code manufacturerData} is the manufacturerId.
+         *
+         * @throws IllegalArgumentException If the {@code manufacturerId} is invalid.
+         */
+        public Builder setManufacturerData(int manufacturerId, @Nullable byte[] manufacturerData) {
+            return setManufacturerData(manufacturerId, manufacturerData, null /* mask */);
+        }
+
+        /**
+         * Set filter on partial manufacture data. For any bit in the mask, set it to 1 if it needs
+         * to
+         * match the one in manufacturer data, otherwise set it to 0.
+         *
+         * <p>The {@code manufacturerDataMask} must have the same length of {@code
+         * manufacturerData}.
+         *
+         * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or {@code
+         *                                  manufacturerData} is null while {@code
+         *                                  manufacturerDataMask} is not, or {@code
+         *                                  manufacturerData} and {@code manufacturerDataMask} have
+         *                                  different length.
+         */
+        public Builder setManufacturerData(
+                int manufacturerId,
+                @Nullable byte[] manufacturerData,
+                @Nullable byte[] manufacturerDataMask) {
+            if (manufacturerData != null && manufacturerId < 0) {
+                throw new IllegalArgumentException("invalid manufacture id");
+            }
+            if (manufacturerDataMask != null) {
+                if (manufacturerData == null) {
+                    throw new IllegalArgumentException(
+                            "manufacturerData is null while manufacturerDataMask is not null");
+                }
+                // Since the manufacturerDataMask is a bit mask for manufacturerData, the lengths
+                // of the two byte array need to be the same.
+                if (manufacturerData.length != manufacturerDataMask.length) {
+                    throw new IllegalArgumentException(
+                            "size mismatch for manufacturerData and manufacturerDataMask");
+                }
+            }
+            this.mManufacturerId = manufacturerId;
+            this.mManufacturerData = manufacturerData == null ? new byte[0] : manufacturerData;
+            this.mManufacturerDataMask = manufacturerDataMask;
+            return this;
+        }
+
+
+        /**
+         * Builds the filter.
+         *
+         * @throws IllegalArgumentException If the filter cannot be built.
+         */
+        public BleFilter build() {
+            return new BleFilter(
+                    mDeviceName,
+                    mDeviceAddress,
+                    mServiceUuid,
+                    mUuidMask,
+                    mServiceDataUuid,
+                    mServiceData,
+                    mServiceDataMask,
+                    mManufacturerId,
+                    mManufacturerData,
+                    mManufacturerDataMask);
+        }
+    }
+
+    /**
+     * Changes ble filter to os filter
+     */
+    public ScanFilter toOsFilter() {
+        ScanFilter.Builder osFilterBuilder = new ScanFilter.Builder();
+        if (!TextUtils.isEmpty(getDeviceAddress())) {
+            osFilterBuilder.setDeviceAddress(getDeviceAddress());
+        }
+        if (!TextUtils.isEmpty(getDeviceName())) {
+            osFilterBuilder.setDeviceName(getDeviceName());
+        }
+
+        byte[] manufacturerData = getManufacturerData();
+        if (getManufacturerId() != -1 && manufacturerData != null) {
+            byte[] manufacturerDataMask = getManufacturerDataMask();
+            if (manufacturerDataMask != null) {
+                osFilterBuilder.setManufacturerData(
+                        getManufacturerId(), manufacturerData, manufacturerDataMask);
+            } else {
+                osFilterBuilder.setManufacturerData(getManufacturerId(), manufacturerData);
+            }
+        }
+
+        ParcelUuid serviceDataUuid = getServiceDataUuid();
+        byte[] serviceData = getServiceData();
+        if (serviceDataUuid != null && serviceData != null) {
+            byte[] serviceDataMask = getServiceDataMask();
+            if (serviceDataMask != null) {
+                osFilterBuilder.setServiceData(serviceDataUuid, serviceData, serviceDataMask);
+            } else {
+                osFilterBuilder.setServiceData(serviceDataUuid, serviceData);
+            }
+        }
+
+        ParcelUuid serviceUuid = getServiceUuid();
+        if (serviceUuid != null) {
+            ParcelUuid serviceUuidMask = getServiceUuidMask();
+            if (serviceUuidMask != null) {
+                osFilterBuilder.setServiceUuid(serviceUuid, serviceUuidMask);
+            } else {
+                osFilterBuilder.setServiceUuid(serviceUuid);
+            }
+        }
+        return osFilterBuilder.build();
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java b/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java
new file mode 100644
index 0000000..103a27f
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleRecord.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.ble.util.StringUtils;
+
+import com.google.common.collect.ImmutableList;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Represents a BLE record from Bluetooth LE scan.
+ */
+public final class BleRecord {
+
+    // The following data type values are assigned by Bluetooth SIG.
+    // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18.
+    private static final int DATA_TYPE_FLAGS = 0x01;
+    private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02;
+    private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03;
+    private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04;
+    private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05;
+    private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06;
+    private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07;
+    private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08;
+    private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09;
+    private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A;
+    private static final int DATA_TYPE_SERVICE_DATA = 0x16;
+    private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
+
+    /** The base 128-bit UUID representation of a 16-bit UUID. */
+    private static final ParcelUuid BASE_UUID =
+            ParcelUuid.fromString("00000000-0000-1000-8000-00805F9B34FB");
+    /** Length of bytes for 16 bit UUID. */
+    private static final int UUID_BYTES_16_BIT = 2;
+    /** Length of bytes for 32 bit UUID. */
+    private static final int UUID_BYTES_32_BIT = 4;
+    /** Length of bytes for 128 bit UUID. */
+    private static final int UUID_BYTES_128_BIT = 16;
+
+    // Flags of the advertising data.
+    // -1 when the scan record is not valid.
+    private final int mAdvertiseFlags;
+
+    private final ImmutableList<ParcelUuid> mServiceUuids;
+
+    // null when the scan record is not valid.
+    @Nullable
+    private final SparseArray<byte[]> mManufacturerSpecificData;
+
+    // null when the scan record is not valid.
+    @Nullable
+    private final Map<ParcelUuid, byte[]> mServiceData;
+
+    // Transmission power level(in dB).
+    // Integer.MIN_VALUE when the scan record is not valid.
+    private final int mTxPowerLevel;
+
+    // Local name of the Bluetooth LE device.
+    // null when the scan record is not valid.
+    @Nullable
+    private final String mDeviceName;
+
+    // Raw bytes of scan record.
+    // Never null, whether valid or not.
+    private final byte[] mBytes;
+
+    // If the raw scan record byte[] cannot be parsed, all non-primitive args here other than the
+    // raw scan record byte[] and serviceUudis will be null. See parsefromBytes().
+    private BleRecord(
+            List<ParcelUuid> serviceUuids,
+            @Nullable SparseArray<byte[]> manufacturerData,
+            @Nullable Map<ParcelUuid, byte[]> serviceData,
+            int advertiseFlags,
+            int txPowerLevel,
+            @Nullable String deviceName,
+            byte[] bytes) {
+        this.mServiceUuids = ImmutableList.copyOf(serviceUuids);
+        mManufacturerSpecificData = manufacturerData;
+        this.mServiceData = serviceData;
+        this.mDeviceName = deviceName;
+        this.mAdvertiseFlags = advertiseFlags;
+        this.mTxPowerLevel = txPowerLevel;
+        this.mBytes = bytes;
+    }
+
+    /**
+     * Returns a list of service UUIDs within the advertisement that are used to identify the
+     * bluetooth GATT services.
+     */
+    public ImmutableList<ParcelUuid> getServiceUuids() {
+        return mServiceUuids;
+    }
+
+    /**
+     * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
+     * data.
+     */
+    @Nullable
+    public SparseArray<byte[]> getManufacturerSpecificData() {
+        return mManufacturerSpecificData;
+    }
+
+    /**
+     * Returns the manufacturer specific data associated with the manufacturer id. Returns {@code
+     * null} if the {@code manufacturerId} is not found.
+     */
+    @Nullable
+    public byte[] getManufacturerSpecificData(int manufacturerId) {
+        if (mManufacturerSpecificData == null) {
+            return null;
+        }
+        return mManufacturerSpecificData.get(manufacturerId);
+    }
+
+    /** Returns a map of service UUID and its corresponding service data. */
+    @Nullable
+    public Map<ParcelUuid, byte[]> getServiceData() {
+        return mServiceData;
+    }
+
+    /**
+     * Returns the service data byte array associated with the {@code serviceUuid}. Returns {@code
+     * null} if the {@code serviceDataUuid} is not found.
+     */
+    @Nullable
+    public byte[] getServiceData(ParcelUuid serviceDataUuid) {
+        if (serviceDataUuid == null || mServiceData == null) {
+            return null;
+        }
+        return mServiceData.get(serviceDataUuid);
+    }
+
+    /**
+     * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE}
+     * if
+     * the field is not set. This value can be used to calculate the path loss of a received packet
+     * using the following equation:
+     *
+     * <p><code>pathloss = txPowerLevel - rssi</code>
+     */
+    public int getTxPowerLevel() {
+        return mTxPowerLevel;
+    }
+
+    /** Returns the local name of the BLE device. The is a UTF-8 encoded string. */
+    @Nullable
+    public String getDeviceName() {
+        return mDeviceName;
+    }
+
+    /** Returns raw bytes of scan record. */
+    public byte[] getBytes() {
+        return mBytes;
+    }
+
+    /**
+     * Parse scan record bytes to {@link BleRecord}.
+     *
+     * <p>The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18.
+     *
+     * <p>All numerical multi-byte entities and values shall use little-endian <strong>byte</strong>
+     * order.
+     *
+     * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response.
+     */
+    public static BleRecord parseFromBytes(byte[] scanRecord) {
+        int currentPos = 0;
+        int advertiseFlag = -1;
+        List<ParcelUuid> serviceUuids = new ArrayList<>();
+        String localName = null;
+        int txPowerLevel = Integer.MIN_VALUE;
+
+        SparseArray<byte[]> manufacturerData = new SparseArray<>();
+        Map<ParcelUuid, byte[]> serviceData = new HashMap<>();
+
+        try {
+            while (currentPos < scanRecord.length) {
+                // length is unsigned int.
+                int length = scanRecord[currentPos++] & 0xFF;
+                if (length == 0) {
+                    break;
+                }
+                // Note the length includes the length of the field type itself.
+                int dataLength = length - 1;
+                // fieldType is unsigned int.
+                int fieldType = scanRecord[currentPos++] & 0xFF;
+                switch (fieldType) {
+                    case DATA_TYPE_FLAGS:
+                        advertiseFlag = scanRecord[currentPos] & 0xFF;
+                        break;
+                    case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL:
+                    case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE:
+                        parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_16_BIT,
+                                serviceUuids);
+                        break;
+                    case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL:
+                    case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE:
+                        parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_32_BIT,
+                                serviceUuids);
+                        break;
+                    case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL:
+                    case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE:
+                        parseServiceUuid(scanRecord, currentPos, dataLength, UUID_BYTES_128_BIT,
+                                serviceUuids);
+                        break;
+                    case DATA_TYPE_LOCAL_NAME_SHORT:
+                    case DATA_TYPE_LOCAL_NAME_COMPLETE:
+                        localName = new String(extractBytes(scanRecord, currentPos, dataLength));
+                        break;
+                    case DATA_TYPE_TX_POWER_LEVEL:
+                        txPowerLevel = scanRecord[currentPos];
+                        break;
+                    case DATA_TYPE_SERVICE_DATA:
+                        // The first two bytes of the service data are service data UUID in little
+                        // endian. The rest bytes are service data.
+                        int serviceUuidLength = UUID_BYTES_16_BIT;
+                        byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
+                                serviceUuidLength);
+                        ParcelUuid serviceDataUuid = parseUuidFrom(serviceDataUuidBytes);
+                        byte[] serviceDataArray =
+                                extractBytes(
+                                        scanRecord, currentPos + serviceUuidLength,
+                                        dataLength - serviceUuidLength);
+                        serviceData.put(serviceDataUuid, serviceDataArray);
+                        break;
+                    case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA:
+                        // The first two bytes of the manufacturer specific data are
+                        // manufacturer ids in little endian.
+                        int manufacturerId =
+                                ((scanRecord[currentPos + 1] & 0xFF) << 8) + (scanRecord[currentPos]
+                                        & 0xFF);
+                        byte[] manufacturerDataBytes = extractBytes(scanRecord, currentPos + 2,
+                                dataLength - 2);
+                        manufacturerData.put(manufacturerId, manufacturerDataBytes);
+                        break;
+                    default:
+                        // Just ignore, we don't handle such data type.
+                        break;
+                }
+                currentPos += dataLength;
+            }
+
+            return new BleRecord(
+                    serviceUuids,
+                    manufacturerData,
+                    serviceData,
+                    advertiseFlag,
+                    txPowerLevel,
+                    localName,
+                    scanRecord);
+        } catch (Exception e) {
+            Log.w("BleRecord", "Unable to parse scan record: " + Arrays.toString(scanRecord), e);
+            // As the record is invalid, ignore all the parsed results for this packet
+            // and return an empty record with raw scanRecord bytes in results
+            // check at the top of this method does? Maybe we expect callers to use the
+            // scanRecord part in
+            // some fallback. But if that's the reason, it would seem we still can return null.
+            // They still
+            // have the raw scanRecord in hand, 'cause they passed it to us. It seems too easy for a
+            // caller to misuse this "empty" BleRecord (as in b/22693067).
+            return new BleRecord(ImmutableList.of(), null, null, -1, Integer.MIN_VALUE, null,
+                    scanRecord);
+        }
+    }
+
+    // Parse service UUIDs.
+    private static int parseServiceUuid(
+            byte[] scanRecord,
+            int currentPos,
+            int dataLength,
+            int uuidLength,
+            List<ParcelUuid> serviceUuids) {
+        while (dataLength > 0) {
+            byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength);
+            serviceUuids.add(parseUuidFrom(uuidBytes));
+            dataLength -= uuidLength;
+            currentPos += uuidLength;
+        }
+        return currentPos;
+    }
+
+    // Helper method to extract bytes from byte array.
+    private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
+        byte[] bytes = new byte[length];
+        System.arraycopy(scanRecord, start, bytes, 0, length);
+        return bytes;
+    }
+
+    @Override
+    public String toString() {
+        return "BleRecord [advertiseFlags="
+                + mAdvertiseFlags
+                + ", serviceUuids="
+                + mServiceUuids
+                + ", manufacturerSpecificData="
+                + StringUtils.toString(mManufacturerSpecificData)
+                + ", serviceData="
+                + StringUtils.toString(mServiceData)
+                + ", txPowerLevel="
+                + mTxPowerLevel
+                + ", deviceName="
+                + mDeviceName
+                + "]";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof BleRecord)) {
+            return false;
+        }
+        BleRecord record = (BleRecord) obj;
+        // BleRecord objects are built from bytes, so we only need that field.
+        return Arrays.equals(mBytes, record.mBytes);
+    }
+
+    @Override
+    public int hashCode() {
+        // BleRecord objects are built from bytes, so we only need that field.
+        return Arrays.hashCode(mBytes);
+    }
+
+    /**
+     * Parse UUID from bytes. The {@code uuidBytes} can represent a 16-bit, 32-bit or 128-bit UUID,
+     * but the returned UUID is always in 128-bit format. Note UUID is little endian in Bluetooth.
+     *
+     * @param uuidBytes Byte representation of uuid.
+     * @return {@link ParcelUuid} parsed from bytes.
+     * @throws IllegalArgumentException If the {@code uuidBytes} cannot be parsed.
+     */
+    private static ParcelUuid parseUuidFrom(byte[] uuidBytes) {
+        if (uuidBytes == null) {
+            throw new IllegalArgumentException("uuidBytes cannot be null");
+        }
+        int length = uuidBytes.length;
+        if (length != UUID_BYTES_16_BIT
+                && length != UUID_BYTES_32_BIT
+                && length != UUID_BYTES_128_BIT) {
+            throw new IllegalArgumentException("uuidBytes length invalid - " + length);
+        }
+        // Construct a 128 bit UUID.
+        if (length == UUID_BYTES_128_BIT) {
+            ByteBuffer buf = ByteBuffer.wrap(uuidBytes).order(ByteOrder.LITTLE_ENDIAN);
+            long msb = buf.getLong(8);
+            long lsb = buf.getLong(0);
+            return new ParcelUuid(new UUID(msb, lsb));
+        }
+        // For 16 bit and 32 bit UUID we need to convert them to 128 bit value.
+        // 128_bit_value = uuid * 2^96 + BASE_UUID
+        long shortUuid;
+        if (length == UUID_BYTES_16_BIT) {
+            shortUuid = uuidBytes[0] & 0xFF;
+            shortUuid += (uuidBytes[1] & 0xFF) << 8;
+        } else {
+            shortUuid = uuidBytes[0] & 0xFF;
+            shortUuid += (uuidBytes[1] & 0xFF) << 8;
+            shortUuid += (uuidBytes[2] & 0xFF) << 16;
+            shortUuid += (uuidBytes[3] & 0xFF) << 24;
+        }
+        long msb = BASE_UUID.getUuid().getMostSignificantBits() + (shortUuid << 32);
+        long lsb = BASE_UUID.getUuid().getLeastSignificantBits();
+        return new ParcelUuid(new UUID(msb, lsb));
+    }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java b/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java
new file mode 100644
index 0000000..71ec10c
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleSighting.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.os.Build.VERSION_CODES;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A sighting of a BLE device found in a Bluetooth LE scan.
+ */
+
+public class BleSighting implements Parcelable {
+
+    public static final Parcelable.Creator<BleSighting> CREATOR = new Creator<BleSighting>() {
+        @Override
+        public BleSighting createFromParcel(Parcel source) {
+            BleSighting nBleSighting = new BleSighting(source.readParcelable(null),
+                    source.marshall(), source.readInt(), source.readLong());
+            return null;
+        }
+
+        @Override
+        public BleSighting[] newArray(int size) {
+            return new BleSighting[size];
+        }
+    };
+
+    // Max and min rssi value which is from {@link android.bluetooth.le.ScanResult#getRssi()}.
+    @VisibleForTesting
+    public static final int MAX_RSSI_VALUE = 126;
+    @VisibleForTesting
+    public static final int MIN_RSSI_VALUE = -127;
+
+    /** Remote bluetooth device. */
+    private final BluetoothDevice mDevice;
+
+    /**
+     * BLE record, including advertising data and response data. BleRecord is not parcelable, so
+     * this
+     * is created from bleRecordBytes.
+     */
+    private final BleRecord mBleRecord;
+
+    /** The bytes of a BLE record. */
+    private final byte[] mBleRecordBytes;
+
+    /** Received signal strength. */
+    private final int mRssi;
+
+    /** Nanos timestamp when the ble device was observed (epoch time). */
+    private final long mTimestampEpochNanos;
+
+    /**
+     * Constructor of a BLE sighting.
+     *
+     * @param device              Remote bluetooth device that is found.
+     * @param bleRecordBytes      The bytes that will create a BleRecord.
+     * @param rssi                Received signal strength.
+     * @param timestampEpochNanos Nanos timestamp when the BLE device was observed (epoch time).
+     */
+    public BleSighting(BluetoothDevice device, byte[] bleRecordBytes, int rssi,
+            long timestampEpochNanos) {
+        this.mDevice = device;
+        this.mBleRecordBytes = bleRecordBytes;
+        this.mRssi = rssi;
+        this.mTimestampEpochNanos = timestampEpochNanos;
+        mBleRecord = BleRecord.parseFromBytes(bleRecordBytes);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Returns the remote bluetooth device identified by the bluetooth device address. */
+    public BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
+    /** Returns the BLE record, which is a combination of advertisement and scan response. */
+    public BleRecord getBleRecord() {
+        return mBleRecord;
+    }
+
+    /** Returns the bytes of the BLE record. */
+    public byte[] getBleRecordBytes() {
+        return mBleRecordBytes;
+    }
+
+    /** Returns the received signal strength in dBm. The valid range is [-127, 127]. */
+    public int getRssi() {
+        return mRssi;
+    }
+
+    /**
+     * Returns the received signal strength normalized with the offset specific to the given device.
+     * 3 is the rssi offset to calculate fast init distance.
+     * <p>This method utilized the rssi offset maintained by Nearby Sharing.
+     *
+     * @return normalized rssi which is between [-127, 126] according to {@link
+     * android.bluetooth.le.ScanResult#getRssi()}.
+     */
+    public int getNormalizedRSSI() {
+        int adjustedRssi = mRssi + 3;
+        if (adjustedRssi < MIN_RSSI_VALUE) {
+            return MIN_RSSI_VALUE;
+        } else if (adjustedRssi > MAX_RSSI_VALUE) {
+            return MAX_RSSI_VALUE;
+        } else {
+            return adjustedRssi;
+        }
+    }
+
+    /** Returns timestamp in epoch time when the scan record was observed. */
+    public long getTimestampNanos() {
+        return mTimestampEpochNanos;
+    }
+
+    /** Returns timestamp in epoch time when the scan record was observed, in millis. */
+    public long getTimestampMillis() {
+        return TimeUnit.NANOSECONDS.toMillis(mTimestampEpochNanos);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mDevice, flags);
+        dest.writeByteArray(mBleRecordBytes);
+        dest.writeInt(mRssi);
+        dest.writeLong(mTimestampEpochNanos);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDevice, mRssi, mTimestampEpochNanos, Arrays.hashCode(mBleRecordBytes));
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof BleSighting)) {
+            return false;
+        }
+        BleSighting other = (BleSighting) obj;
+        return Objects.equals(mDevice, other.mDevice)
+                && mRssi == other.mRssi
+                && Arrays.equals(mBleRecordBytes, other.mBleRecordBytes)
+                && mTimestampEpochNanos == other.mTimestampEpochNanos;
+    }
+
+    @Override
+    public String toString() {
+        return "BleSighting{"
+                + "device="
+                + mDevice
+                + ", bleRecord="
+                + mBleRecord
+                + ", rssi="
+                + mRssi
+                + ", timestampNanos="
+                + mTimestampEpochNanos
+                + "}";
+    }
+
+    /** Creates {@link BleSighting} using the {@link ScanResult}. */
+    @RequiresApi(api = VERSION_CODES.LOLLIPOP)
+    @Nullable
+    public static BleSighting createFromOsScanResult(ScanResult osResult) {
+        ScanRecord osScanRecord = osResult.getScanRecord();
+        if (osScanRecord == null) {
+            return null;
+        }
+
+        return new BleSighting(
+                osResult.getDevice(),
+                osScanRecord.getBytes(),
+                osResult.getRssi(),
+                // The timestamp from ScanResult is 'nanos since boot', Beacon lib will change it
+                // as 'nanos
+                // since epoch', but Nearby never reference this field, just pass it as 'nanos
+                // since boot'.
+                // ref to beacon/scan/impl/LBluetoothLeScannerCompat.fromOs for beacon design
+                // about how to
+                // convert nanos since boot to epoch.
+                osResult.getTimestampNanos());
+    }
+}
+
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java b/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java
new file mode 100644
index 0000000..9e795ac
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/decode/BeaconDecoder.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.decode;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.ble.BleRecord;
+
+/**
+ * This class encapsulates the logic specific to each manufacturer for parsing formats for beacons,
+ * and presents a common API to access important ADV/EIR packet fields such as:
+ *
+ * <ul>
+ *   <li><b>UUID (universally unique identifier)</b>, a value uniquely identifying a group of one or
+ *       more beacons as belonging to an organization or of a certain type, up to 128 bits.
+ *   <li><b>Instance</b> a 32-bit unsigned integer that can be used to group related beacons that
+ *       have the same UUID.
+ *   <li>the mathematics of <b>TX signal strength</b>, used for proximity calculations.
+ * </ul>
+ *
+ * ...and others.
+ *
+ * @see <a href="http://go/ble-glossary">BLE Glossary</a>
+ * @see <a href="https://www.bluetooth.org/docman/handlers/downloaddoc.ashx?doc_id=245130">Bluetooth
+ * Data Types Specification</a>
+ */
+public abstract class BeaconDecoder {
+    /**
+     * Returns true if the bleRecord corresponds to a beacon format that contains sufficient
+     * information to construct a BeaconId and contains the Tx power.
+     */
+    public boolean supportsBeaconIdAndTxPower(@SuppressWarnings("unused") BleRecord bleRecord) {
+        return true;
+    }
+
+    /**
+     * Returns true if this decoder supports returning TxPower via {@link
+     * #getCalibratedBeaconTxPower(BleRecord)}.
+     */
+    public boolean supportsTxPower() {
+        return true;
+    }
+
+    /**
+     * Reads the calibrated transmitted power at 1 meter of the beacon in dBm. This value is
+     * contained
+     * in the scan record, as set by the transmitting beacon. Suitable for use in computing path
+     * loss,
+     * distance, and related derived values.
+     *
+     * @param bleRecord the parsed payload contained in the beacon packet
+     * @return integer value of the calibrated Tx power in dBm or null if the bleRecord doesn't
+     * contain sufficient information to calculate the Tx power.
+     */
+    @Nullable
+    public abstract Integer getCalibratedBeaconTxPower(BleRecord bleRecord);
+
+    /**
+     * Extract telemetry information from the beacon. Byte 0 of the returned telemetry block should
+     * encode the telemetry format.
+     *
+     * @return telemetry block for this beacon, or null if no telemetry data is found in the scan
+     * record.
+     */
+    @Nullable
+    public byte[] getTelemetry(@SuppressWarnings("unused") BleRecord bleRecord) {
+        return null;
+    }
+
+    /** Returns the appropriate type for this scan record. */
+    public abstract int getBeaconIdType();
+
+    /**
+     * Returns an array of bytes which uniquely identify this beacon, for beacons from any of the
+     * supported beacon types. This unique identifier is the indexing key for various internal
+     * services. Returns null if the bleRecord doesn't contain sufficient information to construct
+     * the
+     * ID.
+     */
+    @Nullable
+    public abstract byte[] getBeaconIdBytes(BleRecord bleRecord);
+
+    /**
+     * Returns the URL of the beacon. Returns null if the bleRecord doesn't contain a URL or
+     * contains
+     * a malformed URL.
+     */
+    @Nullable
+    public String getUrl(BleRecord bleRecord) {
+        return null;
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java
new file mode 100644
index 0000000..c1ff9fd
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/decode/FastPairDecoder.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.decode;
+
+import android.bluetooth.le.ScanRecord;
+import android.os.ParcelUuid;
+import android.util.Log;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+
+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).
+ *
+ * @see <a href="http://go/fast-pair-2-service-data">go/fast-pair-2-service-data</a>
+ */
+public class FastPairDecoder extends BeaconDecoder {
+
+    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);
+        }
+    }
+
+    @Nullable
+    @Override
+    public Integer getCalibratedBeaconTxPower(BleRecord bleRecord) {
+        return null;
+    }
+
+    // TODO(b/205320613) create beacon type
+    @Override
+    public int getBeaconIdType() {
+        return 1;
+    }
+
+    /** Returns the Model ID from our service data, if present. */
+    @Nullable
+    @Override
+    public byte[] getBeaconIdBytes(BleRecord bleRecord) {
+        return getModelId(bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID));
+    }
+
+    /** Returns the Model ID from our service data, if present. */
+    @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);
+    }
+
+    /** Gets the battery level from extra fields if available, otherwise return null. */
+    @Nullable
+    public static byte[] getBatteryLevel(byte[] serviceData) {
+        return getExtraField(serviceData, FIELD_TYPE_BATTERY);
+    }
+
+    /**
+     * Gets the suppress notification with battery level from extra fields if available, otherwise
+     * return null.
+     */
+    @Nullable
+    public static byte[] getBatteryLevelNoNotification(byte[] serviceData) {
+        return getExtraField(serviceData, FIELD_TYPE_BATTERY_NO_NOTIFICATION);
+    }
+
+    /**
+     * Gets the random resolvable data from extra fields if available, otherwise
+     * return null.
+     */
+    @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) {
+            Log.v("FastPairDecode", "Extra fields incorrectly formatted.");
+            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/service/java/com/android/server/nearby/common/ble/util/StringUtils.java b/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java
new file mode 100644
index 0000000..4d90b6d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/ble/util/StringUtils.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble.util;
+
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Map;
+
+/** Helper class for Bluetooth LE utils. */
+public final class StringUtils {
+    private StringUtils() {
+    }
+
+    /** Returns a string composed from a {@link SparseArray}. */
+    public static String toString(@Nullable SparseArray<byte[]> array) {
+        if (array == null) {
+            return "null";
+        }
+        if (array.size() == 0) {
+            return "{}";
+        }
+        StringBuilder buffer = new StringBuilder();
+        buffer.append('{');
+        for (int i = 0; i < array.size(); ++i) {
+            buffer.append(array.keyAt(i)).append("=").append(Arrays.toString(array.valueAt(i)));
+        }
+        buffer.append('}');
+        return buffer.toString();
+    }
+
+    /** Returns a string composed from a {@link Map}. */
+    public static <T> String toString(@Nullable Map<T, byte[]> map) {
+        if (map == null) {
+            return "null";
+        }
+        if (map.isEmpty()) {
+            return "{}";
+        }
+        StringBuilder buffer = new StringBuilder();
+        buffer.append('{');
+        Iterator<Map.Entry<T, byte[]>> it = map.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<T, byte[]> entry = it.next();
+            Object key = entry.getKey();
+            buffer.append(key).append("=").append(Arrays.toString(map.get(key)));
+            if (it.hasNext()) {
+                buffer.append(", ");
+            }
+        }
+        buffer.append('}');
+        return buffer.toString();
+    }
+}
diff --git a/nearby/tests/src/com/android/server/nearby/common/ble/BleRecordTest.java b/nearby/tests/src/com/android/server/nearby/common/ble/BleRecordTest.java
new file mode 100644
index 0000000..56ac1b8
--- /dev/null
+++ b/nearby/tests/src/com/android/server/nearby/common/ble/BleRecordTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/** Test for Bluetooth LE {@link BleRecord}. */
+public class BleRecordTest {
+
+    // iBeacon (Apple) Packet 1
+    private static final byte[] BEACON = {
+            // Flags
+            (byte) 0x02,
+            (byte) 0x01,
+            (byte) 0x06,
+            // Manufacturer-specific data header
+            (byte) 0x1a,
+            (byte) 0xff,
+            (byte) 0x4c,
+            (byte) 0x00,
+            // iBeacon Type
+            (byte) 0x02,
+            // Frame length
+            (byte) 0x15,
+            // iBeacon Proximity UUID
+            (byte) 0xf7,
+            (byte) 0x82,
+            (byte) 0x6d,
+            (byte) 0xa6,
+            (byte) 0x4f,
+            (byte) 0xa2,
+            (byte) 0x4e,
+            (byte) 0x98,
+            (byte) 0x80,
+            (byte) 0x24,
+            (byte) 0xbc,
+            (byte) 0x5b,
+            (byte) 0x71,
+            (byte) 0xe0,
+            (byte) 0x89,
+            (byte) 0x3e,
+            // iBeacon Instance ID (Major/Minor)
+            (byte) 0x44,
+            (byte) 0xd0,
+            (byte) 0x25,
+            (byte) 0x22,
+            // Tx Power
+            (byte) 0xb3,
+            // RSP
+            (byte) 0x08,
+            (byte) 0x09,
+            (byte) 0x4b,
+            (byte) 0x6f,
+            (byte) 0x6e,
+            (byte) 0x74,
+            (byte) 0x61,
+            (byte) 0x6b,
+            (byte) 0x74,
+            (byte) 0x02,
+            (byte) 0x0a,
+            (byte) 0xf4,
+            (byte) 0x0a,
+            (byte) 0x16,
+            (byte) 0x0d,
+            (byte) 0xd0,
+            (byte) 0x74,
+            (byte) 0x6d,
+            (byte) 0x4d,
+            (byte) 0x6b,
+            (byte) 0x32,
+            (byte) 0x36,
+            (byte) 0x64,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00
+    };
+
+    // iBeacon (Apple) Packet 1
+    private static final byte[] SAME_BEACON = {
+            // Flags
+            (byte) 0x02,
+            (byte) 0x01,
+            (byte) 0x06,
+            // Manufacturer-specific data header
+            (byte) 0x1a,
+            (byte) 0xff,
+            (byte) 0x4c,
+            (byte) 0x00,
+            // iBeacon Type
+            (byte) 0x02,
+            // Frame length
+            (byte) 0x15,
+            // iBeacon Proximity UUID
+            (byte) 0xf7,
+            (byte) 0x82,
+            (byte) 0x6d,
+            (byte) 0xa6,
+            (byte) 0x4f,
+            (byte) 0xa2,
+            (byte) 0x4e,
+            (byte) 0x98,
+            (byte) 0x80,
+            (byte) 0x24,
+            (byte) 0xbc,
+            (byte) 0x5b,
+            (byte) 0x71,
+            (byte) 0xe0,
+            (byte) 0x89,
+            (byte) 0x3e,
+            // iBeacon Instance ID (Major/Minor)
+            (byte) 0x44,
+            (byte) 0xd0,
+            (byte) 0x25,
+            (byte) 0x22,
+            // Tx Power
+            (byte) 0xb3,
+            // RSP
+            (byte) 0x08,
+            (byte) 0x09,
+            (byte) 0x4b,
+            (byte) 0x6f,
+            (byte) 0x6e,
+            (byte) 0x74,
+            (byte) 0x61,
+            (byte) 0x6b,
+            (byte) 0x74,
+            (byte) 0x02,
+            (byte) 0x0a,
+            (byte) 0xf4,
+            (byte) 0x0a,
+            (byte) 0x16,
+            (byte) 0x0d,
+            (byte) 0xd0,
+            (byte) 0x74,
+            (byte) 0x6d,
+            (byte) 0x4d,
+            (byte) 0x6b,
+            (byte) 0x32,
+            (byte) 0x36,
+            (byte) 0x64,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00,
+            (byte) 0x00
+    };
+
+    // iBeacon (Apple) Packet 1 with a modified second field.
+    private static final byte[] OTHER_BEACON = {
+            (byte) 0x02, // Length of this Data
+            (byte) 0x02, // <<Flags>>
+            (byte) 0x04, // BR/EDR Not Supported.
+            // Apple Specific Data
+            26, // length of data that follows
+            (byte) 0xff, // <<Manufacturer Specific Data>>
+            // Company Identifier Code = Apple
+            (byte) 0x4c, // LSB
+            (byte) 0x00, // MSB
+            // iBeacon Header
+            0x02,
+            // iBeacon Length
+            0x15,
+            // UUID = PROXIMITY_NOW
+            // IEEE 128-bit UUID represented as UUID[15]: msb To UUID[0]: lsb
+            (byte) 0x14,
+            (byte) 0xe4,
+            (byte) 0xfd,
+            (byte) 0x9f, // UUID[15] - UUID[12]
+            (byte) 0x66,
+            (byte) 0x67,
+            (byte) 0x4c,
+            (byte) 0xcb, // UUID[11] - UUID[08]
+            (byte) 0xa6,
+            (byte) 0x1b,
+            (byte) 0x24,
+            (byte) 0xd0, // UUID[07] - UUID[04]
+            (byte) 0x9a,
+            (byte) 0xb1,
+            (byte) 0x7e,
+            (byte) 0x93, // UUID[03] - UUID[00]
+            // ID as an int (decimal) = 1297482358
+            (byte) 0x76, // Major H
+            (byte) 0x02, // Major L
+            (byte) 0x56, // Minor H
+            (byte) 0x4d, // Minor L
+            // Normalized Tx Power of -77dbm
+            (byte) 0xb3,
+            0x00, // Zero padding for testing
+    };
+
+    @Test
+    public void testEquals() {
+        BleRecord record = BleRecord.parseFromBytes(BEACON);
+        BleRecord record2 = BleRecord.parseFromBytes(SAME_BEACON);
+
+
+        assertThat(record).isEqualTo(record2);
+
+        // Different items.
+        record2 = BleRecord.parseFromBytes(OTHER_BEACON);
+        assertThat(record).isNotEqualTo(record2);
+        assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
+    }
+}
+