Handle Bluetooth being disabled

This commit is contained in:
darken
2022-01-05 12:13:26 +01:00
parent 6ad93a2417
commit 462c464de2
4 changed files with 144 additions and 104 deletions
@@ -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<ScanFilter> = ProximityPairing.getBleScanFilter(),
mode: Int = ScanSettings.SCAN_MODE_LOW_POWER,
delay: Long = 100,
mode: Int = ScanSettings.SCAN_MODE_BALANCED,
): Flow<List<ScanResult>> = 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" }
@@ -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<Boolean> = callbackFlow {
send(manager.adapter?.isEnabled ?: false)
@@ -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<List<PodDevice>> = 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
@@ -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<Unit> {
// 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<Unit> {
// Cancel worker, ui scans manually
workerScope.coroutineContext.cancelChildren()
}
MonitorMode.ALWAYS -> emptyFlow()
MonitorMode.AUTOMATIC -> flow<Unit> {
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<Unit> {
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 {