Implement periodic checks for wear

This commit is contained in:
darken
2022-09-14 18:00:18 +02:00
committed by Matthias Urhahn
parent 87ca90f6cc
commit ca2e4fec79
10 changed files with 174 additions and 17 deletions
@@ -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>
+5
View File
@@ -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>
+29 -3
View File
@@ -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