UNPKG

@nativescript-community/ble

Version:

Connect to and interact with Bluetooth LE peripherals.

1,102 lines 114 kB
/* eslint-disable no-caller */ import { arrayToNativeArray } from '@nativescript-community/arraybuffers'; import { Status, check, request } from '@nativescript-community/perms'; import { Application, Device, Trace, Utils } from '@nativescript/core'; import PQueue from 'p-queue'; import { BluetoothCommon, BluetoothError, CLog, CLogTypes, bluetoothEnabled, prepareArgs } from './index.common'; let _bluetoothInstance; export function getBluetoothInstance() { if (!_bluetoothInstance) { _bluetoothInstance = new Bluetooth(); } return _bluetoothInstance; } export { BleTraceCategory, BluetoothError } from './index.common'; const sdkVersion = parseInt(Device.sdkVersion, 10); let context; function getContext() { if (!context) { context = Utils.ad.getApplicationContext(); } return context; } const ACCESS_LOCATION_PERMISSION_REQUEST_CODE = 222; const ACTION_REQUEST_ENABLE_BLUETOOTH_REQUEST_CODE = 223; const GATT_SUCCESS = 0; const JELLY_BEAN = 18; const LOLLIPOP = 21; const MARSHMALLOW = 23; const OREO = 26; const ANDROID10 = 29; const MAX_MTU = 247; export var ScanMode; (function (ScanMode) { ScanMode[ScanMode["LOW_LATENCY"] = 0] = "LOW_LATENCY"; ScanMode[ScanMode["BALANCED"] = 1] = "BALANCED"; ScanMode[ScanMode["LOW_POWER"] = 2] = "LOW_POWER"; ScanMode[ScanMode["OPPORTUNISTIC"] = 3] = "OPPORTUNISTIC"; // = android.bluetooth.le.ScanSettings.SCAN_MODE_OPPORTUNISTIC })(ScanMode || (ScanMode = {})); function androidScanMode(mode) { switch (mode) { case ScanMode.BALANCED: return android.bluetooth.le.ScanSettings.SCAN_MODE_BALANCED; case ScanMode.LOW_POWER: return android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_POWER; case ScanMode.OPPORTUNISTIC: return android.bluetooth.le.ScanSettings.SCAN_MODE_OPPORTUNISTIC; case ScanMode.LOW_LATENCY: default: return android.bluetooth.le.ScanSettings.SCAN_MODE_LOW_LATENCY; } } export var MatchMode; (function (MatchMode) { MatchMode[MatchMode["AGGRESSIVE"] = 0] = "AGGRESSIVE"; MatchMode[MatchMode["STICKY"] = 1] = "STICKY"; // = android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY })(MatchMode || (MatchMode = {})); function androidMatchMode(mode) { switch (mode) { case MatchMode.STICKY: return android.bluetooth.le.ScanSettings.MATCH_MODE_STICKY; default: return android.bluetooth.le.ScanSettings.MATCH_MODE_AGGRESSIVE; } } export var MatchNum; (function (MatchNum) { MatchNum[MatchNum["MAX_ADVERTISEMENT"] = 0] = "MAX_ADVERTISEMENT"; MatchNum[MatchNum["FEW_ADVERTISEMENT"] = 1] = "FEW_ADVERTISEMENT"; MatchNum[MatchNum["ONE_ADVERTISEMENT"] = 2] = "ONE_ADVERTISEMENT"; // = android.bluetooth.le.ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT })(MatchNum || (MatchNum = {})); function androidMatchNum(mode) { switch (mode) { case MatchNum.ONE_ADVERTISEMENT: return android.bluetooth.le.ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT; case MatchNum.FEW_ADVERTISEMENT: return android.bluetooth.le.ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT; default: return android.bluetooth.le.ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT; } } export var CallbackType; (function (CallbackType) { CallbackType[CallbackType["ALL_MATCHES"] = 0] = "ALL_MATCHES"; CallbackType[CallbackType["FIRST_MATCH"] = 1] = "FIRST_MATCH"; CallbackType[CallbackType["MATCH_LOST"] = 2] = "MATCH_LOST"; // = android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST })(CallbackType || (CallbackType = {})); function androidCallbackType(mode) { switch (mode) { case CallbackType.MATCH_LOST: return android.bluetooth.le.ScanSettings.CALLBACK_TYPE_MATCH_LOST; case CallbackType.FIRST_MATCH: return android.bluetooth.le.ScanSettings.CALLBACK_TYPE_FIRST_MATCH; default: return android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES; } } export var Phy; (function (Phy) { Phy[Phy["LE_1M"] = 0] = "LE_1M"; Phy[Phy["LE_CODED"] = 1] = "LE_CODED"; Phy[Phy["LE_ALL_SUPPORTED"] = 2] = "LE_ALL_SUPPORTED"; // = android.bluetooth.le.ScanSettings.PHY_LE_ALL_SUPPORTED })(Phy || (Phy = {})); // function androidPhy(mode: Phy) { // switch (mode) { // case Phy.LE_1M: // return android.bluetooth.BluetoothDevice.PHY_LE_1M; // case Phy.LE_CODED: // return android.bluetooth.BluetoothDevice.PHY_LE_CODED; // default: // // PHY_LE_ALL_SUPPORTED // return android.bluetooth.le.ScanSettings.PHY_LE_ALL_SUPPORTED; // } // } const uuidRegexp = new RegExp('0000(.{4})-0000-1000-8000-00805f9b34fb'); export function uuidToString(uuid) { const uuidStr = uuid.toString(); if (uuidStr.length !== 4) { const match = uuidRegexp.exec(uuidStr); if (match) { return match[1]; } } return uuidStr; } export function arrayToNativeByteArray(val) { const length = val.length; const result = Array.create('byte', length); for (let i = 0; i < length; i++) { result[i] = val[i]; } return result; } export function byteArrayToBuffer(value) { if (!value) { return null; } const length = value.length; const ret = new Uint8Array(length); const isString = typeof value === 'string'; for (let i = 0; i < length; i++) { ret[i] = isString ? value.charCodeAt(i) : value[i]; } return ret.buffer; } // JS UUID -> Java export function stringToUuid(uuidStr) { if (uuidStr.length === 4) { uuidStr = '0000' + uuidStr + '-0000-1000-8000-00805f9b34fb'; } return java.util.UUID.fromString(uuidStr); } let LeScanCallbackVar; function initLeScanCallback() { if (LeScanCallbackVar) { return; } class ScanRecord { getManufacturerSpecificData() { return this.manufacturerData; } getBytes() { return this.bytes; } getAdvertiseFlags() { return this.advertiseFlags; } getServiceUuids() { return this.serviceUuids; } getServiceData() { return this.serviceData; } getDeviceName() { return this.localName; } getTxPowerLevel() { return this.txPowerLevel; } constructor(serviceUuids, manufacturerData, serviceData, advertiseFlags, txPowerLevel, localName, bytes) { this.serviceUuids = serviceUuids; this.manufacturerData = manufacturerData; this.serviceData = serviceData; this.advertiseFlags = advertiseFlags; this.txPowerLevel = txPowerLevel; this.localName = localName; this.bytes = bytes; } } class ScanAdvertisment { constructor(scanRecord) { this.scanRecord = scanRecord; } get manufacturerData() { const data = this.scanRecord.getManufacturerSpecificData(); const size = data.size(); if (size > 0) { const mKey = data.keyAt(0); return byteArrayToBuffer(data.get(mKey)); } return undefined; } get data() { return byteArrayToBuffer(this.scanRecord.getBytes()); } get manufacturerId() { const data = this.scanRecord.getManufacturerSpecificData(); const size = data.size(); if (size > 0) { return data.keyAt(0); } return -1; } get txPowerLevel() { return this.scanRecord.getTxPowerLevel(); } get localName() { return this.scanRecord.getDeviceName(); } get flags() { return this.scanRecord.getAdvertiseFlags(); } get serviceUUIDs() { const result = []; const serviceUuids = this.scanRecord.getServiceUuids(); for (let i = 0; i < serviceUuids.length; i++) { result.push(uuidToString(serviceUuids[i])); } return result; } get serviceData() { const result = {}; const serviceData = this.scanRecord.getServiceData(); const keys = Object.keys(serviceData); let currentKey; for (let i = 0; i < keys.length; i++) { currentKey = keys[i]; result[uuidToString(currentKey)] = byteArrayToBuffer(serviceData[currentKey]); } return result; } } // Helper method to extract bytes from byte array. function extractBytes(scanRecord, start, length) { // const bytes = new byte[length]; // System.arraycopy(scanRecord, start, bytes, 0, length); return java.util.Arrays.copyOfRange(scanRecord, start, start + length); } let BASE_UUID; function getBASE_UUID() { if (!BASE_UUID) { BASE_UUID = android.os.ParcelUuid.fromString('00000000-0000-1000-8000-00805F9B34FB'); } return BASE_UUID; } /** Length of bytes for 16 bit UUID */ const UUID_BYTES_16_BIT = 2; /** Length of bytes for 32 bit UUID */ const UUID_BYTES_32_BIT = 4; /** Length of bytes for 128 bit UUID */ const UUID_BYTES_128_BIT = 16; function parseUuidFrom(uuidBytes) { if (uuidBytes == null) { throw new Error('uuidBytes cannot be null'); } const length = uuidBytes.length; if (length !== UUID_BYTES_16_BIT && length !== UUID_BYTES_32_BIT && length !== UUID_BYTES_128_BIT) { throw new Error('uuidBytes length invalid - ' + length); } // Construct a 128 bit UUID. if (length === UUID_BYTES_128_BIT) { const buf = java.nio.ByteBuffer.wrap(uuidBytes).order(java.nio.ByteOrder.LITTLE_ENDIAN); const msb = buf.getLong(8); const lsb = buf.getLong(0); return new java.util.UUID(msb, lsb).toString(); } // For 16 bit and 32 bit UUID we need to convert them to 128 bit value. // 128_bit_value = uuid * 2^96 + BASE_UUID let 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; } const msb = getBASE_UUID().getUuid().getMostSignificantBits() + (shortUuid << 32); const lsb = getBASE_UUID().getUuid().getLeastSignificantBits(); return new java.util.UUID(msb, lsb).toString(); } function parseServiceUuid(scanRecord, currentPos, dataLength, uuidLength, serviceUuids) { while (dataLength > 0) { const uuidBytes = extractBytes(scanRecord, currentPos, uuidLength); serviceUuids.push(parseUuidFrom(uuidBytes)); dataLength -= uuidLength; currentPos += uuidLength; } return currentPos; } const DATA_TYPE_FLAGS = 0x01; const DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02; const DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03; const DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04; const DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05; const DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06; const DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07; const DATA_TYPE_LOCAL_NAME_SHORT = 0x08; const DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09; const DATA_TYPE_TX_POWER_LEVEL = 0x0a; const DATA_TYPE_SERVICE_DATA_16_BIT = 0x16; const DATA_TYPE_SERVICE_DATA_32_BIT = 0x20; const DATA_TYPE_SERVICE_DATA_128_BIT = 0x21; const DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xff; function parseFromBytes(scanRecord) { if (scanRecord == null) { return null; } let currentPos = 0; let advertiseFlag = -1; let serviceUuids = []; let localName = null; let txPowerLevel = Number.MIN_VALUE; const manufacturerData = new android.util.SparseArray(); // const manufacturerData = null; const serviceData = {}; try { while (currentPos < scanRecord.length) { // length is unsigned int. const length = scanRecord[currentPos++] & 0xff; if (length === 0) { break; } // Note the length includes the length of the field type itself. const dataLength = length - 1; // fieldType is unsigned int. const 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 = String.fromCharCode.apply(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: let serviceUuidLength = UUID_BYTES_16_BIT; if (fieldType === DATA_TYPE_SERVICE_DATA_32_BIT) { serviceUuidLength = UUID_BYTES_32_BIT; } else if (fieldType === DATA_TYPE_SERVICE_DATA_128_BIT) { serviceUuidLength = UUID_BYTES_128_BIT; } const serviceDataUuidBytes = extractBytes(scanRecord, currentPos, serviceUuidLength); const serviceDataUuid = parseUuidFrom(serviceDataUuidBytes); const serviceDataArray = extractBytes(scanRecord, currentPos + serviceUuidLength, dataLength - serviceUuidLength); serviceData[serviceDataUuid] = serviceDataArray; break; case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA: // The first two bytes of the manufacturer specific data are // manufacturer ids in little endian. const manufacturerId = ((scanRecord[currentPos + 1] & 0xff) << 8) + (scanRecord[currentPos] & 0xff); const 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.length === 0) { serviceUuids = null; } return new ScanRecord(serviceUuids, manufacturerData, serviceData, advertiseFlag, txPowerLevel, localName, scanRecord); } catch (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, -1, Number.MIN_VALUE, null, scanRecord); } } /** * Do not mark this one as a native class. Not doing so will allow this class to be compiled into a JavaScript pure class. * For a strange reason, LeScanCallback will throw errors if it's compiled into a function. * That is why we want it to remain a class after compile procedure. * Also, class will work properly if implementor is given as an argument to super class since method 'onLeScan' is originally abstract. */ class LeScanCallbackImpl extends android.bluetooth.BluetoothAdapter.LeScanCallback { constructor(owner) { super({ onLeScan(device, rssi, data) { if (Trace.isEnabled()) { CLog(CLogTypes.info, `TNS_LeScanCallback.onLeScan ---- device: ${device}, rssi: ${rssi}, scanRecord: ${data}`); } const owner = this.owner && this.owner.get(); if (!owner) { return; } let stateObject = owner.connections[device.getAddress()]; if (!stateObject) { stateObject = owner.connections[device.getAddress()] = { state: 'disconnected' }; const scanRecord = parseFromBytes(data); const advertismentData = new ScanAdvertisment(scanRecord); stateObject.advertismentData = advertismentData; const payload = { type: 'scanResult', // TODO or use different callback functions? UUID: device.getAddress(), // TODO consider renaming to id (and iOS as well) name: device.getName(), localName: advertismentData.localName, RSSI: rssi, state: 'disconnected', advertismentData, nativeDevice: device, manufacturerId: advertismentData.manufacturerId }; if (Trace.isEnabled()) { CLog(CLogTypes.info, `TNS_LeScanCallback.onLeScan ---- payload: ${JSON.stringify(payload)}`); } this.onPeripheralDiscovered && this.onPeripheralDiscovered(payload); owner.sendEvent(Bluetooth.device_discovered_event, payload); } } }); this.owner = owner; /** * Callback reporting an LE device found during a device scan initiated by the startLeScan(BluetoothAdapter.LeScanCallback) function. * @param device [android.bluetooth.BluetoothDevice] - Identifies the remote device * @param rssi [number] - The RSSI value for the remote device as reported by the Bluetooth hardware. 0 if no RSSI value is available. * @param scanRecord [byte[]] - The content of the advertisement record offered by the remote device. */ return global.__native(this); } } LeScanCallbackVar = LeScanCallbackImpl; } let ScanCallbackVar; function initScanCallback() { if (ScanCallbackVar) { return; } var ScanCallBackImpl = /** @class */ (function (_super) { __extends(ScanCallBackImpl, _super); function ScanCallBackImpl(owner) { var _this = _super.call(this) || this; _this.owner = owner; return global.__native(_this); } /** * Callback when batch results are delivered. * @param results [List<android.bluetooth.le.ScanResult>] - List of scan results that are previously scanned. */ ScanCallBackImpl.prototype.onBatchScanResults = function (results) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_ScanCallback.onBatchScanResults ---- results: ".concat(results)); } }; /** * Callback when scan could not be started. * @param errorCode [number] - Error code (one of SCAN_FAILED_*) for scan failure. */ ScanCallBackImpl.prototype.onScanFailed = function (errorCode) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_ScanCallback.onScanFailed ---- errorCode: ".concat(errorCode)); } var errorMessage; if (errorCode === android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED) { errorMessage = 'Scan already started'; } else if (errorCode === android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) { errorMessage = 'Application registration failed'; } else if (errorCode === android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED) { errorMessage = 'Feature unsupported'; } else if (errorCode === android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR) { errorMessage = 'Internal error'; } else { errorMessage = 'Scan failed to start'; } if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_ScanCallback.onScanFailed errorMessage: ".concat(errorMessage)); } }; /** * Callback when a BLE advertisement has been found. * @param callbackType [number] - Determines how this callback was triggered. Could be one of CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH or CALLBACK_TYPE_MATCH_LOST * @param result [android.bluetooth.le.ScanResult] - A Bluetooth LE scan result. */ ScanCallBackImpl.prototype.onScanResult = function (callbackType, result) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_ScanCallback.onScanResult ---- callbackType: ".concat(callbackType, ", result: ").concat(result)); } var owner = this.owner && this.owner.get(); if (!owner) { return; } var stateObject = owner.connections[result.getDevice().getAddress()]; if (!stateObject) { stateObject = owner.connections[result.getDevice().getAddress()] = { state: 'disconnected' }; } var advertismentData = new ScanAdvertisment(result.getScanRecord()); stateObject.advertismentData = advertismentData; var payload = { type: 'scanResult', // TODO or use different callback functions? UUID: result.getDevice().getAddress(), name: result.getDevice().getName(), RSSI: result.getRssi(), localName: advertismentData.localName, state: 'disconnected', manufacturerId: advertismentData.manufacturerId, advertismentData: advertismentData }; if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_ScanCallback.onScanResult ---- payload: ".concat(JSON.stringify(payload))); } this.onPeripheralDiscovered && this.onPeripheralDiscovered(payload); owner.sendEvent(Bluetooth.device_discovered_event, payload); }; return ScanCallBackImpl; }(android.bluetooth.le.ScanCallback)); class ScanAdvertisment { constructor(scanRecord) { this.scanRecord = scanRecord; } get manufacturerData() { const data = this.scanRecord.getManufacturerSpecificData(); const size = data ? data.size() : 0; if (size > 0) { const mKey = data.keyAt(0); return byteArrayToBuffer(data.get(mKey)); } return undefined; } get data() { return byteArrayToBuffer(this.scanRecord.getBytes()); } get manufacturerId() { const data = this.scanRecord.getManufacturerSpecificData(); const size = data ? data.size() : 0; if (size > 0) { return data.keyAt(0); } return -1; } get txPowerLevel() { return this.scanRecord.getTxPowerLevel(); } get localName() { let deviceName = this.scanRecord.getDeviceName(); if (deviceName) { deviceName = deviceName.replace('\0', '').replace('�', ''); } return deviceName; } get flags() { return this.scanRecord.getAdvertiseFlags(); } get serviceUUIDs() { const result = []; const serviceUuids = this.scanRecord.getServiceUuids(); if (serviceUuids) { for (let i = 0; i < serviceUuids.size(); i++) { result.push(uuidToString(serviceUuids.get(i))); } } return result; } get serviceData() { const result = {}; const serviceData = this.scanRecord.getServiceData(); if (serviceData && serviceData.size() > 0) { const entries = serviceData.entrySet().iterator(); while (entries.hasNext()) { const entry = entries.next(); result[uuidToString(entry.getKey())] = byteArrayToBuffer(entry.getValue()); } } return result; } } ScanCallbackVar = ScanCallBackImpl; } let BluetoothGattCallback; function initBluetoothGattCallback() { if (BluetoothGattCallback) { return; } var BluetoothGattCallbackImpl = /** @class */ (function (_super) { __extends(BluetoothGattCallbackImpl, _super); // private owner: WeakRef<Bluetooth>; function BluetoothGattCallbackImpl(owner) { var _this = _super.call(this) || this; _this.owner = owner; _this.subDelegates = []; return global.__native(_this); } BluetoothGattCallbackImpl.prototype.addSubDelegate = function (delegate) { var index = this.subDelegates.indexOf(delegate); if (index === -1) { this.subDelegates.push(delegate); } }; BluetoothGattCallbackImpl.prototype.removeSubDelegate = function (delegate) { var index = this.subDelegates.indexOf(delegate); if (index !== -1) { this.subDelegates.splice(index, 1); } }; /** * Callback indicating when GATT client has connected/disconnected to/from a remote GATT server. * @param bluetoothGatt [android.bluetooth.BluetoothGatt] - GATT client * @param status [number] - Status of the connect or disconnect operation. GATT_SUCCESS if the operation succeeds. * @param newState [number] - Returns the new connection state. Can be one of STATE_DISCONNECTED or STATE_CONNECTED */ BluetoothGattCallbackImpl.prototype.onConnectionStateChange = function (gatt, status, newState) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onConnectionStateChange ---- gatt: ".concat(gatt, ", device:").concat(gatt.getDevice() && gatt.getDevice().getAddress(), " status: ").concat(status, ", newState: ").concat(newState, ", subdelegates:").concat(this.subDelegates.length)); } this.subDelegates.forEach(function (d) { if (d.onConnectionStateChange) { d.onConnectionStateChange(gatt, status, newState); } }); var owner = this.owner && this.owner.get(); if (!owner) { return; } if (newState === android.bluetooth.BluetoothProfile.STATE_CONNECTED && status === GATT_SUCCESS) { var device = gatt.getDevice(); var address = null; if (device == null) { // happens some time, why ... ? } else { address = device.getAddress(); } var stateObject = owner.connections[address]; if (!stateObject) { owner.gattDisconnect(gatt); } } else { // perhaps the device was manually disconnected, or in use by another device owner.gattDisconnect(gatt); } }; /** * Callback invoked when the list of remote services, characteristics and descriptors for the remote device have been updated, ie new services have been discovered. * @param gatt [android.bluetooth.BluetoothGatt] - GATT client invoked discoverServices() * @param status [number] - GATT_SUCCESS if the remote device has been explored successfully. */ BluetoothGattCallbackImpl.prototype.onServicesDiscovered = function (gatt, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onServicesDiscovered ---- gatt: ".concat(gatt, ", status (0=success): ").concat(status, " ").concat(this.subDelegates)); } this.subDelegates.forEach(function (d) { if (d.onServicesDiscovered) { d.onServicesDiscovered(gatt, status); } }); }; /** * Callback reporting the result of a characteristic read operation. * @param gatt [android.bluetooth.BluetoothGatt] - GATT client invoked readCharacteristic(BluetoothGattCharacteristic) * @param characteristic - Characteristic that was read from the associated remote device. * @params value - byte: the value of the characteristic This value cannot be null. This will "status" in Android API level < 33. * @param status [number] - GATT_SUCCESS if the read operation was completed successfully. */ BluetoothGattCallbackImpl.prototype.onCharacteristicRead = function (gatt, characteristic, valueOrStatus, status) { this.subDelegates.forEach(function (d) { if (d.onCharacteristicRead) { d.onCharacteristicRead(gatt, characteristic, valueOrStatus, status); } }); }; /** * Callback triggered as a result of a remote characteristic notification. * @param gatt [android.bluetooth.BluetoothGatt] - GATT client the characteristic is associated with. * @param characteristic [android.bluetooth.BluetoothGattCharacteristic] - Characteristic that has been updated as a result of a remote notification event. */ BluetoothGattCallbackImpl.prototype.onCharacteristicChanged = function (gatt, characteristic) { var device = gatt.getDevice(); var pUUID = null; if (device == null) { // happens some time, why ... ? } else { pUUID = device.getAddress(); } if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onCharacteristicChanged ---- gatt: ".concat(gatt, ", characteristic: ").concat(characteristic, ", device: ").concat(pUUID)); } this.subDelegates.forEach(function (d) { if (d.onCharacteristicChanged) { d.onCharacteristicChanged(gatt, characteristic); } }); var owner = this.owner.get(); if (!owner) { return; } var stateObject = owner.connections[pUUID]; if (stateObject) { var cUUID = uuidToString(characteristic.getUuid()); var sUUID = uuidToString(characteristic.getService().getUuid()); var key = sUUID + '/' + cUUID; if (stateObject.onNotifyCallbacks && stateObject.onNotifyCallbacks[key]) { var value = characteristic.getValue(); stateObject.onNotifyCallbacks[key]({ android: value, value: byteArrayToBuffer(value), peripheralUUID: pUUID, serviceUUID: sUUID, characteristicUUID: cUUID }); } } }; /** * Callback indicating the result of a characteristic write operation. * If this callback is invoked while a reliable write transaction is in progress, the value of the characteristic represents the value reported by the remote device. * An application should compare this value to the desired value to be written. * If the values don't match, the application must abort the reliable write transaction. * @param gatt - GATT client invoked writeCharacteristic(BluetoothGattCharacteristic) * @param characteristic - Characteristic that was written to the associated remote device. * @param status - The result of the write operation GATT_SUCCESS if the operation succeeds. */ BluetoothGattCallbackImpl.prototype.onCharacteristicWrite = function (gatt, characteristic, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onCharacteristicWrite ---- gatt: ".concat(gatt, ", characteristic: ").concat(characteristic), this.subDelegates.length); } this.subDelegates.forEach(function (d) { if (d.onCharacteristicWrite) { d.onCharacteristicWrite(gatt, characteristic, status); } }); }; /** * Callback reporting the result of a descriptor read operation. * @param gatt - GATT client invoked readDescriptor(BluetoothGattDescriptor) * @param descriptor - Descriptor that was read from the associated remote device. * @param status - GATT_SUCCESS if the read operation was completed successfully */ BluetoothGattCallbackImpl.prototype.onDescriptorRead = function (gatt, descriptor, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onDescriptorRead ---- gatt: ".concat(gatt, ", descriptor: ").concat(descriptor, ", status: ").concat(status)); } this.subDelegates.forEach(function (d) { if (d.onDescriptorRead) { d.onDescriptorRead(gatt, descriptor, status); } }); }; /** * Callback indicating the result of a descriptor write operation. * @param gatt - GATT client invoked writeDescriptor(BluetoothGattDescriptor). * @param descriptor - Descriptor that was written to the associated remote device. * @param status - The result of the write operation GATT_SUCCESS if the operation succeeds. */ BluetoothGattCallbackImpl.prototype.onDescriptorWrite = function (gatt, descriptor, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onDescriptorWrite ---- gatt: ".concat(gatt, ", descriptor: ").concat(descriptor, ", status: ").concat(status)); } this.subDelegates.forEach(function (d) { if (d.onDescriptorWrite) { d.onDescriptorWrite(gatt, descriptor, status); } }); }; /** * Callback reporting the RSSI for a remote device connection. This callback is triggered in response to the readRemoteRssi() function. * @param gatt - GATT client invoked readRemoteRssi(). * @param rssi - The RSSI value for the remote device. * @param status - GATT_SUCCESS if the RSSI was read successfully. */ BluetoothGattCallbackImpl.prototype.onReadRemoteRssi = function (gatt, rssi, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onReadRemoteRssi ---- gatt: ".concat(gatt, " rssi: ").concat(rssi, ", status: ").concat(status)); } this.subDelegates.forEach(function (d) { if (d.onReadRemoteRssi) { d.onReadRemoteRssi(gatt, rssi, status); } }); }; /** * Callback indicating the MTU for a given device connection has changed. This callback is triggered in response to the requestMtu(int) function, or in response to a connection event. * @param gatt - GATT client invoked requestMtu(int). * @param mtu - The new MTU size. * @param status - GATT_SUCCESS if the MTU has been changed successfully. */ BluetoothGattCallbackImpl.prototype.onMtuChanged = function (gatt, mtu, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, "TNS_BluetoothGattCallback.onMtuChanged ---- gatt: ".concat(gatt, " mtu: ").concat(mtu, ", status: ").concat(status)); } var owner = this.owner.get(); if (owner) { owner.notify({ eventName: 'mtu', object: owner, data: { device: gatt, mtu: mtu } }); } this.subDelegates.forEach(function (d) { if (d.onMtuChanged) { d.onMtuChanged(gatt, mtu, status); } }); }; /** * Callback indicating the MTU for a given device connection has changed. This callback is triggered in response to the requestMtu(int) function, or in response to a connection event. * @param gatt - GATT client invoked requestMtu(int). * @param txPhy - The new tx PHY. * @param rxPhy - The new rx PHY. * @param status - GATT_SUCCESS if the PHY has been changed successfully. */ BluetoothGattCallbackImpl.prototype.onPhyUpdate = function (gatt, txPhy, rxPhy, status) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'TNS_BluetoothGattCallback.onPhyUpdate ---- gatt: ' + gatt + ' txPhy: ' + txPhy + ' rxPhy: ' + rxPhy + ', status: ' + status); } var owner = this.owner.get(); if (owner) { owner.notify({ eventName: 'phy', object: owner, data: { txPhy: txPhy, rxPhy: rxPhy } }); } this.subDelegates.forEach(function (d) { if (d.onPhyUpdate) { d.onPhyUpdate(gatt, txPhy, rxPhy, status); } }); }; return BluetoothGattCallbackImpl; }(android.bluetooth.BluetoothGattCallback)); BluetoothGattCallback = BluetoothGattCallbackImpl; } function getGattDeviceServiceInfo(gatt, args) { const services = gatt.getServices(); const servicesJs = []; const BluetoothGattCharacteristic = android.bluetooth.BluetoothGattCharacteristic; const serviceUUIDs = args.serviceUUIDs; const all = args.all; for (let i = 0; i < services.size(); i++) { const service = services.get(i); const serviceUUID = uuidToString(service.getUuid()); if (serviceUUIDs && serviceUUIDs.indexOf(serviceUUID) === -1) { continue; } let characteristicsJs; if (all === true) { const characteristics = service.getCharacteristics(); characteristicsJs = []; for (let j = 0; j < characteristics.size(); j++) { const characteristic = characteristics.get(j); const characteristicUUID = uuidToString(characteristic.getUuid()); const props = characteristic.getProperties(); const descriptors = characteristic.getDescriptors(); const descriptorsJs = []; for (let k = 0; k < descriptors.size(); k++) { const descriptor = descriptors.get(k); const descriptorJs = { UUID: uuidToString(descriptor.getUuid()), value: descriptor.getValue(), // always empty btw permissions: null }; const descPerms = descriptor.getPermissions(); if (descPerms > 0) { descriptorJs.permissions = { read: (descPerms & BluetoothGattCharacteristic.PERMISSION_READ) !== 0, readEncrypted: (descPerms & BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED) !== 0, readEncryptedMitm: (descPerms & BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM) !== 0, write: (descPerms & BluetoothGattCharacteristic.PERMISSION_WRITE) !== 0, writeEncrypted: (descPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED) !== 0, writeEncryptedMitm: (descPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM) !== 0, writeSigned: (descPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED) !== 0, writeSignedMitm: (descPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM) !== 0 }; } if (Trace.isEnabled()) { CLog(CLogTypes.info, `TNS_BluetoothGattCallback.onServicesDiscovered ---- pushing descriptor: ${descriptor}`); } descriptorsJs.push(descriptorJs); } const characteristicJs = { serviceUUID, UUID: characteristicUUID, name: characteristicUUID, // there's no sep field on Android properties: { read: (props & BluetoothGattCharacteristic.PROPERTY_READ) !== 0, write: (props & BluetoothGattCharacteristic.PROPERTY_WRITE) !== 0, writeWithoutResponse: (props & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) !== 0, notify: (props & BluetoothGattCharacteristic.PROPERTY_NOTIFY) !== 0, indicate: (props & BluetoothGattCharacteristic.PROPERTY_INDICATE) !== 0, broadcast: (props & BluetoothGattCharacteristic.PROPERTY_BROADCAST) !== 0, authenticatedSignedWrites: (props & BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) !== 0, extendedProperties: (props & BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS) !== 0 }, descriptors: descriptorsJs, permissions: null }; // permissions are usually not provided, so let's not return them in that case const charPerms = characteristic.getPermissions(); if (charPerms > 0) { characteristicJs.permissions = { read: (charPerms & BluetoothGattCharacteristic.PERMISSION_READ) !== 0, readEncrypted: (charPerms & BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED) !== 0, readEncryptedMitm: (charPerms & BluetoothGattCharacteristic.PERMISSION_READ_ENCRYPTED_MITM) !== 0, write: (charPerms & BluetoothGattCharacteristic.PERMISSION_WRITE) !== 0, writeEncrypted: (charPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED) !== 0, writeEncryptedMitm: (charPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_ENCRYPTED_MITM) !== 0, writeSigned: (charPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED) !== 0, writeSignedMitm: (charPerms & BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM) !== 0 }; } if (Trace.isEnabled()) { CLog(CLogTypes.info, `TNS_BluetoothGattCallback.onServicesDiscovered ---- pushing characteristic: ${JSON.stringify(characteristicJs)} for service:${serviceUUID}`); } characteristicsJs.push(characteristicJs); } } servicesJs.push({ UUID: serviceUUID, characteristics: characteristicsJs }); } return { services: servicesJs }; } export class Bluetooth extends BluetoothCommon { get adapter() { if (!this._adapter) { this._adapter = this.bluetoothManager.getAdapter(); } return this._adapter; } get bluetoothManager() { if (!this._bluetoothManager) { this._bluetoothManager = getContext().getSystemService(android.content.Context.BLUETOOTH_SERVICE); } return this._bluetoothManager; } get bluetoothGattCallback() { if (!this._bluetoothGattCallback) { initBluetoothGattCallback(); this._bluetoothGattCallback = new BluetoothGattCallback(new WeakRef(this)); } return this._bluetoothGattCallback; } constructor(restoreIdentifierOrOptions) { super(); /** * Connections are stored as key-val pairs of UUID-Connection. * So something like this: * [{ * 34343-2434-5454: { * state: 'connected', * discoveredState: '', * operationConnect: someCallbackFunction * }, * 1323213-21321323: { * .. * } * }, ..] */ this.connections = {}; this.broadcastRegistered = false; this.disconnectListeners = []; if (Trace.isEnabled()) { CLog(CLogTypes.info, '*** Android Bluetooth Constructor ***'); } // if >= Android21 (Lollipop) if (sdkVersion >= LOLLIPOP) { initScanCallback(); this.scanCallback = new ScanCallbackVar(new WeakRef(this)); } else if (sdkVersion >= JELLY_BEAN) { initLeScanCallback(); this.LeScanCallback = new LeScanCallbackVar(new WeakRef(this)); } if (typeof restoreIdentifierOrOptions === 'object' && !!restoreIdentifierOrOptions.disableAndroidQueue) { this.gattQueue = undefined; } else { this.gattQueue = new PQueue({ concurrency: 1 }); } } clear() { if (this.gattQueue) { this.gattQueue.clear(); } } registerBroadcast() { if (this.broadcastRegistered) { return; } this.broadcastRegistered = true; if (Trace.isEnabled()) { CLog(CLogTypes.info, 'Android Bluetooth registering for state change'); } Application.android.registerBroadcastReceiver(android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED, (context, intent) => { const state = intent.getIntExtra(android.bluetooth.BluetoothAdapter.EXTRA_STATE, android.bluetooth.BluetoothAdapter.ERROR); if (Trace.isEnabled()) { CLog(CLogTypes.info, 'Android Bluetooth ACTION_STATE_CHANGED', state, android.bluetooth.BluetoothAdapter.STATE_ON, android.bluetooth.BluetoothAdapter.STATE_OFF); } if (state === android.bluetooth.BluetoothAdapter.STATE_TURNING_OFF) { // ensure all connections are closed correctly on bluetooth closing Object.keys(this.connections).forEach((k) => { const stateObject = this.connections[k]; if (stateObject.device) { this.gattDisconnect(stateObject.device); } }); } if (state === android.bluetooth.BluetoothAdapter.STATE_ON || state === android.bluetooth.BluetoothAdapter.STATE_OFF) { this.sendEvent(Bluetooth.bluetooth_status_event, { state: state === android.bluetooth.BluetoothAdapter.STATE_ON ? 'on' : 'off' }); } }); } unregisterBroadcast() { if (!this.broadcastRegistered) { return; } this.broadcastRegistered = false; Application.android.unregisterBroadcastReceiver(android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED); } onListenerAdded(eventName, count) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'onListenerAdded', eventName, count); } if (eventName === Bluetooth.bluetooth_status_event) { this.registerBroadcast(); } } onListenerRemoved(eventName, count) { if (Trace.isEnabled()) { CLog(CLogTypes.info, 'onListenerRemoved', eventName, count); } if (eventName === Bluetooth.bluetooth_status_event && count === 0) { this.unregisterBroadcast(); } } stop() { this.unregisterBroadcast(); } getAndroidLocationManager() { return Utils.android.getApplicationContext().getSystemService(android.content.Context.LOCATION_SERVICE); } async hasLocationPermission() { if (sdkVersion >= 31) { // location permission not needed anymore return true; } return check('location', { coarse: false, precise: true }).then((r) => r === Status.Authorized); } async requestLocationPermission() { if (sdkVersion >= 31) { // location permission not needed anymore return true; } return request('location', { coarse: false, precise: true }).then((r) => r === Status.Authorized); } async isGPSEnabled() { if (sdkVersion >= 31) { // location permission not needed anymore return true; } if (!this.hasLocationPermission()) { return this.requestLocationPermission().then(() => this.isGPSEnabled()); } const result = this.getAndroidLocationManager().isProviderEnabled(android.location.LocationManager.GPS_PROVIDER); const providers = this.getAndroid