Split the project into a library and sample app
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
package uk.co.alt236.bluetoothlelib.device;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecordStore;
|
||||
import uk.co.alt236.bluetoothlelib.util.AdRecordUtils;
|
||||
import uk.co.alt236.bluetoothlelib.util.ByteUtils;
|
||||
import android.bluetooth.BluetoothClass;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class BluetoothLeDevice implements Parcelable{
|
||||
private final AdRecordStore mRecordStore;
|
||||
private final BluetoothDevice mDevice;
|
||||
private final byte[] mScanRecord;
|
||||
private transient final int mRssi;
|
||||
|
||||
public static final Parcelable.Creator<BluetoothLeDevice> CREATOR = new Parcelable.Creator<BluetoothLeDevice>() {
|
||||
public BluetoothLeDevice createFromParcel(Parcel in) {
|
||||
return new BluetoothLeDevice(in);
|
||||
}
|
||||
|
||||
public BluetoothLeDevice[] newArray(int size) {
|
||||
return new BluetoothLeDevice[size];
|
||||
}
|
||||
};
|
||||
|
||||
public BluetoothLeDevice(BluetoothDevice device, int rssi, byte[] scanRecord){
|
||||
mDevice = device;
|
||||
mRssi = rssi;
|
||||
mScanRecord = scanRecord;
|
||||
mRecordStore = new AdRecordStore(AdRecordUtils.parseScanRecordAsSparseArray(scanRecord));
|
||||
}
|
||||
|
||||
private BluetoothLeDevice(Parcel in) {
|
||||
final Bundle b = in.readBundle(getClass().getClassLoader());
|
||||
|
||||
mDevice = b.getParcelable("bluetooth_device");
|
||||
mRecordStore = b.getParcelable("device_scanrecord_store");
|
||||
mRssi = b.getInt("device_rssi");
|
||||
mScanRecord = b.getByteArray("device_scanrecord");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
BluetoothLeDevice other = (BluetoothLeDevice) obj;
|
||||
if (mDevice == null) {
|
||||
if (other.mDevice != null)
|
||||
return false;
|
||||
} else if (!mDevice.equals(other.mDevice))
|
||||
return false;
|
||||
if (!Arrays.equals(mScanRecord, other.mScanRecord))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getAddress(){
|
||||
return mDevice.getAddress();
|
||||
}
|
||||
|
||||
public AdRecordStore getAdRecordStore(){
|
||||
return mRecordStore;
|
||||
}
|
||||
|
||||
public String getBluetoothDeviceBondState(){
|
||||
return resolveBondingState(mDevice.getBondState());
|
||||
}
|
||||
|
||||
public String getBluetoothDeviceClassName(){
|
||||
return resolveBluetoothClass(mDevice.getBluetoothClass().getDeviceClass());
|
||||
}
|
||||
|
||||
public BluetoothDevice getDevice() {
|
||||
return mDevice;
|
||||
}
|
||||
|
||||
public String getName(){
|
||||
return mDevice.getName();
|
||||
}
|
||||
|
||||
public int getRssi() {
|
||||
return mRssi;
|
||||
}
|
||||
|
||||
public byte[] getScanRecord() {
|
||||
return mScanRecord;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((mDevice == null) ? 0 : mDevice.hashCode());
|
||||
result = prime * result + Arrays.hashCode(mScanRecord);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BluetoothLeDevice [mDevice=" + mDevice + ", mRssi=" + mRssi + ", mScanRecord=" + ByteUtils.byteArrayToHexString(mScanRecord) + ", mRecordStore=" + mRecordStore + ", getBluetoothDeviceBondState()=" + getBluetoothDeviceBondState() + ", getBluetoothDeviceClassName()=" + getBluetoothDeviceClassName() + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int arg1) {
|
||||
final Bundle b = new Bundle(getClass().getClassLoader());
|
||||
|
||||
b.putByteArray("device_scanrecord", mScanRecord);
|
||||
b.putInt("device_rssi", mRssi);
|
||||
b.putParcelable("bluetooth_device", mDevice);
|
||||
b.putParcelable("device_scanrecord_store", mRecordStore);
|
||||
|
||||
parcel.writeBundle(b);
|
||||
}
|
||||
|
||||
private static String resolveBluetoothClass(int btClass){
|
||||
switch (btClass){
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_CAMCORDER:
|
||||
return "A/V, Camcorder";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
|
||||
return "A/V, Car Audio";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
|
||||
return "A/V, Handsfree";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES:
|
||||
return "A/V, Headphones";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO:
|
||||
return "A/V, HiFi Audio";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER:
|
||||
return "A/V, Loudspeaker";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE:
|
||||
return "A/V, Microphone";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO:
|
||||
return "A/V, Portable Audio";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX:
|
||||
return "A/V, Set Top Box";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED:
|
||||
return "A/V, Uncategorized";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_VCR:
|
||||
return "A/V, VCR";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CAMERA:
|
||||
return "A/V, Video Camera";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CONFERENCING:
|
||||
return "A/V, Video Conferencing";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER:
|
||||
return "A/V, Video Display and Loudspeaker";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_GAMING_TOY:
|
||||
return "A/V, Video Gaming Toy";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_VIDEO_MONITOR:
|
||||
return "A/V, Video Monitor";
|
||||
case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
|
||||
return "A/V, Video Wearable Headset";
|
||||
case BluetoothClass.Device.COMPUTER_DESKTOP:
|
||||
return "Computer, Desktop";
|
||||
case BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA:
|
||||
return "Computer, Handheld PC/PDA";
|
||||
case BluetoothClass.Device.COMPUTER_LAPTOP:
|
||||
return "Computer, Laptop";
|
||||
case BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA:
|
||||
return "Computer, Palm Size PC/PDA";
|
||||
case BluetoothClass.Device.COMPUTER_SERVER:
|
||||
return "Computer, Server";
|
||||
case BluetoothClass.Device.COMPUTER_UNCATEGORIZED:
|
||||
return "Computer, Uncategorized";
|
||||
case BluetoothClass.Device.COMPUTER_WEARABLE:
|
||||
return "Computer, Wearable";
|
||||
case BluetoothClass.Device.HEALTH_BLOOD_PRESSURE:
|
||||
return "Health, Blood Pressure";
|
||||
case BluetoothClass.Device.HEALTH_DATA_DISPLAY:
|
||||
return "Health, Data Display";
|
||||
case BluetoothClass.Device.HEALTH_GLUCOSE:
|
||||
return "Health, Glucose";
|
||||
case BluetoothClass.Device.HEALTH_PULSE_OXIMETER :
|
||||
return "Health, Pulse Oximeter";
|
||||
case BluetoothClass.Device.HEALTH_PULSE_RATE :
|
||||
return "Health, Pulse Rate";
|
||||
case BluetoothClass.Device.HEALTH_THERMOMETER :
|
||||
return "Health, Thermometer";
|
||||
case BluetoothClass.Device.HEALTH_UNCATEGORIZED :
|
||||
return "Health, Uncategorized";
|
||||
case BluetoothClass.Device.HEALTH_WEIGHING:
|
||||
return "Health, Weighting";
|
||||
case BluetoothClass.Device.PHONE_CELLULAR:
|
||||
return "Phone, Cellular";
|
||||
case BluetoothClass.Device.PHONE_CORDLESS:
|
||||
return "Phone, Cordless";
|
||||
case BluetoothClass.Device.PHONE_ISDN:
|
||||
return "Phone, ISDN";
|
||||
case BluetoothClass.Device.PHONE_MODEM_OR_GATEWAY:
|
||||
return "Phone, Modem or Gateway";
|
||||
case BluetoothClass.Device.PHONE_SMART:
|
||||
return "Phone, Smart";
|
||||
case BluetoothClass.Device.PHONE_UNCATEGORIZED:
|
||||
return "Phone, Uncategorized";
|
||||
case BluetoothClass.Device.TOY_CONTROLLER:
|
||||
return "Toy, Controller";
|
||||
case BluetoothClass.Device.TOY_DOLL_ACTION_FIGURE:
|
||||
return "Toy, Doll/Action Figure";
|
||||
case BluetoothClass.Device.TOY_GAME:
|
||||
return "Toy, Game";
|
||||
case BluetoothClass.Device.TOY_ROBOT:
|
||||
return "Toy, Robot";
|
||||
case BluetoothClass.Device.TOY_UNCATEGORIZED:
|
||||
return "Toy, Uncategorized";
|
||||
case BluetoothClass.Device.TOY_VEHICLE:
|
||||
return "Toy, Vehicle";
|
||||
case BluetoothClass.Device.WEARABLE_GLASSES:
|
||||
return "Wearable, Glasses";
|
||||
case BluetoothClass.Device.WEARABLE_HELMET:
|
||||
return "Wearable, Helmet";
|
||||
case BluetoothClass.Device.WEARABLE_JACKET:
|
||||
return "Wearable, Jacket";
|
||||
case BluetoothClass.Device.WEARABLE_PAGER:
|
||||
return "Wearable, Pager";
|
||||
case BluetoothClass.Device.WEARABLE_UNCATEGORIZED:
|
||||
return "Wearable, Uncategorized";
|
||||
case BluetoothClass.Device.WEARABLE_WRIST_WATCH:
|
||||
return "Wearable, Wrist Watch";
|
||||
default:
|
||||
return "Unknown, Unknown (class=" + btClass +")";
|
||||
}
|
||||
}
|
||||
|
||||
private static String resolveBondingState(int bondState){
|
||||
switch (bondState){
|
||||
case BluetoothDevice.BOND_BONDED:
|
||||
return "Paired";
|
||||
case BluetoothDevice.BOND_BONDING:
|
||||
return "Pairing";
|
||||
case BluetoothDevice.BOND_NONE:
|
||||
return "Unbonded";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package uk.co.alt236.bluetoothlelib.device.adrecord;
|
||||
import java.util.Arrays;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
/**
|
||||
* Created by Dave Smith
|
||||
* Double Encore, Inc.
|
||||
*
|
||||
* Expanded by Alexandros Schillings
|
||||
*/
|
||||
public final class AdRecord implements Parcelable{
|
||||
|
||||
// 02 # Number of bytes that follow in first AD structure
|
||||
// 01 # Flags AD type
|
||||
// 1A # Flags value 0x1A = 000011010
|
||||
// bit 0 (OFF) LE Limited Discoverable Mode
|
||||
// bit 1 (ON) LE General Discoverable Mode
|
||||
// bit 2 (OFF) BR/EDR Not Supported
|
||||
// bit 3 (ON) Simultaneous LE and BR/EDR to Same Device Capable (controller)
|
||||
// bit 4 (ON) Simultaneous LE and BR/EDR to Same Device Capable (Host)
|
||||
// 1A # Number of bytes that follow in second (and last) AD structure
|
||||
// FF # Manufacturer specific data AD type
|
||||
// 4C 00 # Company identifier code (0x004C == Apple)
|
||||
// 02 # Byte 0 of iBeacon advertisement indicator
|
||||
// 15 # Byte 1 of iBeacon advertisement indicator
|
||||
// e2 c5 6d b5 df fb 48 d2 b0 60 d0 f5 a7 10 96 e0 # iBeacon proximity uuid
|
||||
// 00 00 # major
|
||||
// 00 00 # minor
|
||||
// c5 # The 2's complement of the calibrated Tx Power
|
||||
|
||||
|
||||
private static final String PARCEL_RECORD_DATA = "record_data";
|
||||
private static final String PARCEL_RECORD_TYPE = "record_type";
|
||||
private static final String PARCEL_RECORD_LENGTH = "record_length";
|
||||
|
||||
/**
|
||||
* General FLAGS
|
||||
*
|
||||
* Description: Flags
|
||||
*
|
||||
* Information:
|
||||
* Bit 0: LE Limited Discoverable Mode
|
||||
* Bit 1: LE General Discoverable Mode
|
||||
* Bit 2: BR/EDR Not Supported (i.e. bit 37 of LMP Extended Feature bits Page 0)
|
||||
* Bit 3: Simultaneous LE and BR/EDR to Same Device Capable (Controller) (i.e. bit 49 of LMP Extended Feature bits Page 0)
|
||||
* Bit 4: Simultaneous LE and BR/EDR to Same Device Capable (Host) (i.e. bit 66 of LMP Extended Feature bits Page 1)
|
||||
* Bits 5-7 Reserved
|
||||
*/
|
||||
public static final int TYPE_FLAGS = 0x01;
|
||||
|
||||
// SERVICE
|
||||
public static final int TYPE_UUID16_INC = 0x02;
|
||||
public static final int TYPE_UUID16 = 0x03;
|
||||
public static final int TYPE_UUID32_INC = 0x04;
|
||||
public static final int TYPE_UUID32 = 0x05;
|
||||
public static final int TYPE_UUID128_INC = 0x06;
|
||||
public static final int TYPE_UUID128 = 0x07;
|
||||
|
||||
// Local name
|
||||
public static final int TYPE_LOCAL_NAME_SHORT = 0x08;
|
||||
public static final int TYPE_LOCAL_NAME_COMPLETE = 0x09;
|
||||
|
||||
// TX Power Level
|
||||
public static final int TYPE_TX_POWER_LEVEL = 0x0A;
|
||||
|
||||
// SIMPLE PAIRING OPTIONAL OOB TAGS
|
||||
public static final int TYPE_DEVICE_CLASS = 0x0D;
|
||||
public static final int TYPE_SIMPLE_PAIRING_HASH_C = 0x0E;
|
||||
public static final int TYPE_SIMPLE_PAIRING_RANDOMIZER_R = 0x0F;
|
||||
|
||||
// SECURITY MANAGER TK VALUE
|
||||
public static final int TYPE_TK_VALUE = 0x10;
|
||||
|
||||
|
||||
/* SECURITY MANAGER OOB FLAGS
|
||||
*
|
||||
* Description: Flag (1 octet)
|
||||
*
|
||||
* Information:
|
||||
* Bit 0: OOB Flags Field: (0 = OOB data not present, 1 = OOB data present)
|
||||
* Bit 1: LE supported (Host) (i.e. bit 65 of LMP Extended Feature bits Page 1
|
||||
* Bit 2: Simultaneous LE and BR/EDR to Same Device Capable (Host) (i.e. bit 66 of LMP Extended Feature bits Page 1)
|
||||
* Bit 3: Address type (0 = Public Address, 1 = Random Address)
|
||||
* Bits 4-7 Reserved
|
||||
*/
|
||||
public static final int TYPE_SECURITY_MANAGER_OOB_FLAGS = 0x11;
|
||||
|
||||
|
||||
/* SLAVE CONNECTION INTERVAL RANGE
|
||||
*
|
||||
* Description: Slave Connection Interval Range
|
||||
*
|
||||
* Information:
|
||||
* The first 2 octets defines the minimum value for the connection interval in the following manner:
|
||||
* connInterval min = Conn_Interval_Min * 1.25 ms
|
||||
* Conn_Interval_Min range: 0x0006 to 0x0C80
|
||||
* Value of 0xFFFF indicates no specific minimum.
|
||||
* Values outside the range are reserved. (excluding 0xFFFF)
|
||||
*
|
||||
* The second 2 octets defines the maximum value for the connection interval in the following manner:
|
||||
* connInterval max = Conn_Interval_Max * 1.25 ms
|
||||
* Conn_Interval_Max range: 0x0006 to 0x0C80
|
||||
* Conn_Interval_Max shall be equal to or greater
|
||||
* than the Conn_Interval_Min.
|
||||
* Value of 0xFFFF indicates no specific maximum.
|
||||
* Values outside the range are reserved (excluding 0xFFFF)
|
||||
*/
|
||||
public static final int TYPE_CONNECTION_INTERVAL_RANGE = 0x12;
|
||||
|
||||
// SERVICE SOLICITATION
|
||||
public static final int TYPE_SERVICE_UUIDS_LIST_16BIT = 0x14;
|
||||
public static final int TYPE_SERVICE_UUIDS_LIST_128BIT = 0x15;
|
||||
|
||||
/* SERVICE DATA
|
||||
*
|
||||
* Description: Service Data (2 or more octets)
|
||||
* Information: The first 2 octets contain the 16 bit Service UUID followed by additional service data
|
||||
*/
|
||||
public static final int TYPE_SERVICE_DATA = 0x16;
|
||||
|
||||
|
||||
/* MANUFACTURER SPECIFIC DATA
|
||||
*
|
||||
* Description: Manufacturer Specific Data (2 or more octets)
|
||||
* Information: The first 2 octets contain the Company Identifier Code followed by additional manufacturer specific data
|
||||
*/
|
||||
public static final int TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
|
||||
|
||||
/* Model Object Definition */
|
||||
private final int mLength;
|
||||
private final int mType;
|
||||
private final byte[] mData;
|
||||
|
||||
|
||||
public static final Parcelable.Creator<AdRecord> CREATOR = new Parcelable.Creator<AdRecord>() {
|
||||
public AdRecord createFromParcel(Parcel in) {
|
||||
return new AdRecord(in);
|
||||
}
|
||||
|
||||
public AdRecord[] newArray(int size) {
|
||||
return new AdRecord[size];
|
||||
}
|
||||
};
|
||||
|
||||
public AdRecord(int length, int type, byte[] data) {
|
||||
mLength = length;
|
||||
mType = type;
|
||||
mData = data;
|
||||
}
|
||||
|
||||
public AdRecord(Parcel in) {
|
||||
final Bundle b = in.readBundle(getClass().getClassLoader());
|
||||
mLength = b.getInt(PARCEL_RECORD_LENGTH);
|
||||
mType = b.getInt(PARCEL_RECORD_TYPE);
|
||||
mData = b.getByteArray(PARCEL_RECORD_DATA);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public byte[] getData(){
|
||||
return mData;
|
||||
}
|
||||
|
||||
public String getHumanReadableType(){
|
||||
return getHumanReadableAdType(mType);
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return mLength;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AdRecord [mLength=" + mLength + ", mType=" + mType + ", mData=" + Arrays.toString(mData) + ", getHumanReadableType()=" + getHumanReadableType() + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int arg1) {
|
||||
final Bundle b = new Bundle(getClass().getClassLoader());
|
||||
|
||||
b.putInt(PARCEL_RECORD_LENGTH, mLength);
|
||||
b.putInt(PARCEL_RECORD_TYPE, mType);
|
||||
b.putByteArray(PARCEL_RECORD_DATA, mData);
|
||||
|
||||
parcel.writeBundle(b);
|
||||
}
|
||||
|
||||
private static String getHumanReadableAdType(int type){
|
||||
switch(type){
|
||||
case TYPE_CONNECTION_INTERVAL_RANGE:
|
||||
return "Slave Connection Interval Range";
|
||||
case TYPE_DEVICE_CLASS:
|
||||
return "Class of device";
|
||||
case TYPE_FLAGS:
|
||||
return "Flags";
|
||||
case TYPE_MANUFACTURER_SPECIFIC_DATA:
|
||||
return "Manufacturer Specific Data";
|
||||
case TYPE_LOCAL_NAME_COMPLETE:
|
||||
return "Name (Complete)";
|
||||
case TYPE_LOCAL_NAME_SHORT:
|
||||
return "Name (Short)";
|
||||
case TYPE_SECURITY_MANAGER_OOB_FLAGS:
|
||||
return "Security Manager OOB Flags";
|
||||
case TYPE_SERVICE_UUIDS_LIST_128BIT:
|
||||
return "Service UUIDs (128bit)";
|
||||
case TYPE_SERVICE_UUIDS_LIST_16BIT:
|
||||
return "Service UUIDs (16bit)";
|
||||
case TYPE_SERVICE_DATA:
|
||||
return "Service Data";
|
||||
case TYPE_SIMPLE_PAIRING_HASH_C:
|
||||
return "Simple Pairing Hash C";
|
||||
case TYPE_SIMPLE_PAIRING_RANDOMIZER_R:
|
||||
return "Simple Pairing Randomizer R";
|
||||
case TYPE_TK_VALUE:
|
||||
return "TK Value";
|
||||
case TYPE_TX_POWER_LEVEL:
|
||||
return "Transmission Power Level";
|
||||
case TYPE_UUID128:
|
||||
return "Complete list of 128-bit UUIDs available";
|
||||
case TYPE_UUID128_INC:
|
||||
return "More 128-bit UUIDs available";
|
||||
case TYPE_UUID16:
|
||||
return "Complete list of 16-bit UUIDs available";
|
||||
case TYPE_UUID16_INC:
|
||||
return "More 16-bit UUIDs available";
|
||||
case TYPE_UUID32:
|
||||
return "Complete list of 32-bit UUIDs available";
|
||||
case TYPE_UUID32_INC:
|
||||
return "More 32-bit UUIDs available";
|
||||
default:
|
||||
return "Unknown AdRecord Structure: " + type;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package uk.co.alt236.bluetoothlelib.device.adrecord;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import uk.co.alt236.bluetoothlelib.util.AdRecordUtils;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.SparseArray;
|
||||
|
||||
public class AdRecordStore implements Parcelable{
|
||||
|
||||
private final SparseArray<AdRecord> mAdRecords;
|
||||
private final String mLocalNameComplete;
|
||||
private final String mLocalNameShort;
|
||||
|
||||
public static final Parcelable.Creator<AdRecordStore> CREATOR = new Parcelable.Creator<AdRecordStore>() {
|
||||
public AdRecordStore createFromParcel(Parcel in) {
|
||||
return new AdRecordStore(in);
|
||||
}
|
||||
|
||||
public AdRecordStore[] newArray(int size) {
|
||||
return new AdRecordStore[size];
|
||||
}
|
||||
};
|
||||
|
||||
public AdRecordStore(Parcel in) {
|
||||
final Bundle b = in.readBundle(getClass().getClassLoader());
|
||||
mAdRecords = b.getSparseParcelableArray("records_array");
|
||||
mLocalNameComplete = b.getString("local_name_complete");
|
||||
mLocalNameShort = b.getString("local_name_short");
|
||||
}
|
||||
|
||||
public AdRecordStore(final SparseArray<AdRecord> adRecords){
|
||||
mAdRecords = adRecords;
|
||||
|
||||
mLocalNameComplete = AdRecordUtils.getRecordDataAsString(
|
||||
mAdRecords.get(AdRecord.TYPE_LOCAL_NAME_COMPLETE));
|
||||
|
||||
mLocalNameShort = AdRecordUtils.getRecordDataAsString(
|
||||
mAdRecords.get(AdRecord.TYPE_LOCAL_NAME_SHORT));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public String getLocalNameComplete() {
|
||||
return mLocalNameComplete;
|
||||
}
|
||||
|
||||
public String getLocalNameShort() {
|
||||
return mLocalNameShort;
|
||||
}
|
||||
|
||||
public AdRecord getRecord(int record){
|
||||
return mAdRecords.get(record);
|
||||
}
|
||||
|
||||
public String getRecordDataAsString(int record){
|
||||
return AdRecordUtils.getRecordDataAsString(
|
||||
mAdRecords.get(record));
|
||||
}
|
||||
|
||||
public Collection<AdRecord> getRecordsAsCollection() {
|
||||
return Collections.unmodifiableCollection(asList(mAdRecords));
|
||||
}
|
||||
|
||||
public boolean isRecordPresent(int record){
|
||||
return mAdRecords.indexOfKey(record) >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AdRecordStore [mLocalNameComplete=" + mLocalNameComplete + ", mLocalNameShort=" + mLocalNameShort + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel parcel, int arg1) {
|
||||
final Bundle b = new Bundle();
|
||||
b.putString("local_name_complete", mLocalNameComplete);
|
||||
b.putString("local_name_short", mLocalNameShort);
|
||||
b.putSparseParcelableArray("records_array", mAdRecords);
|
||||
|
||||
parcel.writeBundle(b);
|
||||
}
|
||||
|
||||
public static <C> Collection<C> asList(SparseArray<C> sparseArray) {
|
||||
if (sparseArray == null) return null;
|
||||
|
||||
final Collection<C> arrayList = new ArrayList<C>(sparseArray.size());
|
||||
for (int i = 0; i < sparseArray.size(); i++){
|
||||
arrayList.add(sparseArray.valueAt(i));
|
||||
}
|
||||
|
||||
return arrayList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package uk.co.alt236.bluetoothlelib.device.mfdata;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice;
|
||||
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord;
|
||||
import uk.co.alt236.bluetoothlelib.util.ByteUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public final class IBeaconManufacturerData {
|
||||
// 0 4C # Byte 1 (LSB) of Company identifier code
|
||||
// 1 00 # Byte 0 (MSB) of Company identifier code (0x004C == Apple)
|
||||
// 2 02 # Byte 0 of iBeacon advertisement indicator
|
||||
// 3 15 # Byte 1 of iBeacon advertisement indicator
|
||||
// 4 e2 |\
|
||||
// 5 c5 |\\
|
||||
// 6 6d |#\\
|
||||
// 7 b5 |##\\
|
||||
// 8 df |###\\
|
||||
// 9 fb |####\\
|
||||
// 10 48 |#####\\
|
||||
// 11 d2 |#####|| iBeacon proximity UUID
|
||||
// 12 b0 |#####||
|
||||
// 13 60 |#####//
|
||||
// 14 d0 |####//
|
||||
// 15 f5 |###//
|
||||
// 16 a7 |##//
|
||||
// 17 10 |#//
|
||||
// 18 96 |//
|
||||
// 19 e0 |/
|
||||
// 20 00 # major
|
||||
// 21 00
|
||||
// 22 00 # minor
|
||||
// 23 00
|
||||
// 24 c5 # The 2's complement of the calibrated Tx Power
|
||||
|
||||
private final byte[] mData;
|
||||
private final int mCalibratedTxPower;
|
||||
private final int mCompanyIdentidier;
|
||||
private final int mIBeaconAdvertisment;
|
||||
private final int mMajor;
|
||||
private final int mMinor;
|
||||
private final UUID mUUID;
|
||||
|
||||
public IBeaconManufacturerData(BluetoothLeDevice device){
|
||||
this(device.getAdRecordStore().getRecord(AdRecord.TYPE_MANUFACTURER_SPECIFIC_DATA).getData());
|
||||
}
|
||||
|
||||
public IBeaconManufacturerData(byte[] data){
|
||||
mData = data;
|
||||
Log.d("TAG", "~ Reading iBeacon Data: " + ByteUtils.byteArrayToHexString(data));
|
||||
|
||||
mCompanyIdentidier = ByteUtils.getIntFrom2ByteArray(
|
||||
ByteUtils.invertArray(
|
||||
Arrays.copyOfRange(mData, 0, 2)));
|
||||
|
||||
mIBeaconAdvertisment = ByteUtils.getIntFrom2ByteArray(Arrays.copyOfRange(mData, 2, 4));
|
||||
mUUID = UUID.nameUUIDFromBytes(Arrays.copyOfRange(mData, 4, 20));
|
||||
mMajor = ByteUtils.getIntFrom2ByteArray(Arrays.copyOfRange(mData, 20, 22));
|
||||
mMinor = ByteUtils.getIntFrom2ByteArray(Arrays.copyOfRange(mData, 22, 24));
|
||||
mCalibratedTxPower = ByteUtils.getIntFromByte(data[24]);
|
||||
}
|
||||
|
||||
public int getCalibratedTxPower(){
|
||||
return mCalibratedTxPower;
|
||||
}
|
||||
|
||||
public int getCompanyIdentifier(){
|
||||
return mCompanyIdentidier;
|
||||
}
|
||||
|
||||
public int getIBeaconAdvertisement(){
|
||||
return mIBeaconAdvertisment;
|
||||
}
|
||||
|
||||
public int getMajor(){
|
||||
return mMajor;
|
||||
}
|
||||
|
||||
public int getMinor(){
|
||||
return mMinor;
|
||||
}
|
||||
|
||||
public UUID getUUID(){
|
||||
return mUUID;
|
||||
}
|
||||
|
||||
// Code taken from: http://stackoverflow.com/questions/20416218/understanding-ibeacon-distancing
|
||||
protected static double calculateAccuracy(int txPower, double rssi) {
|
||||
if (rssi == 0) {
|
||||
return -1.0; // if we cannot determine accuracy, return -1.
|
||||
}
|
||||
|
||||
double ratio = rssi*1.0/txPower;
|
||||
if (ratio < 1.0) {
|
||||
return Math.pow(ratio,10);
|
||||
}
|
||||
else {
|
||||
final double accuracy = (0.89976)*Math.pow(ratio,7.7095) + 0.111;
|
||||
return accuracy;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package uk.co.alt236.bluetoothlelib.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.util.SparseArray;
|
||||
|
||||
public class AdRecordUtils {
|
||||
|
||||
public static String getRecordDataAsString(final AdRecord nameRecord) {
|
||||
if(nameRecord == null){return new String();}
|
||||
return new String(nameRecord.getData());
|
||||
}
|
||||
|
||||
public static byte[] getServiceData(final AdRecord serviceData) {
|
||||
if (serviceData == null) {return null;}
|
||||
if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return null;
|
||||
|
||||
final byte[] raw = serviceData.getData();
|
||||
//Chop out the uuid
|
||||
return Arrays.copyOfRange(raw, 2, raw.length);
|
||||
}
|
||||
|
||||
public static int getServiceDataUuid(final AdRecord serviceData) {
|
||||
if (serviceData == null) {return -1;}
|
||||
if (serviceData.getType() != AdRecord.TYPE_SERVICE_DATA) return -1;
|
||||
|
||||
final byte[] raw = serviceData.getData();
|
||||
//Find UUID data in byte array
|
||||
int uuid = (raw[1] & 0xFF) << 8;
|
||||
uuid += (raw[0] & 0xFF);
|
||||
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/*
|
||||
* Read out all the AD structures from the raw scan record
|
||||
*/
|
||||
public static List<AdRecord> parseScanRecordAsList(final byte[] scanRecord) {
|
||||
final List<AdRecord> records = new ArrayList<AdRecord>();
|
||||
|
||||
int index = 0;
|
||||
while (index < scanRecord.length) {
|
||||
final int length = scanRecord[index++];
|
||||
//Done once we run out of records
|
||||
if (length == 0) break;
|
||||
|
||||
final int type = ByteUtils.getIntFromByte(scanRecord[index]);
|
||||
|
||||
//Done if our record isn't a valid type
|
||||
if (type == 0) break;
|
||||
|
||||
final byte[] data = Arrays.copyOfRange(scanRecord, index+1, index+length);
|
||||
|
||||
records.add(new AdRecord(length, type, data));
|
||||
|
||||
//Advance
|
||||
index += length;
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(records);
|
||||
}
|
||||
|
||||
@SuppressLint("UseSparseArrays")
|
||||
public static Map<Integer, AdRecord> parseScanRecordAsMap(final byte[] scanRecord) {
|
||||
final Map<Integer, AdRecord> records = new HashMap<Integer, AdRecord>();
|
||||
|
||||
int index = 0;
|
||||
while (index < scanRecord.length) {
|
||||
final int length = scanRecord[index++];
|
||||
//Done once we run out of records
|
||||
if (length == 0) break;
|
||||
|
||||
final int type = ByteUtils.getIntFromByte(scanRecord[index]);
|
||||
|
||||
//Done if our record isn't a valid type
|
||||
if (type == 0) break;
|
||||
|
||||
final byte[] data = Arrays.copyOfRange(scanRecord, index+1, index+length);
|
||||
|
||||
records.put(type, new AdRecord(length, type, data));
|
||||
|
||||
//Advance
|
||||
index += length;
|
||||
}
|
||||
|
||||
return Collections.unmodifiableMap(records);
|
||||
}
|
||||
|
||||
public static SparseArray<AdRecord> parseScanRecordAsSparseArray(byte[] scanRecord) {
|
||||
final SparseArray<AdRecord> records = new SparseArray<AdRecord>();
|
||||
|
||||
int index = 0;
|
||||
while (index < scanRecord.length) {
|
||||
final int length = scanRecord[index++];
|
||||
//Done once we run out of records
|
||||
if (length == 0) break;
|
||||
|
||||
final int type = ByteUtils.getIntFromByte(scanRecord[index]);
|
||||
|
||||
//Done if our record isn't a valid type
|
||||
if (type == 0) break;
|
||||
|
||||
final byte[] data = Arrays.copyOfRange(scanRecord, index+1, index+length);
|
||||
|
||||
records.put(type, new AdRecord(length, type, data));
|
||||
|
||||
//Advance
|
||||
index += length;
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package uk.co.alt236.bluetoothlelib.util;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class ByteUtils {
|
||||
private static final String HEXES = "0123456789ABCDEF";
|
||||
|
||||
/**
|
||||
* Gets a pretty representation of a Byte Array as a HEX String.
|
||||
*
|
||||
* Sample output: [01, 30, FF, AA]
|
||||
*
|
||||
* @param array the array
|
||||
* @return the string
|
||||
*/
|
||||
public static String byteArrayToHexString(final byte[] array){
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
boolean firstEntry = true;
|
||||
sb.append('[');
|
||||
|
||||
for ( final byte b : array ) {
|
||||
if(!firstEntry){
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(HEXES.charAt((b & 0xF0) >> 4));
|
||||
sb.append(HEXES.charAt((b & 0x0F)));
|
||||
firstEntry = false;
|
||||
}
|
||||
|
||||
sb.append(']');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static byte[] invertArray(byte[] array){
|
||||
final int size = array.length;
|
||||
byte temp;
|
||||
|
||||
for (int i = 0; i < size/2; i++)
|
||||
{
|
||||
temp = array[i];
|
||||
array[i] = array[size-1 - i];
|
||||
array[size-1 - i] = temp;
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public static int getIntFrom2ByteArray(byte[] input){
|
||||
final byte[] result = new byte[4];
|
||||
|
||||
result[0] = 0;
|
||||
result[1] = 0;
|
||||
result[2] = input[0];
|
||||
result[3] = input[1];
|
||||
|
||||
return ByteUtils.getIntFromByteArray(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a byte to an int, preserving the sign.
|
||||
*
|
||||
* For example, FF will be converted to 255 and not -1.
|
||||
*
|
||||
* @param bite the bite
|
||||
* @return the int from byte
|
||||
*/
|
||||
public static int getIntFromByte(final byte bite){
|
||||
return Integer.valueOf(bite & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a byte array to an integer;
|
||||
*
|
||||
* @param bytes the bytes
|
||||
* @return the int from byte array
|
||||
*/
|
||||
public static int getIntFromByteArray(final byte[] bytes) {
|
||||
return ByteBuffer.wrap(bytes).getInt();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a byte array to a long;
|
||||
*
|
||||
* @param bytes the bytes
|
||||
* @return the long from byte array
|
||||
*/
|
||||
public static long getLongFromByteArray(final byte[] bytes) {
|
||||
return ByteBuffer.wrap(bytes).getLong();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package uk.co.alt236.bluetoothlelib.util;
|
||||
|
||||
import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice;
|
||||
|
||||
public class IBeaconUtils {
|
||||
private static final byte[] SCAN_RECORD_PREFIX_IBEACON_1 = new byte[]{0x02, 0x01, 0x1A, 0x1A, (byte) 0xFF, 0x4C, 0x00, 0x02, 0x15};
|
||||
private static final byte[] SCAN_RECORD_PREFIX_IBEACON_2 = new byte[]{0x02, 0x01, 0x06, 0x1A, (byte) 0xFF, 0x4C, 0x00, 0x02, 0x15};
|
||||
|
||||
public static boolean isThisAnIBeacon(BluetoothLeDevice device){
|
||||
return isThisAnIBeacon(device.getScanRecord());
|
||||
}
|
||||
|
||||
public static boolean isThisAnIBeacon(byte[] scanRecord){
|
||||
if(doesArrayBeginWith(scanRecord, SCAN_RECORD_PREFIX_IBEACON_1)){
|
||||
return true;
|
||||
}
|
||||
|
||||
if(doesArrayBeginWith(scanRecord, SCAN_RECORD_PREFIX_IBEACON_2)){
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean doesArrayBeginWith(byte[] array, byte[] prefix){
|
||||
if(array.length < prefix.length){return false;}
|
||||
|
||||
for(int i = 0; i < prefix.length; i++){
|
||||
if(array[i] != prefix[i]){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user