mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
Handle Bluetooth being disabled
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user