mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 11:16:12 -04:00
Implement periodic checks for wear
This commit is contained in:
@@ -3,6 +3,7 @@ package eu.darken.capod.common.bluetooth
|
|||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import eu.darken.capod.common.SystemClockWrap
|
import eu.darken.capod.common.SystemClockWrap
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ class FakeBleData @Inject constructor(
|
|||||||
val fakeDevices = mutableListOf<BleScanResult>()
|
val fakeDevices = mutableListOf<BleScanResult>()
|
||||||
// AirPods Gen1
|
// AirPods Gen1
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "78:73:AF:B4:85:22",
|
address = "78:73:AF:B4:85:22",
|
||||||
rssi = Random.nextInt(100) * -1,
|
rssi = Random.nextInt(100) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
@@ -32,6 +34,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// AirPods Gen2
|
// AirPods Gen2
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "78:73:FF:B4:85:5E",
|
address = "78:73:FF:B4:85:5E",
|
||||||
rssi = Random.nextInt(100) * -1,
|
rssi = Random.nextInt(100) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
@@ -44,6 +47,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// AirPods Gen3
|
// AirPods Gen3
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "4E:9E:D1:49:D2:6D",
|
address = "4E:9E:D1:49:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
||||||
@@ -55,6 +59,7 @@ class FakeBleData @Inject constructor(
|
|||||||
}
|
}
|
||||||
// AirPods Max
|
// AirPods Max
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "7E:E5:C7:65:D2:B5",
|
address = "7E:E5:C7:65:D2:B5",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
||||||
@@ -66,6 +71,7 @@ class FakeBleData @Inject constructor(
|
|||||||
}
|
}
|
||||||
// BeatsFlex
|
// BeatsFlex
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "5E:9E:D1:49:D2:6D",
|
address = "5E:9E:D1:49:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
@@ -78,6 +84,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// Tws i99999
|
// Tws i99999
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "5E:9E:D1:29:D2:6D",
|
address = "5E:9E:D1:29:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
@@ -90,6 +97,7 @@ class FakeBleData @Inject constructor(
|
|||||||
|
|
||||||
// Unknown Device
|
// Unknown Device
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
|
receivedAt = Instant.now(),
|
||||||
address = "6E:9E:D1:49:D2:6D",
|
address = "6E:9E:D1:49:D2:6D",
|
||||||
rssi = Random.nextInt(15, 75) * -1,
|
rssi = Random.nextInt(15, 75) * -1,
|
||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
||||||
|
|||||||
@@ -18,7 +18,4 @@ enum class MonitorMode(
|
|||||||
@Json(name = "monitor.mode.always") ALWAYS(
|
@Json(name = "monitor.mode.always") ALWAYS(
|
||||||
R.string.settings_monitor_mode_always_label
|
R.string.settings_monitor_mode_always_label
|
||||||
),
|
),
|
||||||
@Json(name = "monitor.mode.periodically") PERIODICALLY(
|
|
||||||
R.string.settings_monitor_mode_periodically_label
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
@@ -46,11 +46,19 @@ private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
|||||||
|
|
||||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||||
val duration = Duration.between(seenLastAt, now)
|
val duration = Duration.between(seenLastAt, now)
|
||||||
return lastSeenFormatter.format(
|
return if (duration > Duration.ofMinutes(1)) {
|
||||||
duration.seconds.toDouble(),
|
lastSeenFormatter.format(
|
||||||
RelativeDateTimeFormatter.Direction.LAST,
|
duration.toMinutes().toDouble(),
|
||||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
RelativeDateTimeFormatter.Direction.LAST,
|
||||||
)
|
RelativeDateTimeFormatter.RelativeUnit.MINUTES
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
lastSeenFormatter.format(
|
||||||
|
duration.seconds.toDouble(),
|
||||||
|
RelativeDateTimeFormatter.Direction.LAST,
|
||||||
|
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun PodDevice.firstSeenFormatted(now: Instant): String {
|
fun PodDevice.firstSeenFormatted(now: Instant): String {
|
||||||
|
|||||||
@@ -44,7 +44,6 @@
|
|||||||
<string name="settings_monitor_mode_manual_label">When app is open</string>
|
<string name="settings_monitor_mode_manual_label">When app is open</string>
|
||||||
<string name="settings_monitor_mode_automatic_label">When device is connected</string>
|
<string name="settings_monitor_mode_automatic_label">When device is connected</string>
|
||||||
<string name="settings_monitor_mode_always_label">Always</string>
|
<string name="settings_monitor_mode_always_label">Always</string>
|
||||||
<string name="settings_monitor_mode_periodically_label">Periodically</string>
|
|
||||||
<string name="settings_monitor_mode_label">Monitor mode</string>
|
<string name="settings_monitor_mode_label">Monitor mode</string>
|
||||||
<string name="settings_monitor_mode_description">Under which circumstances this app monitors Bluetooth data.</string>
|
<string name="settings_monitor_mode_description">Under which circumstances this app monitors Bluetooth data.</string>
|
||||||
<string name="settings_scanner_mode_label">Scanner mode</string>
|
<string name="settings_scanner_mode_label">Scanner mode</string>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="eu.darken.capod">
|
package="eu.darken.capod">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
@@ -54,6 +55,10 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
tools:node="remove" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -2,16 +2,19 @@ package eu.darken.capod
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.hilt.work.HiltWorkerFactory
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
import androidx.work.Configuration
|
import androidx.work.*
|
||||||
import com.getkeepsafe.relinker.ReLinker
|
import com.getkeepsafe.relinker.ReLinker
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
||||||
import eu.darken.capod.common.debug.logging.*
|
import eu.darken.capod.common.debug.logging.*
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.MonitorMode
|
import eu.darken.capod.wear.core.MonitorWorker
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.time.Duration
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
@@ -20,6 +23,7 @@ open class App : Application(), Configuration.Provider {
|
|||||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||||
@Inject lateinit var autoReporting: AutoReporting
|
@Inject lateinit var autoReporting: AutoReporting
|
||||||
@Inject lateinit var generalSettings: GeneralSettings
|
@Inject lateinit var generalSettings: GeneralSettings
|
||||||
|
@Inject lateinit var workManager: WorkManager
|
||||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -31,15 +35,37 @@ open class App : Application(), Configuration.Provider {
|
|||||||
.loadLibrary(this, "bugsnag-plugin-android-anr")
|
.loadLibrary(this, "bugsnag-plugin-android-anr")
|
||||||
|
|
||||||
generalSettings.apply {
|
generalSettings.apply {
|
||||||
monitorMode.value = MonitorMode.PERIODICALLY
|
|
||||||
scannerMode.value = ScannerMode.BALANCED
|
scannerMode.value = ScannerMode.BALANCED
|
||||||
}
|
}
|
||||||
|
|
||||||
autoReporting.setup()
|
autoReporting.setup()
|
||||||
|
|
||||||
|
setupWorker()
|
||||||
|
|
||||||
log(TAG) { "onCreate() done! ${Exception().asLog()}" }
|
log(TAG) { "onCreate() done! ${Exception().asLog()}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setupWorker() {
|
||||||
|
log(TAG) { "setupWorker()" }
|
||||||
|
val workRequest = PeriodicWorkRequestBuilder<MonitorWorker>(
|
||||||
|
Duration.ofMinutes(15),
|
||||||
|
Duration.ofMinutes(5)
|
||||||
|
).apply {
|
||||||
|
setInputData(Data.Builder().build())
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
log(TAG, VERBOSE) { "Worker request: $workRequest" }
|
||||||
|
|
||||||
|
val operation = workManager.enqueueUniquePeriodicWork(
|
||||||
|
"${BuildConfigWrap.APPLICATION_ID}.monitor.worker",
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP,
|
||||||
|
workRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
operation.result.get()
|
||||||
|
log(TAG) { "Monitor start request send." }
|
||||||
|
}
|
||||||
|
|
||||||
override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
|
override fun getWorkManagerConfiguration(): Configuration = Configuration.Builder()
|
||||||
.setMinimumLoggingLevel(android.util.Log.VERBOSE)
|
.setMinimumLoggingLevel(android.util.Log.VERBOSE)
|
||||||
.setWorkerFactory(workerFactory)
|
.setWorkerFactory(workerFactory)
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package eu.darken.capod.wear.core
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.hilt.work.HiltWorker
|
||||||
|
import androidx.work.CoroutineWorker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import dagger.hilt.EntryPoints
|
||||||
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
|
import eu.darken.capod.common.debug.Bugs
|
||||||
|
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.PermissionTool
|
||||||
|
import eu.darken.capod.monitor.core.MonitorComponent
|
||||||
|
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||||
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.take
|
||||||
|
|
||||||
|
|
||||||
|
@HiltWorker
|
||||||
|
class MonitorWorker @AssistedInject constructor(
|
||||||
|
@Assisted private val context: Context,
|
||||||
|
@Assisted private val params: WorkerParameters,
|
||||||
|
monitorComponentBuilder: MonitorComponent.Builder,
|
||||||
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
|
private val generalSettings: GeneralSettings,
|
||||||
|
private val permissionTool: PermissionTool,
|
||||||
|
private val podMonitor: PodMonitor,
|
||||||
|
private val bluetoothManager: BluetoothManager2,
|
||||||
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
|
private val workerScope = MonitorCoroutineScope()
|
||||||
|
private val monitorComponent = monitorComponentBuilder
|
||||||
|
.coroutineScope(workerScope)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
private val entryPoint by lazy {
|
||||||
|
EntryPoints.get(monitorComponent, MonitorWorkerEntryPoint::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var finishedWithError = false
|
||||||
|
|
||||||
|
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)" }
|
||||||
|
|
||||||
|
doDoWork()
|
||||||
|
|
||||||
|
val duration = System.currentTimeMillis() - start
|
||||||
|
|
||||||
|
log(TAG, VERBOSE) { "Execution finished after ${duration}ms, $inputData" }
|
||||||
|
|
||||||
|
Result.success(inputData)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (e !is CancellationException) {
|
||||||
|
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
||||||
|
finishedWithError = true
|
||||||
|
Result.failure(inputData)
|
||||||
|
} else {
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun doDoWork() = withContext(dispatcherProvider.IO) {
|
||||||
|
val permissionsMissingOnStart = permissionTool.missingPermissions.first()
|
||||||
|
if (permissionsMissingOnStart.isNotEmpty()) {
|
||||||
|
log(TAG, WARN) { "Aborting, missing permissions: $permissionsMissingOnStart" }
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
val monitorJob = podMonitor.mainDevice
|
||||||
|
.take(2)
|
||||||
|
.onEach { log(TAG) { "Observed: $it" } }
|
||||||
|
.launchIn(workerScope)
|
||||||
|
log(TAG, VERBOSE) { "Monitor job is active" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
withTimeout(15 * 1000) {
|
||||||
|
monitorJob.join()
|
||||||
|
}
|
||||||
|
log(TAG) { "Monitor job quit after a few takes." }
|
||||||
|
} catch (e: TimeoutCancellationException) {
|
||||||
|
log(TAG) { "Monitor job quit after finding nothing." }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val TAG = logTag("Monitor", "Worker")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.darken.capod.wear.core
|
||||||
|
|
||||||
|
import dagger.hilt.EntryPoint
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import eu.darken.capod.monitor.core.MonitorComponent
|
||||||
|
|
||||||
|
@InstallIn(MonitorComponent::class)
|
||||||
|
@EntryPoint
|
||||||
|
interface MonitorWorkerEntryPoint
|
||||||
@@ -65,7 +65,7 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val shouldStartMonitor = when (generalSettings.monitorMode.value) {
|
val shouldStartMonitor = when (generalSettings.monitorMode.value) {
|
||||||
MonitorMode.PERIODICALLY, MonitorMode.MANUAL -> false
|
MonitorMode.MANUAL -> false
|
||||||
MonitorMode.AUTOMATIC -> bluetoothManager.connectedDevices().first().isNotEmpty()
|
MonitorMode.AUTOMATIC -> bluetoothManager.connectedDevices().first().isNotEmpty()
|
||||||
MonitorMode.ALWAYS -> true
|
MonitorMode.ALWAYS -> true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,10 +127,10 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
.flatMapLatest { (monitorMode, devices) ->
|
.flatMapLatest { (monitorMode, devices) ->
|
||||||
log(TAG) { "Monitor mode: $monitorMode" }
|
log(TAG) { "Monitor mode: $monitorMode" }
|
||||||
when (monitorMode) {
|
when (monitorMode) {
|
||||||
MonitorMode.PERIODICALLY, MonitorMode.MANUAL -> flow<Unit> {
|
MonitorMode.MANUAL -> flow<Unit> {
|
||||||
// Cancel worker, ui scans manually
|
// Cancel worker, ui scans manually
|
||||||
workerScope.coroutineContext.cancelChildren()
|
workerScope.coroutineContext.cancelChildren()
|
||||||
}
|
}
|
||||||
MonitorMode.ALWAYS -> emptyFlow()
|
MonitorMode.ALWAYS -> emptyFlow()
|
||||||
MonitorMode.AUTOMATIC -> flow {
|
MonitorMode.AUTOMATIC -> flow {
|
||||||
val mainAddress = generalSettings.mainDeviceAddress.value
|
val mainAddress = generalSettings.mainDeviceAddress.value
|
||||||
|
|||||||
Reference in New Issue
Block a user