Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions android/src/main/java/so/onekey/lib/ble/utils/BleUtilsModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ class BleUtilsModule(private val reactContext: ReactApplicationContext) :

override fun getName() = NAME

// Lets JS skip waiting for events this OS version never broadcasts.
override fun getConstants(): MutableMap<String, Any> =
hashMapOf(
"supportsKeyMissingEvent" to supportsKeyMissingEvent(),
"supportsEncryptionChangeEvent" to supportsEncryptionChangeEvent()
)

private fun getBluetoothManager(): BluetoothManager? {
if (bluetoothManager == null) {
bluetoothManager =
Expand All @@ -53,6 +60,14 @@ class BleUtilsModule(private val reactContext: ReactApplicationContext) :

val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
if (supportsKeyMissingEvent()) {
filter.addAction(ACTION_KEY_MISSING)
}
if (supportsEncryptionChangeEvent()) {
filter.addAction(ACTION_ENCRYPTION_CHANGE)
// Tells JS when an earlier encryption result stops describing the current link.
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
val intentFilter = IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST)
intentFilter.priority = IntentFilter.SYSTEM_HIGH_PRIORITY
if (Build.VERSION.SDK_INT >= 34) {
Expand All @@ -78,6 +93,24 @@ class BleUtilsModule(private val reactContext: ReactApplicationContext) :
.emit("onDeviceBondState", params)
}

fun emitOnDeviceKeyMissing(params: WritableMap) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onDeviceKeyMissing", params)
}

fun emitOnDeviceEncryptionChange(params: WritableMap) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onDeviceEncryptionChange", params)
}

fun emitOnDeviceAclDisconnected(params: WritableMap) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("onDeviceAclDisconnected", params)
}

// 事件监听管理
@ReactMethod
fun addListener(eventName: String) {
Expand Down Expand Up @@ -138,6 +171,9 @@ class BleUtilsModule(private val reactContext: ReactApplicationContext) :

var bonded = false
var bonding = false
// False while bonding means the system (or another app) started it, e.g. the
// re-pairing Android runs by itself after it detects a lost bond.
var initiated = false

when (device.bondState) {
BluetoothDevice.BOND_BONDED -> {
Expand All @@ -154,12 +190,14 @@ class BleUtilsModule(private val reactContext: ReactApplicationContext) :
val started = device.createBond()
bonded = false
bonding = started
initiated = started
}
}

val map: WritableMap = Arguments.createMap()
map.putBoolean("bonded", bonded)
map.putBoolean("bonding", bonding)
map.putBoolean("initiated", initiated)
callback.invoke(null, map)
} catch (e: Exception) {
Log.e(LOG_TAG, "pairDevice error: ${e.message}")
Expand Down Expand Up @@ -262,18 +300,78 @@ class BleUtilsModule(private val reactContext: ReactApplicationContext) :
val bond = Arguments.createMap()
bond.putString("state", bondStateStr)
bond.putString("preState", prevBondStateStr)
// EXTRA_UNBOND_REASON is not exposed by the public Android SDK.
val reasonExtra = "android.bluetooth.device.extra.REASON"
if (bondState == BluetoothDevice.BOND_NONE && intent.hasExtra(reasonExtra)) {
bond.putInt("reason", intent.getIntExtra(reasonExtra, BluetoothDevice.ERROR))
}

val peripheral = Peripheral(device!!)
val map = peripheral.asWritableMap()
map.putMap("bondState", bond)
Log.d(LOG_TAG, "onReceive BluetoothDevice BondState Change ${map}")
module.emitOnDeviceBondState(map)
} else if (action == ACTION_KEY_MISSING) {
val device = deviceExtra(intent) ?: return

val map = Arguments.createMap()
map.putString("id", device.address)
Log.d(LOG_TAG, "onReceive BluetoothDevice KeyMissing")
module.emitOnDeviceKeyMissing(map)
} else if (action == ACTION_ENCRYPTION_CHANGE) {
val device = deviceExtra(intent) ?: return
if (!isLeTransport(intent)) return

val map = Arguments.createMap()
map.putString("id", device.address)
// HCI status: 0 on success, 6 (PIN or key missing) when the peer lost the bond.
map.putInt("status", intent.getIntExtra(EXTRA_ENCRYPTION_STATUS, BluetoothDevice.ERROR))
map.putBoolean("enabled", intent.getBooleanExtra(EXTRA_ENCRYPTION_ENABLED, false))
Log.d(LOG_TAG, "onReceive BluetoothDevice EncryptionChange ${map}")
module.emitOnDeviceEncryptionChange(map)
} else if (action == BluetoothDevice.ACTION_ACL_DISCONNECTED) {
val device = deviceExtra(intent) ?: return
if (!isLeTransport(intent)) return

val map = Arguments.createMap()
map.putString("id", device.address)
Log.d(LOG_TAG, "onReceive BluetoothDevice AclDisconnected")
module.emitOnDeviceAclDisconnected(map)
}
}

private fun deviceExtra(intent: Intent): BluetoothDevice? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
} else {
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}

// A dual-mode peer reports BR/EDR events too; only the LE link carries GATT.
private fun isLeTransport(intent: Intent): Boolean {
if (!intent.hasExtra(EXTRA_TRANSPORT)) return true
return intent.getIntExtra(EXTRA_TRANSPORT, TRANSPORT_LE) == TRANSPORT_LE
}
}

companion object {
const val NAME = "BleUtilsModule"
const val LOG_TAG: String = "RNBleUtils"

// These BluetoothDevice constants are public from API 36. The literals keep this
// module building against older compileSdk versions.
const val ACTION_KEY_MISSING = "android.bluetooth.device.action.KEY_MISSING"
const val ACTION_ENCRYPTION_CHANGE = "android.bluetooth.device.action.ENCRYPTION_CHANGE"
private const val EXTRA_ENCRYPTION_STATUS = "android.bluetooth.device.extra.ENCRYPTION_STATUS"
private const val EXTRA_ENCRYPTION_ENABLED = "android.bluetooth.device.extra.ENCRYPTION_ENABLED"
// BluetoothDevice.EXTRA_TRANSPORT and TRANSPORT_LE, public from API 33 and 23.
private const val EXTRA_TRANSPORT = "android.bluetooth.device.extra.TRANSPORT"
private const val TRANSPORT_LE = 2
private const val LINK_SECURITY_EVENTS_MIN_SDK = 36

fun supportsKeyMissingEvent(): Boolean = Build.VERSION.SDK_INT >= LINK_SECURITY_EVENTS_MIN_SDK

fun supportsEncryptionChangeEvent(): Boolean =
Build.VERSION.SDK_INT >= LINK_SECURITY_EVENTS_MIN_SDK
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onekeyfe/react-native-ble-utils",
"version": "0.1.6",
"version": "0.1.9",
"description": "ble uilts",
"source": "./src/index.tsx",
"main": "./dist/commonjs/index.js",
Expand Down
117 changes: 115 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,39 @@
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import type { BleState, Peripheral, BondState, AdvertisingData } from './type';
import type {
BleState,
Peripheral,
BondState,
AdvertisingData,
KeyMissingPeripheral,
EncryptionChangePeripheral,
AclDisconnectedPeripheral,
} from './type';

const { BleUtilsModule } = NativeModules;
type PairDeviceResult = {
bonded: boolean;
bonding: boolean;
/**
* [Android only] True when this call started the bonding. False while `bonding` is
* true means the system started it, e.g. its own re-pairing after a lost bond.
* Undefined on native builds that predate the field.
*/
initiated?: boolean;
};

type NativeConstantName =
| 'supportsKeyMissingEvent'
| 'supportsEncryptionChangeEvent';

const readNativeFlag = (name: NativeConstantName): boolean => {
if (Platform.OS !== 'android' || !BleUtilsModule) return false;
// Interop exposes legacy constants through getConstants(); the bridge exposes them
// as plain properties.
const constants =
typeof BleUtilsModule.getConstants === 'function'
? BleUtilsModule.getConstants()
: BleUtilsModule;
return constants?.[name] === true;
};

class BleUtils {
Expand Down Expand Up @@ -115,7 +144,91 @@ class BleUtils {
this.UiEventEmitter?.removeAllListeners('onDeviceBondState');
};
}

/**
* [Android only]
* Whether this OS version and native build report `onDeviceKeyMissing`. When false
* the event never fires, so callers should not wait for it.
*/
supportsDeviceKeyMissing() {
return readNativeFlag('supportsKeyMissingEvent');
}

/**
* [Android 16+ only]
* A bonded device could not provide its keys when the link was encrypted: the device
* was wiped or removed the bond. Android keeps its side of the bond, so the device
* stays unusable until the user forgets it in system Bluetooth settings.
* Each subscription is removed on its own, so several callers can listen at once.
* @param callback
*/
onDeviceKeyMissing(callback: (peripheral: KeyMissingPeripheral) => void) {
const subscription = this.UiEventEmitter?.addListener(
'onDeviceKeyMissing',
callback
);
return () => {
subscription?.remove();
};
}

/**
* [Android only]
* Whether this OS version and native build report `onDeviceEncryptionChange` and
* `onDeviceAclDisconnected`. When false those events never fire.
*/
supportsDeviceEncryptionChange() {
return readNativeFlag('supportsEncryptionChangeEvent');
}

/**
* [Android 16+ only]
* The LE link to a device finished an encryption attempt. Android starts it on its own
* right after connecting to a bonded device, so this reports whether the stored bond
* still works before any request that needs encryption is sent.
* Each subscription is removed on its own, so several callers can listen at once.
* @param callback
*/
onDeviceEncryptionChange(
callback: (peripheral: EncryptionChangePeripheral) => void
) {
const subscription = this.UiEventEmitter?.addListener(
'onDeviceEncryptionChange',
callback
);
return () => {
subscription?.remove();
};
}

/**
* [Android 16+ only]
* The LE link to a device is gone, so earlier encryption results no longer apply.
* Unlike a GATT disconnect, this does not fire while the system keeps the link alive
* for another client.
* @param callback
*/
onDeviceAclDisconnected(
callback: (peripheral: AclDisconnectedPeripheral) => void
) {
const subscription = this.UiEventEmitter?.addListener(
'onDeviceAclDisconnected',
callback
);
return () => {
subscription?.remove();
};
}
}

export default new BleUtils();
export type { BleState, Peripheral, BondState, AdvertisingData };
export type {
BleState,
Peripheral,
BondState,
AdvertisingData,
KeyMissingPeripheral,
EncryptionChangePeripheral,
AclDisconnectedPeripheral,
PairDeviceResult,
};
21 changes: 21 additions & 0 deletions src/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,27 @@ export interface Peripheral {
export interface BondState {
state: string;
preState: string;
/** Android EXTRA_UNBOND_REASON, when supplied by the system. */
reason?: number;
}

export interface KeyMissingPeripheral {
/** Device MAC address. */
id: string;
}

export interface EncryptionChangePeripheral {
/** Device MAC address. */
id: string;
/** HCI status of the encryption procedure: 0 on success, 6 when the peer lost the bond. */
status: number;
/** Whether the LE link is encrypted after the change. */
enabled: boolean;
}

export interface AclDisconnectedPeripheral {
/** Device MAC address. */
id: string;
}

export interface AdvertisingData {
Expand Down
Loading