@@ -26,19 +26,29 @@ import kotlinx.coroutines.channels.trySendBlocking
2626import kotlinx.coroutines.flow.Flow
2727import kotlinx.coroutines.flow.callbackFlow
2828import kotlinx.coroutines.flow.filter
29+ import kotlin.reflect.KClass
2930import kotlin.uuid.toJavaUuid
3031import kotlin.uuid.toKotlinUuid
3132
33+ internal data class ScanFilters (
34+
35+ /* * [ScanFilter]s applied using Android's native filtering. */
36+ val native : List <ScanFilter >,
37+
38+ /* * [FilterPredicate]s applied via flow [filter][Flow.filter] operator. */
39+ val flow : List <FilterPredicate >,
40+ )
41+
3242internal class BluetoothLeScannerAndroidScanner (
33- private val filters : List <FilterPredicate >,
43+ filters : List <FilterPredicate >,
3444 private val scanSettings : ScanSettings ,
3545 private val preConflate : Boolean ,
3646 logging : Logging ,
3747) : PlatformScanner {
3848
3949 private val logger = Logger (logging, tag = " Kable/Scanner" , identifier = null )
4050
41- private val scanFilters = filters.toNativeScanFilters ()
51+ private val scanFilters = filters.toScanFilters ()
4252
4353 override val advertisements: Flow <PlatformAdvertisement > = callbackFlow {
4454 logger.debug { message = " Initializing scan" }
@@ -87,7 +97,7 @@ internal class BluetoothLeScannerAndroidScanner(
8797 logger.info {
8898 message = logMessage(" Starting" , preConflate, scanFilters)
8999 }
90- scanner.startScan(scanFilters, scanSettings, callback)
100+ scanner.startScan(scanFilters.native , scanSettings, callback)
91101
92102 awaitClose {
93103 logger.info {
@@ -102,76 +112,112 @@ internal class BluetoothLeScannerAndroidScanner(
102112 }
103113 }
104114 }.filter { advertisement ->
105- // Short-circuit (i.e. don't filter) if native scan filters were applied.
106- if (scanFilters.isNotEmpty()) return @filter true
107-
108- // Perform filtering here, since we were not able to use native scan filters.
109- filters.matches(
110- services = advertisement.uuids ,
111- name = advertisement.name ,
112- address = advertisement.address ,
113- manufacturerData = advertisement.manufacturerData ,
114- serviceData = advertisement.serviceData?.mapKeys { (key) -> key.uuid.toKotlinUuid() },
115- )
115+ if (scanFilters.flow.isEmpty()) {
116+ true
117+ } else {
118+ scanFilters.flow.matches(
119+ services = advertisement.uuids,
120+ name = advertisement.name ,
121+ address = advertisement.address ,
122+ manufacturerData = advertisement.manufacturerData ,
123+ serviceData = advertisement.serviceData?.mapKeys { (key) -> key.uuid.toKotlinUuid() } ,
124+ )
125+ }
116126 }
117127}
118128
119129private fun logMessage (
120130 prefix : String ,
121131 preConflate : Boolean ,
122- scanFilters : List < ScanFilter > ,
132+ scanFilters : ScanFilters ,
123133) = buildString {
124134 append(prefix)
125135 append(' ' )
126136 append(" scan " )
127137 if (preConflate) {
128138 append(" pre-conflated " )
129139 }
130- if (scanFilters.isEmpty()) {
140+ if (scanFilters.native.isEmpty() && scanFilters.flow. isEmpty()) {
131141 append(" without filters" )
132142 } else {
133- append(" with ${scanFilters.size} filter(s)" )
143+ append(" with ${scanFilters.native.count()} native and ${scanFilters.flow.count()} flow filter(s)" )
134144 }
135145}
136146
137- private fun List<FilterPredicate>.toNativeScanFilters (): List < ScanFilter > =
147+ internal fun List<FilterPredicate>.toScanFilters (): ScanFilters =
138148 if (all(FilterPredicate ::supportsNativeScanFiltering)) {
139- map(FilterPredicate ::toNativeScanFilter)
149+ ScanFilters (
150+ native = map(FilterPredicate ::toNativeScanFilter),
151+ flow = emptyList(),
152+ )
153+ } else if (count() == 1 ) {
154+ val nativeFilters = mutableMapOf<KClass <* >, Filter > ()
155+ val flowFilters = mutableListOf<Filter >()
156+ single().filters.forEach { filter ->
157+ if (filter.canFilterNatively && filter::class !in nativeFilters) {
158+ nativeFilters[filter::class ] = filter
159+ } else {
160+ flowFilters + = filter
161+ }
162+ }
163+ ScanFilters (
164+ native = listOf (nativeFilters.values.toList().toNativeScanFilter()),
165+ flow = listOf (FilterPredicate (flowFilters)),
166+ )
140167 } else {
141- emptyList()
168+ ScanFilters (
169+ native = emptyList(),
170+ flow = this ,
171+ )
142172 }
143173
144- private fun FilterPredicate.toNativeScanFilter (): ScanFilter =
174+ // Android's `ScanFilter` does not support name prefix filtering, and only allows at most one of each filter type.
175+ private val FilterPredicate .supportsNativeScanFiltering: Boolean
176+ get() {
177+ var service = 0
178+ var nameExact = 0
179+ var address = 0
180+ var manufacturerData = 0
181+ var serviceData = 0
182+ filters.forEach { filter ->
183+ when (filter) {
184+ is Service -> if (++ service > 1 ) return false
185+ is Name .Exact -> if (++ nameExact > 1 ) return false
186+ is Name .Prefix -> return false
187+ is Address -> if (++ address > 1 ) return false
188+ is ManufacturerData -> if (++ manufacturerData > 1 ) return false
189+ is ServiceData -> if (++ serviceData > 1 ) return false
190+ }
191+ }
192+ return true
193+ }
194+
195+ private val Filter .canFilterNatively: Boolean
196+ get() = when (this ) {
197+ is Service -> true
198+ is Name .Exact -> true
199+ is Address -> true
200+ is ManufacturerData -> true
201+ is ServiceData -> true
202+ else -> false
203+ }
204+
205+ private fun FilterPredicate.toNativeScanFilter (): ScanFilter = filters.toNativeScanFilter()
206+
207+ private fun List<Filter>.toNativeScanFilter (): ScanFilter =
145208 ScanFilter .Builder ().apply {
146- filters.map { filter ->
209+ onEach { filter ->
147210 when (filter) {
211+ is Service -> setServiceUuid(ParcelUuid (filter.uuid.toJavaUuid()))
148212 is Name .Exact -> setDeviceName(filter.exact)
149213 is Address -> setDeviceAddress(filter.address)
150214 is ManufacturerData -> setManufacturerData(filter.id, filterDataCompat(filter.data), filter.dataMask)
151215 is ServiceData -> setServiceData(ParcelUuid (filter.uuid.toJavaUuid()), filterDataCompat(filter.data), filter.dataMask)
152- is Service -> setServiceUuid(ParcelUuid (filter.uuid.toJavaUuid()))
153216 else -> throw AssertionError (" Unsupported filter element" )
154217 }
155218 }
156219 }.build()
157220
158- // Scan filter does not support name prefix filtering, and only allows at most one each of the
159- // following: service uuid, manufacturer data, service data.
160- private fun FilterPredicate.supportsNativeScanFiltering (): Boolean =
161- ! containsNamePrefix() && serviceCount() <= 1 && manufacturerDataCount() <= 1 && serviceDataCount() <= 1
162-
163- private fun FilterPredicate.containsNamePrefix (): Boolean =
164- filters.any { it is Name .Prefix }
165-
166- private fun FilterPredicate.serviceCount (): Int =
167- filters.count { it is Service }
168-
169- private fun FilterPredicate.manufacturerDataCount (): Int =
170- filters.count { it is ManufacturerData }
171-
172- private fun FilterPredicate.serviceDataCount (): Int =
173- filters.count { it is ServiceData }
174-
175221// Android doesn't properly check for nullness of manufacturer or service data until Android 16.
176222// See https://github.com/JuulLabs/kable/issues/854 for more details.
177223private fun filterDataCompat (data : ByteArray? ): ByteArray? =
0 commit comments