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
34 changes: 34 additions & 0 deletions android/src/main/java/com/munimbluetooth/BleUuid.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.munimbluetooth

import android.os.ParcelUuid
import java.util.UUID

private const val BLUETOOTH_BASE_UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb"

/**
* Parses a Bluetooth UUID passed from JS. Besides full 128-bit UUIDs this
* accepts the 16- and 32-bit short forms ("180D", "0x180D", "0000180D") that
* iOS accepts and the README uses, expanding them onto the Bluetooth base UUID.
* `UUID.fromString` alone rejects short forms.
*
* @throws IllegalArgumentException when [value] is not a Bluetooth UUID.
*/
internal fun parseBleUuid(value: String): UUID {
val hex = if (value.startsWith("0x", ignoreCase = true)) value.substring(2) else value
val isShortForm = (hex.length == 4 || hex.length == 8) &&
hex.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
if (isShortForm) {
return UUID.fromString(hex.padStart(8, '0') + BLUETOOTH_BASE_UUID_SUFFIX)
}
return UUID.fromString(value)
}

internal fun parseBleParcelUuid(value: String): ParcelUuid = ParcelUuid(parseBleUuid(value))

/** True when [uuid] is the Bluetooth UUID that [value] names, in any accepted form. */
internal fun isSameBleUuid(uuid: UUID, value: String): Boolean =
try {
uuid == parseBleUuid(value)
} catch (_: IllegalArgumentException) {
false
}
55 changes: 33 additions & 22 deletions android/src/main/java/com/munimbluetooth/HybridMunimBluetooth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,13 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {

for (serviceData in services) {
val service = BluetoothGattService(
UUID.fromString(serviceData.uuid),
parseBleUuid(serviceData.uuid),
BluetoothGattService.SERVICE_TYPE_PRIMARY
)

for (characteristicData in serviceData.characteristics) {
val characteristic = BluetoothGattCharacteristic(
UUID.fromString(characteristicData.uuid),
parseBleUuid(characteristicData.uuid),
propertiesFromArray(characteristicData.properties),
characteristicPermissionsFromArray(
characteristicData.permissions,
Expand All @@ -493,7 +493,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
}
characteristicData.descriptors?.forEach { descriptorData ->
val descriptor = BluetoothGattDescriptor(
UUID.fromString(descriptorData.uuid),
parseBleUuid(descriptorData.uuid),
descriptorPermissionsFromArray(descriptorData.permissions)
)
descriptorData.value?.let { value ->
Expand Down Expand Up @@ -973,7 +973,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {

fun filter(serviceUUID: String?): ScanFilter {
val builder = ScanFilter.Builder()
serviceUUID?.let { builder.setServiceUuid(ParcelUuid.fromString(it)) }
serviceUUID?.let { builder.setServiceUuid(parseBleParcelUuid(it)) }
deviceName?.let { builder.setDeviceName(it) }
deviceAddress?.let { builder.setDeviceAddress(it) }
if (manufacturerId != null) {
Expand Down Expand Up @@ -1929,7 +1929,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
val promise = Promise<String>()
val dataBuilder = AdvertiseData.Builder()
options.serviceUUIDs?.forEach { uuid ->
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
dataBuilder.addServiceUuid(parseBleParcelUuid(uuid))
}
normalizeAdvertisingData(
options.advertisingData,
Expand Down Expand Up @@ -2501,17 +2501,28 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
}

val dataBuilder = AdvertiseData.Builder()
currentServiceUUIDs.forEach { uuid ->
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
}

val scanResponseBuilder = AdvertiseData.Builder()
currentAdvertisingData?.let {
processAdvertisingData(
data = it,
dataBuilder = scanResponseBuilder,
includeServiceUuids = false
try {
currentServiceUUIDs.forEach { uuid ->
dataBuilder.addServiceUuid(parseBleParcelUuid(uuid))
}
currentAdvertisingData?.let {
processAdvertisingData(
data = it,
dataBuilder = scanResponseBuilder,
includeServiceUuids = false
)
}
} catch (error: IllegalArgumentException) {
// This coroutine has no exception handler: an invalid UUID from
// JS would otherwise crash the app instead of failing the start.
val message = error.message ?: "Invalid advertising data"
Log.e(TAG, "Advertising failed: $message", error)
eventEmitter.emit(
"advertisingStartFailed",
mapOf("error" to message, "message" to message)
)
return@launch
}

val settings = AdvertiseSettings.Builder()
Expand Down Expand Up @@ -3918,10 +3929,10 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
serviceUUID: String,
characteristicUUID: String
): BluetoothGattCharacteristic? {
val service = gatt.services.firstOrNull { it.uuid.toString().equals(serviceUUID, ignoreCase = true) }
val service = gatt.services.firstOrNull { isSameBleUuid(it.uuid, serviceUUID) }
?: return null
return service.characteristics.firstOrNull {
it.uuid.toString().equals(characteristicUUID, ignoreCase = true)
isSameBleUuid(it.uuid, characteristicUUID)
}
}

Expand All @@ -3933,7 +3944,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
): BluetoothGattDescriptor? {
val characteristic = findCharacteristic(gatt, serviceUUID, characteristicUUID) ?: return null
return characteristic.descriptors.firstOrNull {
it.uuid.toString().equals(descriptorUUID, ignoreCase = true)
isSameBleUuid(it.uuid, descriptorUUID)
}
}

Expand Down Expand Up @@ -4026,8 +4037,8 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
characteristicUUID: String
): BluetoothGattCharacteristic? {
return try {
val service = gattServer?.getService(UUID.fromString(serviceUUID))
service?.getCharacteristic(UUID.fromString(characteristicUUID))
val service = gattServer?.getService(parseBleUuid(serviceUUID))
service?.getCharacteristic(parseBleUuid(characteristicUUID))
} catch (_: IllegalArgumentException) {
null
}
Expand Down Expand Up @@ -4727,7 +4738,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {

private fun addServiceUUIDs(uuids: Array<String>?, dataBuilder: AdvertiseData.Builder) {
uuids?.forEach { uuid ->
dataBuilder.addServiceUuid(ParcelUuid.fromString(uuid))
dataBuilder.addServiceUuid(parseBleParcelUuid(uuid))
}
}

Expand All @@ -4737,7 +4748,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {
) {
serviceDataEntries?.forEach { entry ->
hexStringToByteArray(entry.data)?.let { dataBytes ->
dataBuilder.addServiceData(ParcelUuid.fromString(entry.uuid), dataBytes)
dataBuilder.addServiceData(parseBleParcelUuid(entry.uuid), dataBytes)
}
}
}
Expand Down Expand Up @@ -4800,7 +4811,7 @@ class HybridMunimBluetooth : HybridMunimBluetoothSpec() {

serviceUUIDs.forEach { uuid ->
val service = BluetoothGattService(
UUID.fromString(uuid),
parseBleUuid(uuid),
BluetoothGattService.SERVICE_TYPE_PRIMARY
)
gattServer?.addService(service)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ class MunimBluetoothBackgroundService : Service() {
val data = AdvertiseData.Builder()

serviceUUIDs.forEach { uuid ->
runCatching { ParcelUuid.fromString(uuid) }.getOrNull()?.let(data::addServiceUuid)
runCatching { parseBleParcelUuid(uuid) }.getOrNull()?.let(data::addServiceUuid)
}

val scanResponse = AdvertiseData.Builder()
Expand Down Expand Up @@ -312,14 +312,14 @@ class MunimBluetoothBackgroundService : Service() {
for (index in 0 until services.length()) {
val serviceJson = services.getJSONObject(index)
val service = BluetoothGattService(
UUID.fromString(serviceJson.getString("uuid")),
parseBleUuid(serviceJson.getString("uuid")),
BluetoothGattService.SERVICE_TYPE_PRIMARY
)
val characteristics = serviceJson.optJSONArray("characteristics") ?: JSONArray()
for (characteristicIndex in 0 until characteristics.length()) {
val characteristicJson = characteristics.getJSONObject(characteristicIndex)
val characteristic = BluetoothGattCharacteristic(
UUID.fromString(characteristicJson.getString("uuid")),
parseBleUuid(characteristicJson.getString("uuid")),
propertiesFromJson(characteristicJson.optJSONArray("properties")),
characteristicPermissionsFromJson(
characteristicJson.optJSONArray("permissions"),
Expand All @@ -342,7 +342,7 @@ class MunimBluetoothBackgroundService : Service() {
for (descriptorIndex in 0 until descriptors.length()) {
val descriptorJson = descriptors.getJSONObject(descriptorIndex)
val descriptor = BluetoothGattDescriptor(
UUID.fromString(descriptorJson.getString("uuid")),
parseBleUuid(descriptorJson.getString("uuid")),
descriptorPermissionsFromJson(descriptorJson.optJSONArray("permissions"))
)
optionalString(descriptorJson, "value")?.let { value ->
Expand Down Expand Up @@ -581,7 +581,7 @@ class MunimBluetoothBackgroundService : Service() {
val filters = serviceUUIDs.mapNotNull { uuid ->
runCatching {
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString(uuid))
.setServiceUuid(parseBleParcelUuid(uuid))
.build()
}.getOrNull()
}
Expand Down
10 changes: 9 additions & 1 deletion app.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ const ANDROID_BLUETOOTH_PERMISSIONS = {
{ name: 'android.permission.BLUETOOTH', maxSdkVersion: '30' },
{ name: 'android.permission.BLUETOOTH_ADMIN', maxSdkVersion: '30' },
{ name: 'android.permission.ACCESS_FINE_LOCATION', maxSdkVersion: '30' },
{ name: 'android.permission.BLUETOOTH_SCAN' },
// Location is capped at API 30, so on Android 12+ scan results are only
// delivered when the scan permission disavows location (see README).
{
name: 'android.permission.BLUETOOTH_SCAN',
usesPermissionFlags: 'neverForLocation',
},
{ name: 'android.permission.BLUETOOTH_CONNECT' },
],
connect: [
Expand Down Expand Up @@ -48,6 +53,9 @@ function ensureAndroidPermission(manifest, permission) {
if (permission.maxSdkVersion) {
attributes['android:maxSdkVersion'] = permission.maxSdkVersion;
}
if (permission.usesPermissionFlags) {
attributes['android:usesPermissionFlags'] = permission.usesPermissionFlags;
}
permissions.push({ $: attributes });
}
}
Expand Down
Loading
Loading