diff --git a/aw-server-rust b/aw-server-rust index e8e6e903..75722f3c 160000 --- a/aw-server-rust +++ b/aw-server-rust @@ -1 +1 @@ -Subproject commit e8e6e903e6e081158376a2147706d1ef97fd5e79 +Subproject commit 75722f3c51269615fbd2ffaf2a946824d7cc8ccd diff --git a/mobile/src/main/java/net/activitywatch/android/RustInterface.kt b/mobile/src/main/java/net/activitywatch/android/RustInterface.kt index 18196bb6..e3b56743 100644 --- a/mobile/src/main/java/net/activitywatch/android/RustInterface.kt +++ b/mobile/src/main/java/net/activitywatch/android/RustInterface.kt @@ -50,6 +50,7 @@ class RustInterface(context: Context? = null) { external fun heartbeat(bucket_id: String, event: String, pulsetime: Double): String external fun query(query: String, timeperiods: String): String external fun androidQuery(timeperiods: String): String + external fun getSetting(key: String): String external fun migrateHostname(hostname: String): String fun sayHello(to: String): String { diff --git a/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt b/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt index fc59f757..ead4d34c 100644 --- a/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt +++ b/mobile/src/main/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdater.kt @@ -26,7 +26,6 @@ import org.threeten.bp.ZoneId import org.threeten.bp.format.DateTimeFormatter private const val TAG = "CategoryTimeWidget" -private val DEFAULT_START_OF_DAY = LocalTime.of(4, 0) // matches aw-webui default "04:00" // Bar chart dimensions private const val BAR_WIDTH = 400 @@ -45,6 +44,8 @@ private val CATEGORY_ACCENT_COLORS = intArrayOf( */ object CategoryTimeWidgetUpdater { + private val DEFAULT_START_OF_DAY = LocalTime.of(4, 0) // matches aw-webui default "04:00" + // App row IDs private val appRowIds = intArrayOf( R.id.app_row_1, @@ -231,30 +232,8 @@ object CategoryTimeWidgetUpdater { } /** - * Fetch the startOfDay boundary from the AW server settings API. - * Falls back to DEFAULT_START_OF_DAY if the server is unavailable or the setting is unset. - */ - private fun fetchStartOfDay(): LocalTime { - return try { - val url = java.net.URL("http://127.0.0.1:5600/api/0/settings/startOfDay") - val conn = url.openConnection() as java.net.HttpURLConnection - conn.connectTimeout = 1000 - conn.readTimeout = 1000 - if (conn.responseCode == 200) { - parseStartOfDay(conn.inputStream.bufferedReader().readText()) - } else { - Log.d(TAG, "startOfDay setting unavailable (HTTP ${conn.responseCode}), using default") - DEFAULT_START_OF_DAY - } - } catch (e: Exception) { - Log.d(TAG, "Could not fetch startOfDay setting, using default: ${e.message}") - DEFAULT_START_OF_DAY - } - } - - /** - * Parse the server's startOfDay JSON response into a LocalTime. - * Server returns null (unset) or a quoted string like "04:00" or "04:30". + * Parse the datastore's startOfDay JSON value into a LocalTime. + * Native settings return null (unset) or a quoted string like "04:00" or "04:30". */ internal fun parseStartOfDay(response: String): LocalTime { val v = response.trim() @@ -264,7 +243,11 @@ object CategoryTimeWidgetUpdater { val parts = v.trim('"').split(":") val hour = parts.getOrNull(0)?.toIntOrNull() ?: return DEFAULT_START_OF_DAY val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0 - LocalTime.of(hour, minute) + try { + LocalTime.of(hour, minute) + } catch (e: Exception) { + DEFAULT_START_OF_DAY + } } else -> DEFAULT_START_OF_DAY } @@ -272,12 +255,13 @@ object CategoryTimeWidgetUpdater { /** * Query today's category times, using the same day boundary as aw-webui. - * Reads startOfDay from the AW server settings so the widget matches the Activity view. + * Reads startOfDay directly from the datastore so the widget matches the Activity view + * even when the authenticated HTTP server is unavailable. */ private fun getCategoryTimesToday(ri: RustInterface): List> { val zone = ZoneId.systemDefault() val formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME - val startOfDayTime = fetchStartOfDay() + val startOfDayTime = parseStartOfDay(ri.getSetting("startOfDay")) // Match aw-webui's day boundary: if current time is before startOfDay we're still // in the previous day's period (e.g. 3:45 AM with startOfDay=04:00 → "yesterday") 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 94425dca..b746d184 100644 --- a/mobile/src/main/java/net/activitywatch/android/workers/NotifyWorker.kt +++ b/mobile/src/main/java/net/activitywatch/android/workers/NotifyWorker.kt @@ -64,6 +64,27 @@ internal fun parseAlerts(json: String): List { } } +internal fun alertsFromSetting(json: String): List { + val value = json.trim() + if (value.isEmpty() || value == "null") return DEFAULT_ALERTS + + return try { + parseAlerts(value).takeIf { it.isNotEmpty() } ?: DEFAULT_ALERTS + } catch (e: Exception) { + DEFAULT_ALERTS + } +} + +internal fun parseStartOfDayHour(response: String): Int { + val value = response.trim() + val hour = when { + value == "null" -> null + value.startsWith("\"") -> value.trim('"').split(":").firstOrNull()?.toIntOrNull() + else -> value.toIntOrNull() + } + return hour?.takeIf { it in 0..23 } ?: DEFAULT_START_OF_DAY_HOUR +} + // Include thresholds in the pref key so state resets when configuration changes. // A lowered threshold mid-day would otherwise be silently skipped because the old // triggered value is higher than all new thresholds. @@ -89,10 +110,10 @@ class NotifyWorker(context: Context, params: WorkerParameters) : Worker(context, return try { val zone = ZoneId.systemDefault() - val startOfDayHour = fetchStartOfDayHour() + val startOfDayHour = parseStartOfDayHour(ri.getSetting("startOfDay")) val now = LocalDateTime.now(zone) val categorySeconds = getCategorySecondsToday(ri, now, zone, startOfDayHour) - val alerts = fetchAlerts() + val alerts = alertsFromSetting(ri.getSetting("aw-notify")) checkAndNotify(categorySeconds, logicalDayDate(now, startOfDayHour), alerts) Result.success() } catch (e: Exception) { @@ -228,51 +249,6 @@ class NotifyWorker(context: Context, params: WorkerParameters) : Worker(context, } } - private fun fetchStartOfDayHour(): Int { - return try { - val url = java.net.URL("http://127.0.0.1:5600/api/0/settings/startOfDay") - val conn = url.openConnection() as java.net.HttpURLConnection - conn.connectTimeout = 1_000 - conn.readTimeout = 1_000 - if (conn.responseCode == 200) { - parseStartOfDayHour(conn.inputStream.bufferedReader().readText()) - } else { - DEFAULT_START_OF_DAY_HOUR - } - } catch (e: Exception) { - DEFAULT_START_OF_DAY_HOUR - } - } - - private fun fetchAlerts(): List { - return try { - val url = java.net.URL("http://127.0.0.1:5600/api/0/settings/aw-notify") - val conn = url.openConnection() as java.net.HttpURLConnection - conn.connectTimeout = 1_000 - conn.readTimeout = 1_000 - if (conn.responseCode == 200) { - val text = conn.inputStream.bufferedReader().readText().trim() - if (text == "null" || text.isBlank()) DEFAULT_ALERTS - else parseAlerts(text).takeIf { it.isNotEmpty() } ?: DEFAULT_ALERTS - } else { - DEFAULT_ALERTS - } - } catch (e: Exception) { - Log.d(TAG, "Could not fetch alert config from server, using defaults") - DEFAULT_ALERTS - } - } - - private fun parseStartOfDayHour(response: String): Int { - val v = response.trim() - return when { - v == "null" -> DEFAULT_START_OF_DAY_HOUR - v.startsWith("\"") -> v.trim('"').split(":").firstOrNull()?.toIntOrNull() - ?: DEFAULT_START_OF_DAY_HOUR - else -> v.toIntOrNull() ?: DEFAULT_START_OF_DAY_HOUR - } - } - private fun formatDuration(minutes: Int): String { val h = minutes / 60 val m = minutes % 60 diff --git a/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt b/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt index d298ee28..02e49460 100644 --- a/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt +++ b/mobile/src/test/java/net/activitywatch/android/widget/CategoryTimeWidgetUpdaterTest.kt @@ -2,9 +2,24 @@ package net.activitywatch.android.widget import org.junit.Assert.assertEquals import org.junit.Test +import org.threeten.bp.LocalTime class CategoryTimeWidgetUpdaterTest { + @Test + fun parseStartOfDay_readsMinutesFromNativeSetting() { + assertEquals( + LocalTime.of(4, 30), + CategoryTimeWidgetUpdater.parseStartOfDay("\"04:30\"") + ) + } + + @Test + fun parseStartOfDay_fallsBackForMissingOrInvalidSetting() { + assertEquals(LocalTime.of(4, 0), CategoryTimeWidgetUpdater.parseStartOfDay("null")) + assertEquals(LocalTime.of(4, 0), CategoryTimeWidgetUpdater.parseStartOfDay("\"99:00\"")) + } + private fun catEvent(duration: Double, vararg category: String): String { val cats = category.joinToString(",") { "\"$it\"" } return """{"duration":$duration,"data":{"${'$'}category":[$cats]}}""" diff --git a/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerTest.kt b/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerTest.kt index 6c286869..067cf697 100644 --- a/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerTest.kt +++ b/mobile/src/test/java/net/activitywatch/android/workers/NotifyWorkerTest.kt @@ -7,6 +7,30 @@ import org.threeten.bp.LocalDate import org.threeten.bp.LocalDateTime class NotifyWorkerTest { + @Test + fun parseStartOfDayHour_readsNativeSettingAndFallsBack() { + assertEquals(6, parseStartOfDayHour("\"06:00\"")) + assertEquals(4, parseStartOfDayHour("null")) + assertEquals(4, parseStartOfDayHour("\"99:00\"")) + } + + @Test + fun alertsFromSetting_readsNativeSetting() { + val alerts = alertsFromSetting( + """[{"category":"Work","label":"Focus","thresholdMinutes":[30]}]""" + ) + + assertEquals(1, alerts.size) + assertEquals("Focus", alerts[0].label) + assertEquals(listOf(30), alerts[0].thresholdMinutes) + } + + @Test + fun alertsFromSetting_fallsBackForMissingOrInvalidSetting() { + assertEquals("All", alertsFromSetting("null")[0].label) + assertEquals("All", alertsFromSetting("not-json")[0].label) + } + @Test fun logicalDayDate_usesPreviousDateBeforeConfiguredBoundary() { val now = LocalDateTime.of(2026, 7, 24, 3, 59)