From ca2e4fec79213765b57ff801f695865fee7ba778 Mon Sep 17 00:00:00 2001 From: darken Date: Tue, 13 Sep 2022 23:18:26 +0200 Subject: [PATCH] Implement periodic checks for wear --- .../capod/common/bluetooth/FakeBleData.kt | 8 ++ .../eu/darken/capod/main/core/MonitorMode.kt | 3 - .../capod/pods/core/PodDeviceExtensions.kt | 18 ++- app-common/src/main/res/values/strings.xml | 1 - app-wear/src/main/AndroidManifest.xml | 5 + app-wear/src/main/java/eu/darken/capod/App.kt | 32 +++++- .../darken/capod/wear/core/MonitorWorker.kt | 105 ++++++++++++++++++ .../wear/core/MonitorWorkerEntryPoint.kt | 9 ++ .../main/ui/overview/OverviewFragmentVM.kt | 2 +- .../monitor/core/worker/MonitorWorker.kt | 8 +- 10 files changed, 174 insertions(+), 17 deletions(-) create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorker.kt create mode 100644 app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorkerEntryPoint.kt diff --git a/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt b/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt index 06ca5121..664170d4 100644 --- a/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt +++ b/app-common/src/main/java/eu/darken/capod/common/bluetooth/FakeBleData.kt @@ -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() // 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, diff --git a/app-common/src/main/java/eu/darken/capod/main/core/MonitorMode.kt b/app-common/src/main/java/eu/darken/capod/main/core/MonitorMode.kt index c1fa7ac2..2ee4fc9c 100644 --- a/app-common/src/main/java/eu/darken/capod/main/core/MonitorMode.kt +++ b/app-common/src/main/java/eu/darken/capod/main/core/MonitorMode.kt @@ -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 - ), } \ No newline at end of file diff --git a/app-common/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt b/app-common/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt index 12069f6a..7d077203 100644 --- a/app-common/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt +++ b/app-common/src/main/java/eu/darken/capod/pods/core/PodDeviceExtensions.kt @@ -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 { diff --git a/app-common/src/main/res/values/strings.xml b/app-common/src/main/res/values/strings.xml index 6df35481..8e71b774 100644 --- a/app-common/src/main/res/values/strings.xml +++ b/app-common/src/main/res/values/strings.xml @@ -44,7 +44,6 @@ When app is open When device is connected Always - Periodically Monitor mode Under which circumstances this app monitors Bluetooth data. Scanner mode diff --git a/app-wear/src/main/AndroidManifest.xml b/app-wear/src/main/AndroidManifest.xml index 28d1698f..1f66df9b 100644 --- a/app-wear/src/main/AndroidManifest.xml +++ b/app-wear/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -54,6 +55,10 @@ + \ No newline at end of file diff --git a/app-wear/src/main/java/eu/darken/capod/App.kt b/app-wear/src/main/java/eu/darken/capod/App.kt index ba896430..1feabf81 100644 --- a/app-wear/src/main/java/eu/darken/capod/App.kt +++ b/app-wear/src/main/java/eu/darken/capod/App.kt @@ -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( + 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) diff --git a/app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorker.kt b/app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorker.kt new file mode 100644 index 00000000..c25bf54a --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorker.kt @@ -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") + } +} diff --git a/app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorkerEntryPoint.kt b/app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorkerEntryPoint.kt new file mode 100644 index 00000000..fddc55e4 --- /dev/null +++ b/app-wear/src/main/java/eu/darken/capod/wear/core/MonitorWorkerEntryPoint.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt index 3722fa5b..73f4b03c 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt @@ -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 } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt index 0a34725a..f4804aa4 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt @@ -127,10 +127,10 @@ class MonitorWorker @AssistedInject constructor( .flatMapLatest { (monitorMode, devices) -> log(TAG) { "Monitor mode: $monitorMode" } when (monitorMode) { - MonitorMode.PERIODICALLY, MonitorMode.MANUAL -> flow { - // Cancel worker, ui scans manually - workerScope.coroutineContext.cancelChildren() - } + MonitorMode.MANUAL -> flow { + // Cancel worker, ui scans manually + workerScope.coroutineContext.cancelChildren() + } MonitorMode.ALWAYS -> emptyFlow() MonitorMode.AUTOMATIC -> flow { val mainAddress = generalSettings.mainDeviceAddress.value