diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index e485edcb..71397a87 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -1,6 +1,5 @@ package eu.darken.capod.common.bluetooth -import android.bluetooth.BluetoothManager import android.bluetooth.le.ScanCallback import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult @@ -20,16 +19,15 @@ import javax.inject.Singleton @Singleton class BleScanner @Inject constructor( @ApplicationContext private val context: Context, - private val bluetoothManager: BluetoothManager, + private val bluetoothManager: BluetoothManager2, ) { - // TODO check Bluetooth available - // TODO check Bluetooth enabled + fun scan( filter: Set = ProximityPairing.getBleScanFilter(), - mode: Int = ScanSettings.SCAN_MODE_LOW_POWER, - delay: Long = 100, + mode: Int = ScanSettings.SCAN_MODE_BALANCED, ): Flow> = callbackFlow { - val scanner = bluetoothManager.adapter.bluetoothLeScanner + val adapter = bluetoothManager.adapter + val scanner = adapter.bluetoothLeScanner val callback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { @@ -49,11 +47,21 @@ class BleScanner @Inject constructor( val settings = ScanSettings.Builder().apply { setScanMode(mode) - setReportDelay(delay) + if (adapter.isOffloadedScanBatchingSupported) { + setReportDelay(100) + } else { + log(TAG, WARN) { "isOffloadedScanBatchingSupported=false" } + } }.build() - scanner.startScan(filter.toList(), settings, callback) - log(TAG, VERBOSE) { "BleScanner started (filter=$filter, settings=$settings)" } + if (adapter.isOffloadedFilteringSupported) { + scanner.startScan(filter.toList(), settings, callback) + log(TAG, VERBOSE) { "BleScanner started (filter=$filter, settings=$settings)" } + } else { + log(TAG, WARN) { "isOffloadedFilteringSupported=false" } + scanner.startScan(callback) + log(TAG, VERBOSE) { "BleScanner started" } + } awaitClose { log(TAG, INFO) { "BleScanner stopped" } diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt index 613e9899..8b2ab14d 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BluetoothManager2.kt @@ -4,6 +4,7 @@ import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile +import android.bluetooth.le.BluetoothLeScanner import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -34,6 +35,13 @@ class BluetoothManager2 @Inject constructor( private val dispatcherProvider: DispatcherProvider, ) { + val adapter: BluetoothAdapter + get() = manager.adapter + + val scanner: BluetoothLeScanner + get() = adapter.bluetoothLeScanner + ?: throw IllegalStateException("Bluetooth is disabled or permissiong missing") + val isBluetoothEnabled: Flow = callbackFlow { send(manager.adapter?.isEnabled ?: false) diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index 569a7c73..617620e1 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -2,17 +2,16 @@ package eu.darken.capod.monitor.core import android.bluetooth.le.ScanSettings import eu.darken.capod.common.bluetooth.BleScanner +import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.ScannerMode import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodFactory -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.* import javax.inject.Inject import javax.inject.Singleton @@ -21,17 +20,26 @@ class PodMonitor @Inject constructor( private val bleScanner: BleScanner, private val podFactory: PodFactory, private val generalSettings: GeneralSettings, + private val bluetoothManager: BluetoothManager2 ) { val devices: Flow> = generalSettings.scannerMode.flow .flatMapLatest { - bleScanner.scan( - mode = when (it) { - ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER - ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED - ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY + val mode = when (it) { + ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER + ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED + ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY + } + bluetoothManager.isBluetoothEnabled.flatMapLatest { isBluetoothEnabled -> + if (isBluetoothEnabled) { + bleScanner.scan( + mode = mode + ) + } else { + log(TAG, WARN) { "Bluetooth is current disabled" } + emptyFlow() } - ) + } } .map { result -> // For each address we only want the newest result, upstream may batch data diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt index 7a71b88e..769fad89 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt @@ -17,7 +17,7 @@ import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.setupCommonEventHandlers import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.common.permissions.Permission -import eu.darken.capod.common.permissions.isGrantedOrNotRequired +import eu.darken.capod.common.permissions.isRequired import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.monitor.core.MonitorComponent @@ -27,6 +27,7 @@ import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import kotlinx.coroutines.cancel import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -58,105 +59,120 @@ class MonitorWorker @AssistedInject constructor( entryPoint.bluetoothManager2() } private var finishedWithError = false + private var monitorFlowError: Throwable? = null + private var podFlowError: Throwable? = null init { log(TAG, VERBOSE) { "init(): workerId=$id" } } - override suspend fun doWork(): Result { - try { - val start = System.currentTimeMillis() - log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" } + override suspend fun doWork(): Result = try { + val start = System.currentTimeMillis() + log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" } - val missingPermissions = Permission.values().filter { !it.isGrantedOrNotRequired(context) } - if (missingPermissions.isNotEmpty()) { - log(TAG, WARN) { "Aborting, missing permissions: $missingPermissions" } - return Result.success() - } + coroutineScope { + doDoWork() + } + monitorFlowError?.let { throw it } + podFlowError?.let { throw it } - generalSettings.monitorMode.flow - .flatMapLatest { monitorMode -> - bluetoothManager2 - .isBluetoothEnabled - .flatMapLatest { bluetoothManager2.connectedDevices() } - .map { devices -> - devices.filter { device -> - ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> - device.hasFeature(feature) - } + val duration = System.currentTimeMillis() - start + + log(TAG, VERBOSE) { "Execution finished after ${duration}ms, $inputData" } + + Result.success(inputData) + } catch (e: Throwable) { + log(TAG, ERROR) { "Execution failed:\n${e.asLog()}" } + Bugs.report(e) + finishedWithError = true + Result.failure(inputData) + } finally { + this.workerScope.cancel("Worker finished (withError?=$finishedWithError).") + } + + private suspend fun doDoWork() { + val missingPermissions = Permission.values().filter { it.isRequired(context) } + if (missingPermissions.isNotEmpty()) { + log(TAG, WARN) { "Aborting, missing permissions: $missingPermissions" } + return + } + + generalSettings.monitorMode.flow + .flatMapLatest { monitorMode -> + bluetoothManager2 + .isBluetoothEnabled + .flatMapLatest { bluetoothManager2.connectedDevices() } + .map { devices -> + devices.filter { device -> + ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> + device.hasFeature(feature) } } - .map { knownDevices -> monitorMode to knownDevices } - } - .setupCommonEventHandlers(TAG) { "MonitorMode" } - .flatMapLatest { (monitorMode, devices) -> - log(TAG) { "Monitor mode: $monitorMode" } - when (monitorMode) { - MonitorMode.MANUAL -> flow { - // Cancel worker, ui scans manually + } + .map { knownDevices -> monitorMode to knownDevices } + } + .setupCommonEventHandlers(TAG) { "MonitorMode" } + .flatMapLatest { (monitorMode, devices) -> + log(TAG) { "Monitor mode: $monitorMode" } + when (monitorMode) { + MonitorMode.MANUAL -> flow { + // Cancel worker, ui scans manually + workerScope.coroutineContext.cancelChildren() + } + MonitorMode.ALWAYS -> emptyFlow() + MonitorMode.AUTOMATIC -> flow { + if (devices.isNotEmpty()) { + log(TAG) { "Pods are connected, aborting any timeout." } + } else { + log(TAG) { "No Pods are connected, canceling worker soon." } + delay(60 * 1000) + log(TAG) { "Canceling worker now, still no Pods connected." } + workerScope.coroutineContext.cancelChildren() } - MonitorMode.ALWAYS -> emptyFlow() - MonitorMode.AUTOMATIC -> flow { - if (devices.isNotEmpty()) { - log(TAG) { "Pods are connected, aborting any timeout." } - } else { - log(TAG) { "No Pods are connected, canceling worker soon." } - delay(60 * 1000) - log(TAG) { "Canceling worker now, still no Pods connected." } + } + } + } + .catch { + monitorFlowError = it + log(TAG, WARN) { "MonitorMode Flow failed:\n${it.asLog()}" } + } + .launchIn(workerScope) - workerScope.coroutineContext.cancelChildren() - } + val monitorJob = podMonitor.mainDevice + .setupCommonEventHandlers(TAG) { "PodMonitor" } + .onStart { + setForeground(monitorNotifications.getForegroundInfo(null)) + } + .onEach { currentDevice -> + notificationManager.notify( + MonitorNotifications.NOTIFICATION_ID, + monitorNotifications.getNotification(currentDevice) + ) + } + .withPrevious() + .onEach { (previous, current) -> + if (previous is HasEarDetection && current is HasEarDetection) { + log(TAG) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" } + log(TAG) { "previous-id=${previous.identifier}, current-id=${current.identifier}" } + if (previous.identifier == current.identifier && previous.isBeingWorn != current.isBeingWorn) { + if (generalSettings.autoPlay.value && !mediaControl.isPlaying) { + mediaControl.sendPlay() + } else if (generalSettings.autoPause.value && mediaControl.isPlaying) { + mediaControl.sendPause() } } } - .launchIn(workerScope) + } + .catch { + podFlowError = it + log(TAG, WARN) { "Pod Flow failed:\n${it.asLog()}" } + } + .launchIn(workerScope) - val monitorJob = podMonitor.mainDevice - .setupCommonEventHandlers(TAG) { "PodMonitor" } - .onStart { - setForeground(monitorNotifications.getForegroundInfo(null)) - } - .onEach { currentDevice -> - notificationManager.notify( - MonitorNotifications.NOTIFICATION_ID, - monitorNotifications.getNotification(currentDevice) - ) - } - .withPrevious() - .onEach { (previous, current) -> - if (previous is HasEarDetection && current is HasEarDetection) { - log(TAG) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" } - log(TAG) { "previous-id=${previous.identifier}, current-id=${current.identifier}" } - if (previous.identifier == current.identifier && previous.isBeingWorn != current.isBeingWorn) { - if (generalSettings.autoPlay.value && !mediaControl.isPlaying) { - mediaControl.sendPlay() - } else if (generalSettings.autoPause.value && mediaControl.isPlaying) { - mediaControl.sendPause() - } - } - } - } - .launchIn(workerScope) - - log(TAG, VERBOSE) { "Monitor job is active" } - monitorJob.join() - log(TAG, VERBOSE) { "Monitor job quit" } - - val duration = System.currentTimeMillis() - start - - log(TAG, VERBOSE) { "Execution finished after ${duration}ms, $inputData" } - - return Result.success(inputData) - } catch (e: Throwable) { - log(TAG, ERROR) { "Execution failed:\n${e.asLog()}" } - Bugs.report(e) - finishedWithError = true - // TODO update result? - return Result.failure(inputData) - } finally { - this.workerScope.cancel("Worker finished (withError?=$finishedWithError).") - } + log(TAG, VERBOSE) { "Monitor job is active" } + monitorJob.join() + log(TAG, VERBOSE) { "Monitor job quit" } } companion object {