@nativescript-community/ble
Version:
Connect to and interact with Bluetooth LE peripherals.
1,102 lines • 114 kB
JavaScript
/* 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