fix(android): refresh the phone's own row when it reports
The fleet card reads last-seen from the claw, and only a landed report moves it. Periodic work is deferred while the device idles — Doze stretches the fifteen-minute floor to hours — so a phone sitting in the human's hand could show itself as "last seen 3h ago" with nothing wrong on the claw. Kick a status run on resume, the same way the commands worker already is, and reload the fleet whenever a run lands. The landed-run signal is a new clock in StatusRepository, written in the same edit that saves the snapshot (which only happens once every post of the run succeeded), so the card reloads on a report that actually reached the claw.
This commit is contained in:
1 parent
4a91635730
commit
385788c935
4 files changed
+39
-5
No files matched your search
+10
-1
@@ -95,7 +95,16 @@ manages the device fleet the claw keeps.
|
|||||||
it is. The heartbeat exists for the fleet's last-seen time: the claw
|
it is. The heartbeat exists for the fleet's last-seen time: the claw
|
||||||
stamps a device "seen" on whatever post lands, so a run with no
|
stamps a device "seen" on whatever post lands, so a run with no
|
||||||
transitions posts one anyway and the row stays fresh at the worker's
|
transitions posts one anyway and the row stays fresh at the worker's
|
||||||
cadence instead of the last flip.
|
cadence instead of the last flip. A failed post ends the run anyway —
|
||||||
|
the snapshot is only saved when every post landed, so the next
|
||||||
|
scheduled run re-derives the whole transition set; `retry()` would
|
||||||
|
hand the cadence to WorkManager's exponential backoff (hours) and the
|
||||||
|
phone would read as gone. And because idle devices defer periodic work
|
||||||
|
(Doze stretches the fifteen-minute floor to hours), the app kicks a
|
||||||
|
status run on every foreground: a phone in hand reports then and
|
||||||
|
there, and the fleet card reloads on each landed run (`lastReportAt`)
|
||||||
|
so its own row reads from the new clock instead of the one it was
|
||||||
|
opened at.
|
||||||
|
|
||||||
Every report is an observation on the claw: it lands in the phone
|
Every report is an observation on the claw: it lands in the phone
|
||||||
tables (the ingest answers `{"ingested": "observed"}`) and a `phone`
|
tables (the ingest answers `{"ingested": "observed"}`) and a `phone`
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import rs.rhema.phone.ui.RhemaTheme
|
|||||||
import rs.rhema.phone.ui.SessionsScreen
|
import rs.rhema.phone.ui.SessionsScreen
|
||||||
import rs.rhema.phone.ui.SettingsScreen
|
import rs.rhema.phone.ui.SettingsScreen
|
||||||
import rs.rhema.phone.work.enqueuePhoneCommandsNow
|
import rs.rhema.phone.work.enqueuePhoneCommandsNow
|
||||||
|
import rs.rhema.phone.work.enqueuePhoneStatusNow
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
@@ -77,11 +78,16 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The control queue (phone commands) is freshest when the human is
|
// Both queues are freshest when the human is holding the phone; KEEP
|
||||||
// holding the phone; KEEP coalesces rapid resume bursts.
|
// coalesces rapid resume bursts. The status run is the one that matters
|
||||||
|
// to the fleet: periodic work is deferred while the device idles (Doze
|
||||||
|
// stretches the 15-minute floor to hours), so without this kick the
|
||||||
|
// phone's own row can say it was last seen hours ago while it sits in
|
||||||
|
// the human's hand.
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
enqueuePhoneCommandsNow(applicationContext)
|
enqueuePhoneCommandsNow(applicationContext)
|
||||||
|
enqueuePhoneStatusNow(applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package rs.rhema.phone.data
|
|||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
|
import androidx.datastore.preferences.core.longPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
@@ -38,6 +39,7 @@ class StatusRepository(private val store: DataStore<Preferences>, private val js
|
|||||||
|
|
||||||
private object Keys {
|
private object Keys {
|
||||||
val snapshot = stringPreferencesKey("last_phone_snapshot")
|
val snapshot = stringPreferencesKey("last_phone_snapshot")
|
||||||
|
val reportedAt = longPreferencesKey("last_phone_report_at")
|
||||||
val notificationCursor = stringPreferencesKey("last_notification_id")
|
val notificationCursor = stringPreferencesKey("last_notification_id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +48,20 @@ class StatusRepository(private val store: DataStore<Preferences>, private val js
|
|||||||
runCatching { json.decodeFromString<PhoneSnapshot>(raw) }.getOrNull()
|
runCatching { json.decodeFromString<PhoneSnapshot>(raw) }.getOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the last run landed every post, epoch millis; 0 before the
|
||||||
|
// first one. The fleet card watches this so it reloads after a report
|
||||||
|
// lands: the last-seen it draws is the claw's clock, which only moves
|
||||||
|
// when a run has actually reached it.
|
||||||
|
val lastReportAt: Flow<Long> =
|
||||||
|
store.data.map { it[Keys.reportedAt] ?: 0L }
|
||||||
|
|
||||||
// Only called after every ingest post of a run succeeded, so a failed run
|
// Only called after every ingest post of a run succeeded, so a failed run
|
||||||
// retries the whole transition set next time instead of swallowing it.
|
// retries the whole transition set next time instead of swallowing it.
|
||||||
suspend fun setSnapshot(snapshot: PhoneSnapshot) {
|
suspend fun setSnapshot(snapshot: PhoneSnapshot) {
|
||||||
store.edit { it[Keys.snapshot] = json.encodeToString(snapshot) }
|
store.edit {
|
||||||
|
it[Keys.snapshot] = json.encodeToString(snapshot)
|
||||||
|
it[Keys.reportedAt] = System.currentTimeMillis()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor into the claw's notification list: the last id this device showed.
|
// Cursor into the claw's notification list: the last id this device showed.
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ fun PhonesCard(container: AppContainer) {
|
|||||||
var forgetCandidate by remember { mutableStateOf<PhoneInfo?>(null) }
|
var forgetCandidate by remember { mutableStateOf<PhoneInfo?>(null) }
|
||||||
var forgotName by remember { mutableStateOf<String?>(null) }
|
var forgotName by remember { mutableStateOf<String?>(null) }
|
||||||
val deviceId by container.settings.deviceIdFlow.collectAsState(initial = "")
|
val deviceId by container.settings.deviceIdFlow.collectAsState(initial = "")
|
||||||
|
// The last run that landed every post. The fleet is read from the claw,
|
||||||
|
// so this device's last-seen only moves when a report has actually
|
||||||
|
// reached it; reloading on this flow keeps the phone from calling
|
||||||
|
// itself hours stale while it is in hand.
|
||||||
|
val lastReportAt by container.status.lastReportAt.collectAsState(initial = 0L)
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -73,7 +78,9 @@ fun PhonesCard(container: AppContainer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) { load() }
|
// First paint loads the fleet as-is; every landed status run reloads it,
|
||||||
|
// so the row this phone reads about itself carries the new last-seen.
|
||||||
|
LaunchedEffect(lastReportAt) { load() }
|
||||||
|
|
||||||
RhemaCard(title = stringResource(R.string.card_phones), meta = stringResource(R.string.phones_hint)) {
|
RhemaCard(title = stringResource(R.string.card_phones), meta = stringResource(R.string.phones_hint)) {
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
Reference in new issue
Block a user