Repository navigation
feat(watcher): record screen and keyguard state in its own bucket - #380
ThisIsLeoS wants to merge 3 commits into
Conversation
Turn SCREEN_INTERACTIVE/SCREEN_NON_INTERACTIVE, KEYGUARD_SHOWN/KEYGUARD_HIDDEN and DEVICE_SHUTDOWN/DEVICE_STARTUP usage events into spans of constant state: whether the screen is interactive and whether the keyguard is shown. This lets later analysis tell the time the phone was actually usable from, for example, a screensaver running in the foreground while the screen is non-interactive. The two states are kept apart because the keyguard changes while the screen is off (it is shown some seconds after screen-off, and a fingerprint can hide it before the screen turns on). A span exists only while both are known: a shutdown makes them unknown until the events recorded at the next startup. The function is pure and resumes from the last stored span, keeping its start and state, so that span is emitted again with only a later end and the server's heartbeat merge extends it. Refs: ActivityWatch#52
Add the bucket aw-watcher-android-screen, of type os.screen.state: one event
per span of constant state, with data
{"screen": "interactive" | "non-interactive", "keyguard": "shown" | "hidden"}
The spans are read from the usage events on every session ingest run, resumed
from the bucket's last event, and the span still open is extended by the next
run. A failure is logged and does not stop the session and unlock ingest.
Only on Android 10 and later: earlier versions record no shutdown, and the
span open when the phone is turned off would run on until it is turned on.
Refs: ActivityWatch#52
[Medium impact] The change adds screen-state records to one Android usage-tracking workflow. The PR appears safe to merge, with a non-blocking improvement for null history responses. Findings
SummaryAdds
One non-blocking improvement remains: skip null history responses rather than extending the stored state. ThisIsLeoS explicitly acknowledges limits around expired history, large clock corrections, repeated late events, and interrupted startup writes. Diagram%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Session watcher acquires shared lock] --> B[Check Android version and usage access]
B --> C[Read last screen span]
C --> D[Query usage events from resume point]
D --> E[Parse screen and keyguard spans]
E --> F[Write spans through insertEvent]
F --> G[Continue session and unlock writes]
Reviews (1) · Last reviewed commit: "feat(watcher): record screen and keyguar..." · Reviewed by Greptile |
Before the user first unlocks the device after a boot, queryEvents returns null (as documented from Android 11) or an empty result, and it returns an empty result when usage access is revoked during the run. An empty read would extend the last span to now, so skip the run before the first unlock, fail it on null, and check usage access again after reading.
|
CI diagnosis for head The retained logcat locates the timeout at I attempted a base-side device replay, but the cached APK stopped earlier on a missing JNI |
Adds a bucket that records whether the screen is interactive and whether the keyguard (lock screen) is shown, as spans of constant state, read from the same usage events as the session and unlock buckets.
Why
The app bucket records what is in the foreground, and that is not always someone using the phone: a screensaver started while charging is a foreground activity on a non-interactive screen, and an app is often resumed a moment before the screen turns on. With this bucket, a query can keep only app time while the screen was on and unlocked, and measure what #52 asked for: time on the lock screen and glances (screen on without unlocking).
Format
Bucket
aw-watcher-android-screen, typeos.screen.state. Each event is a span with{"screen": "interactive" | "non-interactive", "keyguard": "shown" | "hidden"}SCREEN_INTERACTIVE/SCREEN_NON_INTERACTIVE,KEYGUARD_SHOWN/KEYGUARD_HIDDEN). Non-interactive is not "display off" (an always-on display is lit while non-interactive), and an app shown over the lock screen (camera, calls) leaves the keyguard shown.How it works
parseScreenStateSpans) turns the events into spans;ScreenStateWatcherreadsUsageStatsManager.queryEvents, resumes from the bucket's last event and writes the spans with the existinginsertEvent. It runs on every session ingest run through one line inSessionEventWatcher, inside its lock; a failure is logged and does not affect the session and unlock ingest.UserUsageStatsService.initdates it at the last save). If the first shutdown or startup after the stored span's end is a startup, its shutdown predates the read, so the stored state is dropped and the state starts unknown.DEVICE_SHUTDOWNbefore API 29), and only with usage access. No new native functions: it usescreateBucket,getEventsandheartbeat.Known limits are listed in the KDoc of
ScreenStateWatcher: a gap between reads longer than Android's history, clock corrections of more than a couple of seconds, and two rarer cases around late events and interrupted writes.Testing
dumpsys usagestatsdumps within seconds../gradlew :mobile:testStandardDebugUnitTestpasses andassembleResearchDebugbuilds.Closes #52.