From c9e868896cf452e426a204878293a630880a4922 Mon Sep 17 00:00:00 2001 From: pwespi Date: Sat, 10 Apr 2021 16:17:28 +0200 Subject: [PATCH] feat(android): add createBond and isBonded * feat(android): add createBond and isBonded * chore(): fix swiftlint scripts * fix(web): remove unused options --- README.md | 34 +++++++ .../plugins/bluetoothle/BluetoothLe.kt | 88 +++++++++++++------ .../community/plugins/bluetoothle/Device.kt | 61 ++++++++++++- .../plugins/bluetoothle/DeviceList.kt | 2 +- ios/Plugin/Plugin.m | 2 + ios/Plugin/Plugin.swift | 8 ++ package.json | 4 +- src/bleClient.ts | 28 ++++++ src/definitions.ts | 16 ++-- src/web.ts | 17 +++- 10 files changed, 218 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 755ab23a..0b38f3aa 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ Below is an index of all the methods available. - [`requestLEScan(...)`](#requestlescan) - [`stopLEScan()`](#stoplescan) - [`connect(...)`](#connect) +- [`createBond(...)`](#createbond) +- [`isBonded(...)`](#isbonded) - [`disconnect(...)`](#disconnect) - [`read(...)`](#read) - [`write(...)`](#write) @@ -409,6 +411,38 @@ Connect to a peripheral BLE device. For an example, see [usage](#usage). --- +### createBond(...) + +```typescript +createBond(deviceId: string) => Promise +``` + +Create a bond with a peripheral BLE device. +Only available on Android. + +| Param | Type | Description | +| -------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| **`deviceId`** | string | The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) | + +--- + +### isBonded(...) + +```typescript +isBonded(deviceId: string) => Promise +``` + +Report whether a peripheral BLE device is bonded. +Only available on Android. + +| Param | Type | Description | +| -------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| **`deviceId`** | string | The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) | + +**Returns:** Promise<boolean> + +--- + ### disconnect(...) ```typescript diff --git a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt index a2ff0c19..4b2bd7af 100644 --- a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt +++ b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/BluetoothLe.kt @@ -72,13 +72,12 @@ class BluetoothLe : Plugin() { @PluginMethod fun startEnabledNotifications(call: PluginCall) { assertBluetoothAdapter(call) ?: return - createStateReceiver() try { - val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED); - context.registerReceiver(stateReceiver, intentFilter); + createStateReceiver() } catch (e: Error) { - call.reject("Error") + Log.e(TAG, "Error while registering enabled state receiver: ${e.localizedMessage}") + call.reject("startEnabledNotifications failed.") return } call.resolve() @@ -99,6 +98,8 @@ class BluetoothLe : Plugin() { } } } + val intentFilter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED) + context.registerReceiver(stateReceiver, intentFilter) } } @@ -199,27 +200,7 @@ class BluetoothLe : Plugin() { @PluginMethod fun connect(call: PluginCall) { - assertBluetoothAdapter(call) ?: return - val deviceId = call.getString("deviceId", null) - if (deviceId == null) { - call.reject("deviceId required.") - return - } - - val device: Device - try { - device = Device( - activity.applicationContext, - bluetoothAdapter!!, - deviceId - ) { -> - onDisconnect(deviceId) - } - } catch (e: IllegalArgumentException) { - call.reject("Invalid deviceId") - return - } - deviceMap[deviceId] = device + val device = getOrCreateDevice(call) ?: return device.connect { response -> run { if (response.success) { @@ -235,6 +216,29 @@ class BluetoothLe : Plugin() { notifyListeners("disconnected|${deviceId}", null) } + @PluginMethod + fun createBond(call: PluginCall) { + val device = getOrCreateDevice(call) ?: return + device.createBond { response -> + run { + if (response.success) { + call.resolve() + } else { + call.reject(response.value) + } + } + } + } + + @PluginMethod + fun isBonded(call: PluginCall) { + val device = getOrCreateDevice(call) ?: return + val isBonded = device.isBonded() + val result = JSObject() + result.put("value", isBonded) + call.resolve(result) + } + @PluginMethod fun disconnect(call: PluginCall) { val device = getDevice(call) ?: return @@ -479,14 +483,41 @@ class BluetoothLe : Plugin() { ) } - - private fun getDevice(call: PluginCall): Device? { - assertBluetoothAdapter(call) ?: return null + private fun getDeviceId(call: PluginCall): String? { val deviceId = call.getString("deviceId", null) if (deviceId == null) { call.reject("deviceId required.") return null } + return deviceId + } + + private fun getOrCreateDevice(call: PluginCall): Device? { + assertBluetoothAdapter(call) ?: return null + val deviceId = getDeviceId(call) ?: return null + val device = deviceMap[deviceId] + if (device != null) { + return device + } + try { + val newDevice = Device( + activity.applicationContext, + bluetoothAdapter!!, + deviceId + ) { -> + onDisconnect(deviceId) + } + deviceMap[deviceId] = newDevice + return newDevice + } catch (e: IllegalArgumentException) { + call.reject("Invalid deviceId") + return null + } + } + + private fun getDevice(call: PluginCall): Device? { + assertBluetoothAdapter(call) ?: return null + val deviceId = getDeviceId(call) ?: return null val device = deviceMap[deviceId] if (device == null || !device.isConnected()) { call.reject("Not connected to device.") @@ -495,7 +526,6 @@ class BluetoothLe : Plugin() { return device } - private fun getCharacteristic(call: PluginCall): Pair? { val serviceString = call.getString("service", null) val serviceUUID: UUID? diff --git a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt index ce4de119..2041217a 100644 --- a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt +++ b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/Device.kt @@ -1,8 +1,10 @@ package com.capacitorjs.community.plugins.bluetoothle import android.bluetooth.* -import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT +import android.content.BroadcastReceiver import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Handler import android.util.Log import java.util.* @@ -36,6 +38,7 @@ class Device( private var callbackMap = HashMap Unit)>() private var timeoutMap = HashMap() private var setNotificationsKey = "" + private var bondStateReceiver: BroadcastReceiver? = null private val gattCallback: BluetoothGattCallback = object : BluetoothGattCallback() { override fun onConnectionStateChange( @@ -174,6 +177,62 @@ class Device( } } + fun createBond(callback: (CallbackResponse) -> Unit) { + val key = "createBond" + callbackMap[key] = callback + try { + createBondStateReceiver() + } catch (e: Error) { + Log.e(TAG, "Error while registering bondStateReceiver: ${e.localizedMessage}") + reject(key, "Creating bond failed.") + return + } + val result = device.createBond() + if (!result) { + reject(key, "Creating bond failed.") + return + } + // if already bonded, resolve immediately + if (isBonded()) { + resolve(key, "Creating bond succeeded.") + return + } + // otherwise, wait for bond state change + } + + private fun createBondStateReceiver() { + if (bondStateReceiver == null) { + bondStateReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + if (action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) { + val key = "createBond" + val updatedDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) + // BroadcastReceiver receives bond state updates from all devices, need to filter by device + if (device.address == updatedDevice?.address) { + val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1) + val previousBondState = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1) + Log.d(TAG, "Bond state transition $previousBondState -> $bondState") + if (bondState == BluetoothDevice.BOND_BONDED) { + resolve(key, "Creating bond succeeded.") + } else if (previousBondState == BluetoothDevice.BOND_BONDING && bondState == BluetoothDevice.BOND_NONE) { + reject(key, "Creating bond failed.") + } else if (bondState == -1) { + reject(key, "Creating bond failed.") + } + } + } + } + } + val intentFilter = IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED) + context.registerReceiver(bondStateReceiver, intentFilter) + } + } + + fun isBonded(): Boolean { + return device.bondState == BluetoothDevice.BOND_BONDED + } + fun disconnect(callback: (CallbackResponse) -> Unit) { callbackMap["disconnect"] = callback if (bluetoothGatt == null) { diff --git a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt index 5dd4dbec..a54095cc 100644 --- a/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt +++ b/android/src/main/java/com/capacitorjs/community/plugins/bluetoothle/DeviceList.kt @@ -1,7 +1,7 @@ package com.capacitorjs.community.plugins.bluetoothle import android.bluetooth.BluetoothDevice -import java.util.ArrayList +import java.util.* class DeviceList { private val devices: ArrayList = ArrayList() diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m index 90135aa1..40175851 100644 --- a/ios/Plugin/Plugin.m +++ b/ios/Plugin/Plugin.m @@ -12,6 +12,8 @@ CAP_PLUGIN_METHOD(requestLEScan, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(stopLEScan, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(connect, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(createBond, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(isBonded, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(disconnect, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(read, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(write, CAPPluginReturnPromise); diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index 5a41eb34..ee227850 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -127,6 +127,14 @@ public class BluetoothLe: CAPPlugin { } + @objc func createBond(_ call: CAPPluginCall) { + call.reject("Unavailable") + } + + @objc func isBonded(_ call: CAPPluginCall) { + call.reject("Unavailable") + } + @objc func disconnect(_ call: CAPPluginCall) { guard self.getDeviceManager(call) != nil else { return } guard let device = self.getDevice(call) else { return } diff --git a/package.json b/package.json index 73196aba..98884879 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "verify:ios": "cd ios && pod install && xcodebuild clean build test -workspace Plugin.xcworkspace -scheme Plugin -destination \"platform=iOS Simulator,name=iPhone 12\" && cd ..", "verify:android": "cd android && ./gradlew clean build test && cd ..", "verify:web": "npm run build", - "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", - "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- autocorrect --format", + "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint ios", + "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- lint --fix --format ios", "eslint": "eslint . --ext ts", "prettier": "prettier \"**/*.{css,html,ts,js,java}\"", "swiftlint": "node-swiftlint", diff --git a/src/bleClient.ts b/src/bleClient.ts index a8083b89..b6f89068 100644 --- a/src/bleClient.ts +++ b/src/bleClient.ts @@ -74,6 +74,20 @@ export interface BleClientInterface { onDisconnect?: (deviceId: string) => void, ): Promise; + /** + * Create a bond with a peripheral BLE device. + * Only available on Android. + * @param deviceId The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) + */ + createBond(deviceId: string): Promise; + + /** + * Report whether a peripheral BLE device is bonded. + * Only available on Android. + * @param deviceId The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) + */ + isBonded(deviceId: string): Promise; + /** * Disconnect from a peripheral BLE device. For an example, see [usage](#usage). * @param deviceId The ID of the device to use (obtained from [requestDevice](#requestDevice) or [requestLEScan](#requestLEScan)) @@ -250,6 +264,20 @@ class BleClientClass implements BleClientInterface { }); } + async createBond(deviceId: string): Promise { + await this.queue(async () => { + await BluetoothLe.createBond({ deviceId }); + }); + } + + async isBonded(deviceId: string): Promise { + const isBonded = await this.queue(async () => { + const result = await BluetoothLe.isBonded({ deviceId }); + return result.value; + }); + return isBonded; + } + async disconnect(deviceId: string): Promise { await this.queue(async () => { await BluetoothLe.disconnect({ deviceId }); diff --git a/src/definitions.ts b/src/definitions.ts index c897750b..aaa52c20 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -75,7 +75,7 @@ export interface BleDevice { uuids?: string[]; } -export interface ConnectOptions { +export interface DeviceIdOptions { deviceId: string; } @@ -98,6 +98,10 @@ export interface WriteOptions { value: Data; } +export interface BooleanResult { + value: boolean; +} + export interface ReadResult { /** * android, ios: string @@ -156,7 +160,7 @@ export interface ScanResult { export interface BluetoothLePlugin { initialize(): Promise; - getEnabled(): Promise<{ value: boolean }>; + getEnabled(): Promise; startEnabledNotifications(): Promise; stopEnabledNotifications(): Promise; requestDevice(options?: RequestBleDeviceOptions): Promise; @@ -164,7 +168,7 @@ export interface BluetoothLePlugin { stopLEScan(): Promise; addListener( eventName: 'onEnabledChanged', - listenerFunc: (result: { value: boolean }) => void, + listenerFunc: (result: BooleanResult) => void, ): PluginListenerHandle; addListener( eventName: string, @@ -174,8 +178,10 @@ export interface BluetoothLePlugin { eventName: 'onScanResult', listenerFunc: (result: ScanResultInternal) => void, ): PluginListenerHandle; - connect(options: ConnectOptions): Promise; - disconnect(options: ConnectOptions): Promise; + connect(options: DeviceIdOptions): Promise; + createBond(options: DeviceIdOptions): Promise; + isBonded(options: DeviceIdOptions): Promise; + disconnect(options: DeviceIdOptions): Promise; read(options: ReadOptions): Promise; write(options: WriteOptions): Promise; writeWithoutResponse(options: WriteOptions): Promise; diff --git a/src/web.ts b/src/web.ts index 8cfd32ea..68333ff1 100644 --- a/src/web.ts +++ b/src/web.ts @@ -8,7 +8,8 @@ import { import type { BleDevice, BluetoothLePlugin, - ConnectOptions, + BooleanResult, + DeviceIdOptions, ReadOptions, ReadResult, RequestBleDeviceOptions, @@ -40,7 +41,7 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { } } - async getEnabled(): Promise<{ value: true }> { + async getEnabled(): Promise { // not available on web return { value: true }; } @@ -113,7 +114,7 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { this.scan = null; } - async connect(options: ConnectOptions): Promise { + async connect(options: DeviceIdOptions): Promise { const device = await this.getDevice(options.deviceId); device.removeEventListener('gattserverdisconnected', this.onDisconnected); device.addEventListener('gattserverdisconnected', this.onDisconnected); @@ -146,7 +147,15 @@ export class BluetoothLeWeb extends WebPlugin implements BluetoothLePlugin { BluetoothLe.notifyListeners(key, null); } - async disconnect(options: ConnectOptions): Promise { + async createBond(): Promise { + throw new Error('Unavailable'); + } + + async isBonded(): Promise { + throw new Error('Unavailable'); + } + + async disconnect(options: DeviceIdOptions): Promise { this.getDevice(options.deviceId).gatt?.disconnect(); }