blob: 103a27fe8036812154510a8c6bcc3ddc3ed46540 [file] [log] [blame]
/*
* 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));
}
}