diff --git a/mobile/src/main/java/net/activitywatch/android/workers/NotifyWorker.kt b/mobile/src/main/java/net/activitywatch/android/workers/NotifyWorker.kt index f7349e97..5820a5d1 100644 --- a/mobile/src/main/java/net/activitywatch/android/workers/NotifyWorker.kt +++ b/mobile/src/main/java/net/activitywatch/android/workers/NotifyWorker.kt @@ -15,6 +15,8 @@ import net.activitywatch.android.EXTRA_OPEN_ACTIVITY_VIEW import net.activitywatch.android.MainActivity import net.activitywatch.android.R import net.activitywatch.android.RustInterface +import net.activitywatch.android.watcher.SessionEventWatcher +import net.activitywatch.android.watcher.UsageStatsWatcher import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -168,14 +170,24 @@ internal interface NotifyBackend { fun checkServer() fun getSetting(key: String): String fun androidQuery(timeperiod: String): String + fun ingestLatestUsage() } private class RustNotifyBackend(context: Context) : NotifyBackend { + private val appContext = context.applicationContext private val ri = RustInterface(context) override fun checkServer() { ri.getBucketsJSON() } override fun getSetting(key: String): String = ri.getSetting(key) override fun androidQuery(timeperiod: String): String = ri.androidQuery(timeperiod) + + // Usage events otherwise reach the datastore only on the hourly (inexact) alarm, app + // open or widget refresh, so alerts were judged against data up to an hour old. + // processEventsSinceLastUpdate() skips if another ingestion run holds its lock. + override fun ingestLatestUsage() { + if (!UsageStatsWatcher.isUsageAllowed(appContext)) return + SessionEventWatcher(appContext).processEventsSinceLastUpdate() + } } class NotifyWorker internal constructor( @@ -205,6 +217,12 @@ class NotifyWorker internal constructor( // Keep the periodic worker scheduled so enabling later takes effect, // but never query activity or create notifications when disabled. if (alerts.isEmpty()) return Result.success() + try { + backend.ingestLatestUsage() + } catch (e: Exception) { + // Still check thresholds against what is already stored. + Log.w(TAG, "Failed to ingest usage events before checking thresholds", e) + } val zone = ZoneId.systemDefault() val startOfDayHour = parseStartOfDayHour(backend.getSetting("startOfDay")) val now = LocalDateTime.now(zone) diff --git a/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerExecutionTest.kt b/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerExecutionTest.kt index 369a41c6..df211ea0 100644 --- a/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerExecutionTest.kt +++ b/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerExecutionTest.kt @@ -19,7 +19,10 @@ import org.robolectric.annotation.Config @RunWith(RobolectricTestRunner::class) @Config(sdk = [28], application = Application::class) class NotifyWorkerExecutionTest { - private class FakeBackend(private val setting: String) : NotifyBackend { + private class FakeBackend( + private val setting: String, + private val failIngest: Boolean = false, + ) : NotifyBackend { val calls = mutableListOf() override fun checkServer() { @@ -35,6 +38,11 @@ class NotifyWorkerExecutionTest { calls += "query" return """[{"cat_events":[{"duration":120,"data":{"${'$'}category":["Work"]}}]}]""" } + + override fun ingestLatestUsage() { + calls += "ingest" + if (failIngest) throw IllegalStateException("ingest failed") + } } private fun runWorker(backend: FakeBackend): ListenableWorker.Result { @@ -92,7 +100,18 @@ class NotifyWorkerExecutionTest { """{"enabled":true,"alerts":[{"category":"Work","thresholds_minutes":[1]}]}""" ) assertEquals(ListenableWorker.Result.success(), runWorker(backend)) - assertEquals(listOf("server", "aw-notify", "startOfDay", "query"), backend.calls) + assertEquals(listOf("server", "aw-notify", "ingest", "startOfDay", "query"), backend.calls) + assertEquals(1, notificationCount()) + } + + @Test + fun ingestFailureStillChecksStoredData() { + val backend = FakeBackend( + """{"enabled":true,"alerts":[{"category":"Work","thresholds_minutes":[1]}]}""", + failIngest = true, + ) + assertEquals(ListenableWorker.Result.success(), runWorker(backend)) + assertEquals(listOf("server", "aw-notify", "ingest", "startOfDay", "query"), backend.calls) assertEquals(1, notificationCount()) } }