feat(ble): update kable-core to v0.44.0 + surface missing-permission scan failures (#6165)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: James Rich <james.a.rich@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
renovate[bot]
2026-07-09 03:10:50 +00:00
committed by GitHub
parent 49e454a4ba
commit bbc85dcf76
8 changed files with 43 additions and 8 deletions

View File

@@ -123,6 +123,7 @@ bluetooth_feature_config_description
bluetooth_feature_discovery
bluetooth_feature_discovery_description
bluetooth_permission
bluetooth_scan_missing_permission
bluetooth_scan_start_failed
bold_heading
bonding_failed_permissions

View File

@@ -22,6 +22,10 @@ enum class BleScanStartFailureReason(val androidCode: String, val description: S
androidCode = "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED(2)",
description = "Android could not register the app for BLE scanning",
),
MissingScanPermission(
androidCode = "MISSING_SCAN_PERMISSION",
description = "A runtime permission required for BLE scanning is not granted",
),
}
/** A discovery scan-start failure. No advertisements can be delivered until a future scan starts successfully. */

View File

@@ -98,15 +98,20 @@ private fun Throwable.asBleScanStartExceptionOrNull(): BleScanStartException? {
var current: Throwable? = this
var depth = 0
while (current != null && depth < MAX_SCAN_START_FAILURE_CAUSE_DEPTH) {
if (current.isApplicationRegistrationFailure()) {
return BleScanStartException(BleScanStartFailureReason.ApplicationRegistrationFailed, this)
}
val reason = current.scanStartFailureReasonOrNull()
if (reason != null) return BleScanStartException(reason, this)
current = current.cause
depth++
}
return null
}
private fun Throwable.scanStartFailureReasonOrNull(): BleScanStartFailureReason? = when {
isApplicationRegistrationFailure() -> BleScanStartFailureReason.ApplicationRegistrationFailed
isMissingScanPermission() -> BleScanStartFailureReason.MissingScanPermission
else -> null
}
// Kable exposes Android scan-start registration failure as an IllegalStateException message,
// so keep this matcher narrow and local to the scanner adapter.
private fun Throwable.isApplicationRegistrationFailure(): Boolean = this is IllegalStateException &&
@@ -114,3 +119,11 @@ private fun Throwable.isApplicationRegistrationFailure(): Boolean = this is Ille
failureMessage.contains("app cannot be registered", ignoreCase = true) ||
failureMessage.contains("SCAN_FAILED_APPLICATION_REGISTRATION_FAILED", ignoreCase = true)
} == true
// Kable's scan-permission check throws a plain IllegalStateException "Missing required <permission> for scanning"
// (ACCESS_COARSE/FINE_LOCATION on Android <12, BLUETOOTH_SCAN on 12+). Match the stable message anchors.
private fun Throwable.isMissingScanPermission(): Boolean = this is IllegalStateException &&
message?.let { failureMessage ->
failureMessage.contains("Missing required", ignoreCase = true) &&
failureMessage.contains("for scanning", ignoreCase = true)
} == true

View File

@@ -17,7 +17,7 @@
package org.meshtastic.core.ble
import com.juul.kable.Advertisement
import com.juul.kable.ExperimentalApi
import com.juul.kable.ExperimentalKableApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -54,7 +54,7 @@ class MeshtasticBleDevice(
override val rssi: Int? = advertisement?.rssi
@OptIn(ExperimentalApi::class)
@OptIn(ExperimentalKableApi::class)
override suspend fun readRssi(): Int {
val active = ActiveBleConnection.active
return if (active != null && active.address == address) {

View File

@@ -145,6 +145,16 @@ class KableBleConnectionTest {
assertEquals(BleScanStartFailureReason.ApplicationRegistrationFailed, failure.reason)
}
@Test
fun `scan wraps missing scan permission failure`() = runTest {
val message = "Missing required android.permission.ACCESS_COARSE_LOCATION for scanning"
val scanner = TestKableBleScanner(scanResults = flow { throw IllegalStateException(message) })
val failure = assertFailsWith<BleScanStartException> { scanner.scan(timeout = 1.seconds).toList() }
assertEquals(BleScanStartFailureReason.MissingScanPermission, failure.reason)
}
@Test
fun `scan preserves unrelated illegal state failure`() = runTest {
val scanner =

View File

@@ -141,6 +141,7 @@
<string name="bluetooth_feature_discovery">Discovery</string>
<string name="bluetooth_feature_discovery_description">Find and identify Meshtastic devices near you.</string>
<string name="bluetooth_permission">Bluetooth</string>
<string name="bluetooth_scan_missing_permission">Bluetooth scan needs permission. Grant the Nearby devices (or Location) permission to find devices.</string>
<string name="bluetooth_scan_start_failed">Bluetooth scan couldn't start. Try again, or toggle Bluetooth if the problem continues.</string>
<string name="bold_heading">Bold Heading</string>
<string name="bonding_failed_permissions">Pairing failed. Grant nearby device permissions and try again.</string>

View File

@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.ble.BleDevice
import org.meshtastic.core.ble.BleScanStartException
import org.meshtastic.core.ble.BleScanStartFailureReason
import org.meshtastic.core.ble.BleScanner
import org.meshtastic.core.ble.MeshtasticBleConstants
import org.meshtastic.core.common.util.safeCatchingAll
@@ -58,6 +59,7 @@ import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bluetooth_scan_missing_permission
import org.meshtastic.core.resources.bluetooth_scan_start_failed
import org.meshtastic.core.resources.getStringSuspend
import org.meshtastic.core.ui.viewmodel.safeLaunch
@@ -396,9 +398,13 @@ open class ScannerViewModel(
Logger.w(exception) {
"BLE scan could not start: ${exception.reason.androidCode} (${exception.reason.description})"
}
val messageRes =
when (exception.reason) {
BleScanStartFailureReason.MissingScanPermission -> Res.string.bluetooth_scan_missing_permission
BleScanStartFailureReason.ApplicationRegistrationFailed -> Res.string.bluetooth_scan_start_failed
}
val errorMessage =
safeCatchingAll { getStringSuspend(Res.string.bluetooth_scan_start_failed) }
.getOrDefault(BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK)
safeCatchingAll { getStringSuspend(messageRes) }.getOrDefault(BLE_SCAN_START_FAILURE_MESSAGE_FALLBACK)
serviceRepository.setErrorMessage(text = errorMessage, severity = Severity.Warn)
}

View File

@@ -91,7 +91,7 @@ uri-kmp = "0.0.21"
osmdroid-android = "6.1.20"
spotless = "8.8.0"
vico = "3.3.0-next.1"
kable = "0.43.1"
kable = "0.44.0"
mqttastic = "0.4.0"
jmdns = "3.6.3"
qrcode-kotlin = "4.5.0"