/*
|
* Copyright (C) 2014 The Android Open Source Project
|
* Copyright (C) 2015 Joe Rogers
|
*
|
* 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.blakequ.bluetooth_manager_lib.scan.bluetoothcompat;
|
|
import android.annotation.TargetApi;
|
import android.bluetooth.le.ScanRecord;
|
import android.os.Build;
|
import android.os.ParcelUuid;
|
import androidx.annotation.Nullable;
|
import androidx.collection.ArrayMap;
|
import android.util.Log;
|
import android.util.SparseArray;
|
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Iterator;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* Compatible version so the ScanRecord
|
*/
|
public class ScanRecordCompat {
|
private static final String TAG = "ScanRecordCompat";
|
|
// 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.
|
//https://devzone.nordicsemi.com/documentation/nrf51/4.2.0/html/group___b_l_e___g_a_p___a_d___t_y_p_e___d_e_f_i_n_i_t_i_o_n_s.html
|
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;
|
|
// Flags of the advertising data.
|
private final int mAdvertiseFlags;
|
|
@Nullable
|
private final List<ParcelUuid> mServiceUuids;
|
|
private final SparseArray<byte[]> mManufacturerSpecificData;
|
|
private final Map<ParcelUuid, byte[]> mServiceData;
|
|
// Transmission power level(in dB).
|
private final int mTxPowerLevel;
|
|
// Local name of the Bluetooth LE device.
|
private final String mDeviceName;
|
|
// Raw bytes of scan record.
|
private final byte[] mBytes;
|
|
/**
|
* Returns the advertising flags indicating the discoverable mode and capability of the device.
|
* Returns -1 if the flag field is not set.
|
*/
|
public int getAdvertiseFlags() {
|
return mAdvertiseFlags;
|
}
|
|
/**
|
* Returns a list of service UUIDs within the advertisement that are used to identify the
|
* bluetooth GATT services.
|
*/
|
public List<ParcelUuid> getServiceUuids() {
|
return mServiceUuids;
|
}
|
|
/**
|
* Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
|
* data.
|
*/
|
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) {
|
return mManufacturerSpecificData.get(manufacturerId);
|
}
|
|
/**
|
* Returns a map of service UUID and its corresponding service data.
|
*/
|
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) {
|
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;
|
}
|
|
private ScanRecordCompat(@Nullable List<ParcelUuid> serviceUuids,
|
SparseArray<byte[]> manufacturerData,
|
Map<ParcelUuid, byte[]> serviceData,
|
int advertiseFlags, int txPowerLevel,
|
String localName, byte[] bytes) {
|
mServiceUuids = serviceUuids;
|
mManufacturerSpecificData = manufacturerData;
|
mServiceData = serviceData;
|
mDeviceName = localName;
|
mAdvertiseFlags = advertiseFlags;
|
mTxPowerLevel = txPowerLevel;
|
mBytes = bytes;
|
}
|
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
ScanRecordCompat(ScanRecord record) {
|
mServiceUuids = record.getServiceUuids();
|
mManufacturerSpecificData = record.getManufacturerSpecificData();
|
mServiceData = record.getServiceData();
|
mDeviceName = record.getDeviceName();
|
mAdvertiseFlags = record.getAdvertiseFlags();
|
mTxPowerLevel = record.getTxPowerLevel();
|
mBytes = record.getBytes();
|
}
|
|
/**
|
* Parse scan record bytes to {@link ScanRecordCompat}.
|
* <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 ScanRecordCompat parseFromBytes(byte[] scanRecord) {
|
if (scanRecord == null) {
|
return null;
|
}
|
|
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 ArrayMap<>();
|
|
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, BluetoothUuidCompat.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,
|
BluetoothUuidCompat.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,
|
BluetoothUuidCompat.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 = BluetoothUuidCompat.UUID_BYTES_16_BIT;
|
byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos,
|
serviceUuidLength);
|
ParcelUuid serviceDataUuid = BluetoothUuidCompat.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;
|
}
|
|
if (serviceUuids.isEmpty()) {
|
serviceUuids = null;
|
}
|
return new ScanRecordCompat(serviceUuids, manufacturerData, serviceData,
|
advertiseFlag, txPowerLevel, localName, scanRecord);
|
} catch (Exception e) {
|
Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord));
|
// As the record is invalid, ignore all the parsed results for this packet
|
// and return an empty record with raw scanRecord bytes in results
|
return new ScanRecordCompat(null, null, null, -1, Integer.MIN_VALUE, null, scanRecord);
|
}
|
}
|
|
@Override
|
public String toString() {
|
return "ScanRecord [mAdvertiseFlags=" + mAdvertiseFlags + ", mServiceUuids=" + mServiceUuids
|
+ ", mManufacturerSpecificData=" + toString(mManufacturerSpecificData)
|
+ ", mServiceData=" + toString(mServiceData)
|
+ ", mTxPowerLevel=" + mTxPowerLevel + ", mDeviceName=" + mDeviceName + "]";
|
}
|
|
// 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(BluetoothUuidCompat.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;
|
}
|
|
/**
|
* Returns a string composed from a {@link SparseArray}.
|
*/
|
static String toString(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}.
|
*/
|
static <T> String toString(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();
|
T key = entry.getKey();
|
buffer.append(key).append("=").append(Arrays.toString(map.get(key)));
|
if (it.hasNext()) {
|
buffer.append(", ");
|
}
|
}
|
buffer.append('}');
|
return buffer.toString();
|
}
|
}
|