Skip to content

Commit

Permalink
Provide ability to pre-conflate scanned advertisements (#684)
Browse files Browse the repository at this point in the history
  • Loading branch information
djweber authored Jun 3, 2024
1 parent 1dcd05a commit ad45bd3
Show file tree
Hide file tree
Showing 3 changed files with 49 additions and 23 deletions.
2 changes: 2 additions & 0 deletions core/api/android/core.api
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,11 @@ public abstract interface class com/juul/kable/Scanner {
public final class com/juul/kable/ScannerBuilder {
public fun <init> ()V
public final fun getFilters ()Ljava/util/List;
public final fun getPreConflate ()Z
public final fun getScanSettings ()Landroid/bluetooth/le/ScanSettings;
public final fun logging (Lkotlin/jvm/functions/Function1;)V
public final fun setFilters (Ljava/util/List;)V
public final fun setPreConflate (Z)V
public final fun setScanSettings (Landroid/bluetooth/le/ScanSettings;)V
}

Expand Down
53 changes: 30 additions & 23 deletions core/src/androidMain/kotlin/BluetoothLeScannerAndroidScanner.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.juul.kable

import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter.STATE_ON
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanFilter
Expand All @@ -25,6 +24,7 @@ import kotlinx.coroutines.flow.filter
internal class BluetoothLeScannerAndroidScanner(
private val filters: List<Filter>,
private val scanSettings: ScanSettings,
private val preConflate: Boolean,
logging: Logging,
) : AndroidScanner {

Expand All @@ -35,23 +35,24 @@ internal class BluetoothLeScannerAndroidScanner(
override val advertisements: Flow<AndroidAdvertisement> = callbackFlow {
val scanner = getBluetoothAdapter().bluetoothLeScanner ?: throw BluetoothDisabledException()

fun sendResult(scanResult: ScanResult) {
val advertisement = ScanResultAndroidAdvertisement(scanResult)
when {
preConflate -> trySend(advertisement)
else -> trySendBlocking(advertisement)
}.onFailure {
logger.warn { message = "Unable to deliver scan result due to failure in flow or premature closing." }
}
}

val callback = object : ScanCallback() {

override fun onScanResult(callbackType: Int, result: ScanResult) {
trySendBlocking(ScanResultAndroidAdvertisement(result))
.onFailure {
logger.warn { message = "Unable to deliver scan result due to failure in flow or premature closing." }
}
sendResult(result)
}

@SuppressLint("NewApi") // `forEach` incorrectly showing as minimum API 24 despite the Kotlin stdlib version being used.
override fun onBatchScanResults(results: MutableList<ScanResult>) {
runCatching {
results.forEach {
trySendBlocking(ScanResultAndroidAdvertisement(it)).getOrThrow()
}
}.onFailure {
logger.warn { message = "Unable to deliver batch scan results due to failure in flow or premature closing." }
}
results.forEach(::sendResult)
}

override fun onScanFailed(errorCode: Int) {
Expand All @@ -73,22 +74,14 @@ internal class BluetoothLeScannerAndroidScanner(
}

logger.info {
message = if (scanFilters.isEmpty()) {
"Starting scan without filters"
} else {
"Starting scan with ${scanFilters.size} filter(s)"
}
message = logMessage("Starting", preConflate, scanFilters)
}
checkBluetoothAdapterState(STATE_ON)
scanner.startScan(scanFilters, scanSettings, callback)

awaitClose {
logger.info {
message = if (scanFilters.isEmpty()) {
"Stopping scan without filters"
} else {
"Stopping scan with ${scanFilters.size} filter(s)"
}
message = logMessage("Stopping", preConflate, scanFilters)
}
// Can't check BLE state here, only Bluetooth, but should assume `IllegalStateException` means BLE has been disabled.
try {
Expand All @@ -105,3 +98,17 @@ internal class BluetoothLeScannerAndroidScanner(
namePrefixFilters.any { filter -> filter.matches(advertisement.name) }
}
}

private fun logMessage(prefix: String, preConflate: Boolean, scanFilters: List<ScanFilter>) = buildString {
append(prefix)
append(' ')
append("scan ")
if (preConflate) {
append("pre-conflated ")
}
if (scanFilters.isEmpty()) {
append("without filters")
} else {
append("with ${scanFilters.size} filter(s)")
}
}
17 changes: 17 additions & 0 deletions core/src/androidMain/kotlin/ScannerBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.juul.kable
import android.bluetooth.le.ScanSettings
import com.juul.kable.logs.Logging
import com.juul.kable.logs.LoggingBuilder
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.runBlocking

public actual class ScannerBuilder {

Expand All @@ -19,6 +22,19 @@ public actual class ScannerBuilder {

private var logging: Logging = Logging()

/**
* Configures [Scanner] to pre-conflate the [advertisements][Scanner.advertisements] flow.
*
* Roughly equivalent to applying the [conflate][Flow.conflate] flow operator on the
* [advertisements][Scanner.advertisements] property (but without [runBlocking] overhead).
*
* May prevent ANRs on some Android phones (observed on specific Samsung models) that have
* delicate binder threads.
*
* See https://github.com/JuulLabs/kable/issues/654 for more details.
*/
public var preConflate: Boolean = false

public actual fun logging(init: LoggingBuilder) {
logging = Logging().apply(init)
}
Expand All @@ -28,5 +44,6 @@ public actual class ScannerBuilder {
filters = filters.orEmpty(),
scanSettings = scanSettings,
logging = logging,
preConflate = preConflate,
)
}

0 comments on commit ad45bd3

Please sign in to comment.