| /* |
| * Copyright (C) 2014 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 android.bluetooth.le; |
| |
| import android.annotation.Nullable; |
| import android.annotation.UnsupportedAppUsage; |
| import android.bluetooth.BluetoothUuid; |
| import android.os.ParcelUuid; |
| import android.util.ArrayMap; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Represents a scan record from Bluetooth LE scan. |
| */ |
| public final class ScanRecord { |
| |
| private static final String TAG = "ScanRecord"; |
| |
| // 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_16_BIT = 0x16; |
| private static final int DATA_TYPE_SERVICE_DATA_32_BIT = 0x20; |
| private static final int DATA_TYPE_SERVICE_DATA_128_BIT = 0x21; |
| private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_16_BIT = 0x14; |
| private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_32_BIT = 0x1F; |
| private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_128_BIT = 0x15; |
| 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; |
| @Nullable |
| private final List<ParcelUuid> mServiceSolicitationUuids; |
| |
| 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 list of service solicitation UUIDs within the advertisement that are used to |
| * identify the Bluetooth GATT services. |
| */ |
| @Nullable |
| public List<ParcelUuid> getServiceSolicitationUuids() { |
| return mServiceSolicitationUuids; |
| } |
| |
| /** |
| * 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 || 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; |
| } |
| |
| private ScanRecord(List<ParcelUuid> serviceUuids, |
| List<ParcelUuid> serviceSolicitationUuids, |
| SparseArray<byte[]> manufacturerData, |
| Map<ParcelUuid, byte[]> serviceData, |
| int advertiseFlags, int txPowerLevel, |
| String localName, byte[] bytes) { |
| mServiceSolicitationUuids = serviceSolicitationUuids; |
| mServiceUuids = serviceUuids; |
| mManufacturerSpecificData = manufacturerData; |
| mServiceData = serviceData; |
| mDeviceName = localName; |
| mAdvertiseFlags = advertiseFlags; |
| mTxPowerLevel = txPowerLevel; |
| mBytes = bytes; |
| } |
| |
| /** |
| * Parse scan record bytes to {@link ScanRecord}. |
| * <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. |
| * @hide |
| */ |
| @UnsupportedAppUsage |
| public static ScanRecord parseFromBytes(byte[] scanRecord) { |
| if (scanRecord == null) { |
| return null; |
| } |
| |
| int currentPos = 0; |
| int advertiseFlag = -1; |
| List<ParcelUuid> serviceUuids = new ArrayList<ParcelUuid>(); |
| List<ParcelUuid> serviceSolicitationUuids = new ArrayList<ParcelUuid>(); |
| String localName = null; |
| int txPowerLevel = Integer.MIN_VALUE; |
| |
| SparseArray<byte[]> manufacturerData = new SparseArray<byte[]>(); |
| Map<ParcelUuid, byte[]> serviceData = new ArrayMap<ParcelUuid, byte[]>(); |
| |
| 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, BluetoothUuid.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, |
| BluetoothUuid.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, |
| BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids); |
| break; |
| case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_16_BIT: |
| parseServiceSolicitationUuid(scanRecord, currentPos, dataLength, |
| BluetoothUuid.UUID_BYTES_16_BIT, serviceSolicitationUuids); |
| break; |
| case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_32_BIT: |
| parseServiceSolicitationUuid(scanRecord, currentPos, dataLength, |
| BluetoothUuid.UUID_BYTES_32_BIT, serviceSolicitationUuids); |
| break; |
| case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_128_BIT: |
| parseServiceSolicitationUuid(scanRecord, currentPos, dataLength, |
| BluetoothUuid.UUID_BYTES_128_BIT, serviceSolicitationUuids); |
| 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_16_BIT: |
| case DATA_TYPE_SERVICE_DATA_32_BIT: |
| case DATA_TYPE_SERVICE_DATA_128_BIT: |
| int serviceUuidLength = BluetoothUuid.UUID_BYTES_16_BIT; |
| if (fieldType == DATA_TYPE_SERVICE_DATA_32_BIT) { |
| serviceUuidLength = BluetoothUuid.UUID_BYTES_32_BIT; |
| } else if (fieldType == DATA_TYPE_SERVICE_DATA_128_BIT) { |
| serviceUuidLength = BluetoothUuid.UUID_BYTES_128_BIT; |
| } |
| |
| byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos, |
| serviceUuidLength); |
| ParcelUuid serviceDataUuid = BluetoothUuid.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; |
| } |
| if (serviceSolicitationUuids.isEmpty()) { |
| serviceSolicitationUuids = null; |
| } |
| return new ScanRecord(serviceUuids, serviceSolicitationUuids, 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 ScanRecord(null, null, null, null, -1, Integer.MIN_VALUE, null, scanRecord); |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "ScanRecord [mAdvertiseFlags=" + mAdvertiseFlags + ", mServiceUuids=" + mServiceUuids |
| + ", mServiceSolicitationUuids=" + mServiceSolicitationUuids |
| + ", mManufacturerSpecificData=" + BluetoothLeUtils.toString( |
| mManufacturerSpecificData) |
| + ", mServiceData=" + BluetoothLeUtils.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(BluetoothUuid.parseUuidFrom(uuidBytes)); |
| dataLength -= uuidLength; |
| currentPos += uuidLength; |
| } |
| return currentPos; |
| } |
| |
| /** |
| * Parse service Solicitation UUIDs. |
| */ |
| private static int parseServiceSolicitationUuid(byte[] scanRecord, int currentPos, |
| int dataLength, int uuidLength, List<ParcelUuid> serviceSolicitationUuids) { |
| while (dataLength > 0) { |
| byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength); |
| serviceSolicitationUuids.add(BluetoothUuid.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; |
| } |
| } |