From cdfcd24049a63f33401bb4c7b2d62caeb8810f51 Mon Sep 17 00:00:00 2001 From: darken Date: Wed, 14 Sep 2022 20:51:39 +0200 Subject: [PATCH] Ensure that BLEScanner only starts when all necessary permissions are granted. (Or rather that scanner is restarted when permissions change). + some refactoring to improve readability --- .../darken/capod/monitor/core/PodMonitor.kt | 208 +++++++++--------- 1 file changed, 109 insertions(+), 99 deletions(-) diff --git a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index 706720c1..316a7df5 100644 --- a/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app-common/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -4,7 +4,6 @@ import android.bluetooth.le.ScanFilter import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanner import eu.darken.capod.common.bluetooth.BluetoothManager2 -import eu.darken.capod.common.bluetooth.ScannerMode import eu.darken.capod.common.coroutine.AppScope import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE @@ -14,6 +13,7 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.replayingShare import eu.darken.capod.main.core.GeneralSettings +import eu.darken.capod.main.core.PermissionTool import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodFactory import eu.darken.capod.pods.core.apple.protocol.ProximityPairing @@ -36,14 +36,103 @@ class PodMonitor @Inject constructor( private val bluetoothManager: BluetoothManager2, private val debugSettings: DebugSettings, private val podDeviceCache: PodDeviceCache, + private val permissionTool: PermissionTool, ) { private val deviceCache = mutableMapOf() private val cacheLock = Mutex() - private suspend fun List.preFilterAndMap( - scannerMode: ScannerMode - ): List = this + val devices: Flow> = combine( + permissionTool.missingPermissions, + bluetoothManager.isBluetoothEnabled + ) { missingPermissions, isBluetoothEnabled -> + log(TAG) { "devices: missingPermissions=$missingPermissions, isBluetoothEnabled=$isBluetoothEnabled" } + // We just want to retrigger if permissions change. + isBluetoothEnabled + } + .flatMapLatest { isReady -> + if (!isReady) { + log(TAG, WARN) { "Bluetooth is not ready" } + flowOf(null) + } else { + createBleScanner() + } + } + .map { newPods -> + val pods = processWithCache(newPods) + + val presorted = sortPodsToInterest(pods.values) + val main = determineMainDevice(presorted) + newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let { + podDeviceCache.saveMainDevice(it.scanResult) + } + + presorted.sortedByDescending { it == main } + } + .retryWhen { cause, attempt -> + log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" } + delay(3000) + true + } + .onStart { emit(emptyList()) } + .replayingShare(appScope) + + val mainDevice: Flow = devices + .map { determineMainDevice(it) } + .replayingShare(appScope) + + private fun createBleScanner() = combine( + generalSettings.scannerMode.flow, + generalSettings.compatibilityMode.flow, + debugSettings.showUnfiltered.flow + ) { scannerMode, compatMode, unfiltered -> + Triple(scannerMode, compatMode, unfiltered) + } + .flatMapLatest { (mode, compat, unfiltered) -> + val filters = when { + unfiltered -> { + log(TAG, WARN) { "Using unfiltered scan mode" } + setOf(getUnfilteredFilter()) + } + else -> ProximityPairing.getBleScanFilter() + } + + bleScanner.scan( + filters = filters, + scannerMode = mode, + compatMode = compat, + ).map { preFilterAndMap(it) } + } + + private suspend fun processWithCache( + newPods: List? + ): Map = cacheLock.withLock { + if (newPods == null) { + log(TAG) { "Null result, Bluetooth is disabled." } + deviceCache.clear() + return emptyMap() + } + + val now = Instant.now() + deviceCache.toList().forEach { (key, value) -> + if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) { + log(TAG, VERBOSE) { "Removing stale device from cache: $value" } + deviceCache.remove(key) + } + } + + val pods = mutableMapOf() + + pods.putAll(deviceCache) + + newPods.map { it.device }.forEach { + deviceCache[it.identifier] = it + pods[it.identifier] = it + } + return pods + } + + private suspend fun preFilterAndMap(rawResults: List): List = rawResults .groupBy { it.address } .values .map { sameAdrDevs -> @@ -56,115 +145,36 @@ class PodMonitor @Inject constructor( } .mapNotNull { podFactory.createPod(it) } - val devices: Flow> = bluetoothManager.isBluetoothEnabled - .flatMapLatest { isBluetoothEnabled -> - if (isBluetoothEnabled) { - log(TAG) { "Bluetooth is enabled" } - combine( - generalSettings.scannerMode.flow, - generalSettings.compatibilityMode.flow, - debugSettings.showUnfiltered.flow - ) { scannerMode, compatMode, unfiltered -> - Triple(scannerMode, compatMode, unfiltered) - }.flatMapLatest { (mode, compat, unfiltered) -> - log(TAG, VERBOSE) { "Starting BLEScanner mode=$mode, compat=$compat, unfiltered=$unfiltered" } - val filters = if (unfiltered) { - setOf(getUnfilteredFilter()) - } else { - ProximityPairing.getBleScanFilter() - } - bleScanner.scan( - filters = filters, - scannerMode = mode, - compatMode = compat, - ).map { it.preFilterAndMap(mode) } - } - } else { - log(TAG, WARN) { "Bluetooth is currently disabled" } - flowOf(null) - } - } - .map { newPods -> - val pods = mutableMapOf() - - cacheLock.withLock { - if (newPods == null) { - log(TAG) { "Null result, Bluetooth is disabled." } - deviceCache.clear() - return@map emptyList() - } - - val now = Instant.now() - deviceCache.toList().forEach { (key, value) -> - if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) { - log(TAG, VERBOSE) { "Removing stale device from cache: $value" } - deviceCache.remove(key) - } - } - - pods.putAll(deviceCache) - - newPods.map { it.device }.forEach { - deviceCache[it.identifier] = it - pods[it.identifier] = it - } - } - - val presorted = pods.values.sortPodsToInterest() - val main = presorted.determineMainDevice() - newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let { - podDeviceCache.saveMainDevice(it.scanResult) - } - - presorted.sortedByDescending { it == main } - } - .onStart { emit(emptyList()) } - .retryWhen { cause, attempt -> - log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" } - delay(3000) - true - } - .replayingShare(appScope) - - val mainDevice: Flow - get() = devices - .map { it.determineMainDevice() } - .replayingShare(appScope) - - private fun Collection.sortPodsToInterest(): List = this.let { devices -> + private fun sortPodsToInterest(pods: Collection): List { val now = Instant.now() - return@let devices.sortedWith( + return pods.sortedWith( compareByDescending { true } .thenBy { val age = Duration.between(it.seenLastAt, now).seconds - if (age < 5) 0L else (age / 3L).toLong() + if (age < 5) 0L else (age / 3L) } .thenByDescending { it.signalQuality } .thenByDescending { (it.seenCounter / 10) } - ) } - private fun List.determineMainDevice(): PodDevice? = this - .sortPodsToInterest() - .let { devices -> - val mainDeviceModel = generalSettings.mainDeviceModel.value + private fun determineMainDevice(pods: List): PodDevice? { + val mainDeviceModel = generalSettings.mainDeviceModel.value - val presorted = devices.sortedByDescending { - it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN - } - - return@let presorted.firstOrNull()?.let { candidate -> - when { - candidate.model == PodDevice.Model.UNKNOWN -> null - mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null - candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null - else -> candidate - } - } + val presorted = sortPodsToInterest(pods).sortedByDescending { + it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN } + return presorted.firstOrNull()?.let { candidate -> + when { + candidate.model == PodDevice.Model.UNKNOWN -> null + mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null + candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null + else -> candidate + } + } + } private fun getUnfilteredFilter(): ScanFilter { return ScanFilter.Builder().build()