Merge "Remove Jave enum usage in GluetoothGattHelper."
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 0e3b2f0..4a8ab0b 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -43,6 +43,7 @@
"androidx.core_core",
"guava",
"libprotobuf-java-lite",
+ "fast-pair-lite-protos",
],
sdk_version: "system_server_current",
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index dcd802e..0691d21 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -23,6 +23,7 @@
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.FastPairManager;
+
/**
* Service implementing nearby functionality. The actual implementation is delegated to
* {@link NearbyServiceImpl}.
@@ -43,7 +44,6 @@
mImpl = new NearbyServiceImpl(contextBase);
mLocatorContextWrapper = new LocatorContextWrapper(contextBase, null);
mFastPairManager = new FastPairManager(mLocatorContextWrapper);
-
}
@Override
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/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
index 1487d4b..e382f84 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -26,6 +26,8 @@
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import service.proto.Rpcs;
+
/**
* FastPairManager is the class initiated in nearby service to handle Fast Pair related
* work.
@@ -54,6 +56,8 @@
mIntentFilter = new IntentFilter();
mLocator = mLocatorContextWrapper.getLocator();
mLocator.bind(new FastPairModule());
+ Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
+ Rpcs.GetObservedDeviceResponse.newBuilder().build();
}
/**
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
index 30871eb..7955f2a 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
@@ -16,14 +16,52 @@
package com.android.server.nearby.fastpair.cache;
+import android.bluetooth.le.ScanResult;
import android.content.Context;
import android.util.Log;
+
+import com.android.server.nearby.common.eventloop.Annotations;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+
/**
* Save FastPair device info to database to avoid multiple requesting.
*/
public class FastPairCacheManager {
+
+ private static final String FAST_PAIR_SERVER_CACHE = "FAST_PAIR_SERVER_CACHE";
+
public FastPairCacheManager(Context context) {
+
+ }
+
+ /**
+ * Saves the response to the db
+ */
+ private void saveDevice() {
+
+ }
+
+ Cache.ServerResponseDbItem getDeviceFromScanResult(ScanResult scanResult) {
+ return Cache.ServerResponseDbItem.newBuilder().build();
+ }
+
+ /**
+ * Checks if the entry can be auto deleted from the cache
+ */
+ public boolean isDeletable(Cache.ServerResponseDbItem entry) {
+ if (!entry.getExpirable()) {
+ return false;
+ }
+ return true;
+ }
+
+ @Annotations.EventThread
+ private Rpcs.GetObservedDeviceResponse getObservedDeviceInfo(ScanResult scanResult) {
+ return Rpcs.GetObservedDeviceResponse.getDefaultInstance();
}
/**
@@ -32,4 +70,5 @@
public void printLog() {
Log.d("FastPairCacheManager", "print log");
}
+
}
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
new file mode 100644
index 0000000..9eca51d
--- /dev/null
+++ b/nearby/service/proto/Android.bp
@@ -0,0 +1,32 @@
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "fast-pair-lite-protos",
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ srcs: ["src/*/*.proto"],
+ apex_available: [
+ "com.android.nearby",
+ ],
+}
+
+
diff --git a/nearby/service/proto/src/fastpair/cache.proto b/nearby/service/proto/src/fastpair/cache.proto
new file mode 100644
index 0000000..4872ae6
--- /dev/null
+++ b/nearby/service/proto/src/fastpair/cache.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+package service.proto;
+import "src/fastpair/rpcs.proto";
+
+// db information for Fast Pair that gets from server.
+message ServerResponseDbItem {
+ // Device's model id.
+ string model_id = 1;
+
+ // Response was received from the server. Contains data needed to display
+ // FastPair notification such as device name, txPower of device, image used
+ // in the notification, etc.
+ GetObservedDeviceResponse get_observed_device_response = 2;
+
+ // The timestamp that make the server fetch.
+ int64 last_fetch_info_timestamp_millis = 3;
+
+ // Whether the item in the cache is expirable or not (when offline mode this
+ // will be false).
+ bool expirable = 4;
+}
diff --git a/nearby/service/proto/src/fastpair/rpcs.proto b/nearby/service/proto/src/fastpair/rpcs.proto
new file mode 100644
index 0000000..e1ab344
--- /dev/null
+++ b/nearby/service/proto/src/fastpair/rpcs.proto
@@ -0,0 +1,318 @@
+// RPCs for the Nearby Console service.
+syntax = "proto3";
+
+package service.proto;
+// Response containing an observed device.
+message GetObservedDeviceResponse {
+ // The device from the request.
+ Device device = 1;
+
+ // The image icon that shows in the notification
+ bytes image = 3;
+
+ // Strings to be displayed on notifications during the pairing process.
+ ObservedDeviceStrings strings = 4;
+
+ reserved 2;
+}
+
+message Device {
+ // Output only. The server-generated ID of the device.
+ int64 id = 1;
+
+ // The pantheon project number the device is created under. Only Nearby admins
+ // can change this.
+ int64 project_number = 2;
+
+ // How the notification will be displayed to the user
+ NotificationType notification_type = 3;
+
+ // The image to show on the notification.
+ string image_url = 4;
+
+ // The name of the device.
+ string name = 5;
+
+ // The intent that will be launched via the notification.
+ string intent_uri = 6;
+
+ // The transmit power of the device's BLE chip.
+ int32 ble_tx_power = 7;
+
+ // The distance that the device must be within to show a notification.
+ // If no distance is set, we default to 0.6 meters. Only Nearby admins can
+ // change this.
+ float trigger_distance = 8;
+
+ // Output only. Fast Pair only - The anti-spoofing key pair for the device.
+ AntiSpoofingKeyPair anti_spoofing_key_pair = 9;
+
+ // Output only. The current status of the device.
+ Status status = 10;
+
+
+ // DEPRECATED - check for published_version instead.
+ // Output only.
+ // Whether the device has a different, already published version.
+ bool has_published_version = 12;
+
+ // Fast Pair only - The type of device being registered.
+ DeviceType device_type = 13;
+
+
+ // Fast Pair only - Additional images for true wireless headsets.
+ TrueWirelessHeadsetImages true_wireless_images = 15;
+
+ // Fast Pair only - When true, this device can support assistant function.
+ // Only Nearby admins can change this.
+ // TODO(b/128545971): Transition this to a feature.
+ bool assistant_supported = 16;
+
+ // Output only.
+ // The published version of a device that has been approved to be displayed
+ // as a notification - only populated if the device has a different published
+ // version. (A device that only has a published version would not have this
+ // populated).
+ Device published_version = 17;
+
+ // Fast Pair only - When true, Fast Pair will only create a bond with the
+ // device and not attempt to connect any profiles (for example, A2DP or HFP).
+ // Only Nearby admins can change this.
+ // TODO(b/128545971): Transition this to a feature.
+ bool data_only_connection = 18;
+
+ // Name of the company/brand that will be selling the product.
+ string company_name = 19;
+
+ repeated FastPairFeature features = 20;
+
+ // Name of the device that is displayed on the console.
+ string display_name = 21;
+
+ // How the device will be interacted with by the user when the scan record
+ // is detected.
+ InteractionType interaction_type = 22;
+
+ // Companion app information.
+ CompanionAppDetails companion_detail = 23;
+
+ reserved 11, 14;
+}
+
+
+// Represents the format of the final device notification (which is directly
+// correlated to the action taken by the notification).
+enum NotificationType {
+ // Unspecified notification type.
+ NOTIFICATION_TYPE_UNSPECIFIED = 0;
+ // Notification launches the fast pair intent.
+ // Example Notification Title: "Bose SoundLink II"
+ // Notification Description: "Tap to pair with this device"
+ FAST_PAIR = 1;
+ // Notification launches an app.
+ // Notification Title: "[X]" where X is type/name of the device.
+ // Notification Description: "Tap to setup this device"
+ APP_LAUNCH = 2;
+ // Notification launches for Nearby Setup. The notification title and
+ // description is the same as APP_LAUNCH.
+ NEARBY_SETUP = 3;
+ // Notification launches the fast pair intent, but doesn't include an anti-
+ // spoofing key. The notification title and description is the same as
+ // FAST_PAIR.
+ FAST_PAIR_ONE = 4;
+ // Notification launches Smart Setup on devices.
+ // These notifications are identical to APP_LAUNCH except that they always
+ // launch Smart Setup intents within GMSCore.
+ SMART_SETUP = 5;
+}
+
+// How the device will be interacted with when it is seen.
+enum InteractionType {
+ INTERACTION_TYPE_UNKNOWN = 0;
+ AUTO_LAUNCH = 1;
+ NOTIFICATION = 2;
+}
+
+// Features that can be enabled for a Fast Pair device.
+enum FastPairFeature {
+ FAST_PAIR_FEATURE_UNKNOWN = 0;
+ SILENCE_MODE = 1;
+ WIRELESS_CHARGING = 2;
+ DYNAMIC_BUFFER_SIZE = 3;
+ NO_PERSONALIZED_NAME = 4;
+ EDDYSTONE_TRACKING = 5;
+}
+
+message CompanionAppDetails {
+ // Companion app slice provider's authority.
+ string authority = 1;
+
+ // Companion app certificate value.
+ string certificate_hash = 2;
+
+ // Deprecated fields.
+ reserved 3;
+}
+
+// Additional images for True Wireless Fast Pair devices.
+message TrueWirelessHeadsetImages {
+ // Image URL for the left bud.
+ string left_bud_url = 1;
+
+ // Image URL for the right bud.
+ string right_bud_url = 2;
+
+ // Image URL for the case.
+ string case_url = 3;
+}
+
+// Represents the type of device that is being registered.
+enum DeviceType {
+ DEVICE_TYPE_UNSPECIFIED = 0;
+ HEADPHONES = 1;
+ SPEAKER = 2;
+ WEARABLE = 3;
+ INPUT_DEVICE = 4;
+ AUTOMOTIVE = 5;
+ OTHER = 6;
+ TRUE_WIRELESS_HEADPHONES = 7;
+ WEAR_OS = 8;
+ ANDROID_AUTO = 9;
+}
+
+// An anti-spoofing key pair for a device that allows us to verify the device is
+// broadcasting legitimately.
+message AntiSpoofingKeyPair {
+ // The private key (restricted to only be viewable by trusted clients).
+ bytes private_key = 1;
+
+ // The public key.
+ bytes public_key = 2;
+}
+
+// Various states that a customer-configured device notification can be in.
+// PUBLISHED is the only state that shows notifications to the public.
+message Status {
+ // Status types available for each device.
+ enum StatusType {
+ // Unknown status.
+ TYPE_UNSPECIFIED = 0;
+ // Drafted device.
+ DRAFT = 1;
+ // Submitted and waiting for approval.
+ SUBMITTED = 2;
+ // Fully approved and available for end users.
+ PUBLISHED = 3;
+ // Rejected and not available for end users.
+ REJECTED = 4;
+ }
+
+ // Details about a device that has been rejected.
+ message RejectionDetails {
+ // The reason for the rejection.
+ enum RejectionReason {
+ // Unspecified reason.
+ REASON_UNSPECIFIED = 0;
+ // Name is not valid.
+ NAME = 1;
+ // Image is not valid.
+ IMAGE = 2;
+ // Tests have failed.
+ TESTS = 3;
+ // Other reason.
+ OTHER = 4;
+ }
+
+ // A list of reasons the device was rejected.
+ repeated RejectionReason reasons = 1;
+ // Comment about an OTHER rejection reason.
+ string additional_comment = 2;
+ }
+
+ // The status of the device.
+ StatusType status_type = 1;
+
+ // Accompanies Status.REJECTED.
+ RejectionDetails rejection_details = 2;
+}
+
+// Strings to be displayed in notifications surfaced for a device.
+message ObservedDeviceStrings {
+ // The locale of all of the strings.
+ string locale = 1;
+
+ // The notification description for when the device is initially discovered.
+ string initial_notification_description = 2;
+
+ // The notification description for when the device is initially discovered
+ // and no account is logged in.
+ string initial_notification_description_no_account = 3;
+
+ // The notification description for once we have finished pairing and the
+ // companion app has been opened. For Bisto devices, this string will point
+ // users to setting up the assistant.
+ string open_companion_app_description = 4;
+
+ // The notification description for once we have finished pairing and the
+ // companion app needs to be updated before use.
+ string update_companion_app_description = 5;
+
+ // The notification description for once we have finished pairing and the
+ // companion app needs to be installed.
+ string download_companion_app_description = 6;
+
+ // The notification title when a pairing fails.
+ string unable_to_connect_title = 7;
+
+ // The notification summary when a pairing fails.
+ string unable_to_connect_description = 8;
+
+ // The description that helps user initially paired with device.
+ string initial_pairing_description = 9;
+
+ // The description that let user open the companion app.
+ string connect_success_companion_app_installed = 10;
+
+ // The description that let user download the companion app.
+ string connect_success_companion_app_not_installed = 11;
+
+ // The description that reminds user there is a paired device nearby.
+ string subsequent_pairing_description = 12;
+
+ // The description that reminds users opt in their device.
+ string retroactive_pairing_description = 13;
+
+ // The description that indicates companion app is about to launch.
+ string wait_launch_companion_app_description = 14;
+
+ // The description that indicates go to bluetooth settings when connection
+ // fail.
+ string fail_connect_go_to_settings_description = 15;
+
+ // The title of the UI to ask the user to confirm the pin code.
+ string confirm_pin_title = 16;
+
+ // The description of the UI to ask the user to confirm the pin code.
+ string confirm_pin_description = 17;
+
+ // The title of the UI to ask the user to confirm to sync contacts.
+ string sync_contacts_title = 18;
+
+ // The description of the UI to ask the user to confirm to sync contacts.
+ string sync_contacts_description = 19;
+
+ // The title of the UI to ask the user to confirm to sync SMS.
+ string sync_sms_title = 20;
+
+ // The description of the UI to ask the user to confirm to sync SMS.
+ string sync_sms_description = 21;
+
+ // The description in half sheet to ask user setup google assistant
+ string assistant_setup_half_sheet = 22;
+
+ // The description in notification to ask user setup google assistant
+ string assistant_setup_notification = 23;
+
+ // Description of the connect device action on TV, when user is not logged in.
+ string fast_pair_tv_connect_device_no_account_description = 24;
+}
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());
+ }
+}
+