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());
+ }
+}
+