mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -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 eu.darken.capod.common.SystemClockWrap
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
import kotlin.random.Random
|
||||
|
||||
@@ -20,6 +21,7 @@ class FakeBleData @Inject constructor(
|
||||
val fakeDevices = mutableListOf<BleScanResult>()
|
||||
// AirPods Gen1
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "78:73:AF:B4:85:22",
|
||||
rssi = Random.nextInt(100) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||
@@ -32,6 +34,7 @@ class FakeBleData @Inject constructor(
|
||||
|
||||
// AirPods Gen2
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "78:73:FF:B4:85:5E",
|
||||
rssi = Random.nextInt(100) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||
@@ -44,6 +47,7 @@ class FakeBleData @Inject constructor(
|
||||
|
||||
// AirPods Gen3
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "4E:9E:D1:49:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
||||
@@ -55,6 +59,7 @@ class FakeBleData @Inject constructor(
|
||||
}
|
||||
// AirPods Max
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "7E:E5:C7:65:D2:B5",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
||||
@@ -66,6 +71,7 @@ class FakeBleData @Inject constructor(
|
||||
}
|
||||
// BeatsFlex
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "5E:9E:D1:49:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||
@@ -78,6 +84,7 @@ class FakeBleData @Inject constructor(
|
||||
|
||||
// Tws i99999
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "5E:9E:D1:29:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||
@@ -90,6 +97,7 @@ class FakeBleData @Inject constructor(
|
||||
|
||||
// Unknown Device
|
||||
BleScanResult(
|
||||
receivedAt = Instant.now(),
|
||||
address = "6E:9E:D1:49:D2:6D",
|
||||
rssi = Random.nextInt(15, 75) * -1,
|
||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
||||
|
||||
@@ -18,7 +18,4 @@ enum class MonitorMode(
|
||||
@Json(name = "monitor.mode.always") ALWAYS(
|
||||
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 {
|
||||
val duration = Duration.between(seenLastAt, now)
|
||||
return lastSeenFormatter.format(
|
||||
duration.seconds.toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
)
|
||||
return if (duration > Duration.ofMinutes(1)) {
|
||||
lastSeenFormatter.format(
|
||||
duration.toMinutes().toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.MINUTES
|
||||
)
|
||||
} else {
|
||||
lastSeenFormatter.format(
|
||||
duration.seconds.toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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_automatic_label">When device is connected</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_description">Under which circumstances this app monitors Bluetooth data.</string>
|
||||
<string name="settings_scanner_mode_label">Scanner mode</string>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="eu.darken.capod">
|
||||
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -54,6 +55,10 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -2,16 +2,19 @@ package eu.darken.capod
|
||||
|
||||
import android.app.Application
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.*
|
||||
import com.getkeepsafe.relinker.ReLinker
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
import eu.darken.capod.common.debug.autoreport.AutoReporting
|
||||
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.MonitorMode
|
||||
import eu.darken.capod.wear.core.MonitorWorker
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.time.Duration
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
@@ -20,6 +23,7 @@ open class App : Application(), Configuration.Provider {
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
@Inject lateinit var autoReporting: AutoReporting
|
||||
@Inject lateinit var generalSettings: GeneralSettings
|
||||
@Inject lateinit var workManager: WorkManager
|
||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||
|
||||
override fun onCreate() {
|
||||
@@ -31,15 +35,37 @@ open class App : Application(), Configuration.Provider {
|
||||
.loadLibrary(this, "bugsnag-plugin-android-anr")
|
||||
|
||||
generalSettings.apply {
|
||||
monitorMode.value = MonitorMode.PERIODICALLY
|
||||
scannerMode.value = ScannerMode.BALANCED
|
||||
}
|
||||
|
||||
autoReporting.setup()
|
||||
|
||||
setupWorker()
|
||||
|
||||
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()
|
||||
.setMinimumLoggingLevel(android.util.Log.VERBOSE)
|
||||
.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) {
|
||||
MonitorMode.PERIODICALLY, MonitorMode.MANUAL -> false
|
||||
MonitorMode.MANUAL -> false
|
||||
MonitorMode.AUTOMATIC -> bluetoothManager.connectedDevices().first().isNotEmpty()
|
||||
MonitorMode.ALWAYS -> true
|
||||
}
|
||||
|
||||
@@ -127,10 +127,10 @@ class MonitorWorker @AssistedInject constructor(
|
||||
.flatMapLatest { (monitorMode, devices) ->
|
||||
log(TAG) { "Monitor mode: $monitorMode" }
|
||||
when (monitorMode) {
|
||||
MonitorMode.PERIODICALLY, MonitorMode.MANUAL -> flow<Unit> {
|
||||
// Cancel worker, ui scans manually
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
MonitorMode.MANUAL -> flow<Unit> {
|
||||
// Cancel worker, ui scans manually
|
||||
workerScope.coroutineContext.cancelChildren()
|
||||
}
|
||||
MonitorMode.ALWAYS -> emptyFlow()
|
||||
MonitorMode.AUTOMATIC -> flow {
|
||||
val mainAddress = generalSettings.mainDeviceAddress.value
|
||||
|
||||
Reference in New Issue
Block a user