Skip to content

fix(watcher): guard AlarmReceiver finish() with FinishOnce - #362

Open
TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/alarm-receiver-finish-once
Open

TimeToBuildBob wants to merge 2 commits into
ActivityWatch:masterfrom
TimeToBuildBob:fix/alarm-receiver-finish-once

Conversation

@TimeToBuildBob

Copy link
Copy Markdown
Contributor

Problem

AlarmReceiver uses the same goAsync() + finally { pendingResult.finish() } pattern that caused the widget's top Play crash (#360). An OEM pre-finishing the broadcast before the coroutine's finally block throws IllegalStateException("Broadcast already finished"), which escapes the coroutine and kills the process.

This was noted in #360's body:

"Not changed: watcher/AlarmReceiver uses the same goAsync + finally { finish() } pattern, but it has no crash cluster, and putting a time budget on heartbeat parsing changes behavior. It is a candidate for the same helper in a follow-up."

Changes

  • Import AsyncBroadcast and wrap pendingResult.finish() with FinishOnce in the LOG_DATA branch of AlarmReceiver
  • FinishOnce guards with AtomicBoolean and catches/logs IllegalStateException on the second call — same protection as the widget fix
  • No timing budget added: heartbeat parsing has no widget-rendering equivalent, so the existing coroutine lifetime is unchanged

Tests

Adds AlarmReceiverFinishOnceTest with three cases:

  • OEM double-finish (IllegalStateException from PendingResult.finish()) does not escape the coroutine
  • Normal heartbeat finish completes exactly once
  • Exception during heartbeat work still finishes the broadcast

Scope

This is only the FinishOnce guard slice from the task note in ActivityWatch/activitywatch#1435. It does not implement:

  • A time budget for heartbeat parsing (deliberate — behavior-change)
  • Android missing-key migration for aw-notify.enabled (separate slice)

Closes the follow-up noted in #360.

Applies the same OEM double-finish protection added to the widget
broadcast in ActivityWatch#360. AlarmReceiver used the bare `goAsync()` +
`finally { pendingResult.finish() }` pattern; an OEM pre-finishing
the broadcast before the coroutine's finally block throws
`IllegalStateException("Broadcast already finished")`, which escapes
the coroutine and kills the process.

`FinishOnce` wraps the finish call with an `AtomicBoolean` guard and
catches (logs) `IllegalStateException` on the second call, matching
the behaviour already in `widget/AsyncBroadcast.kt`.

No timing budget is added: heartbeat parsing has no widget rendering
equivalent, so the existing coroutine lifetime is preserved.

Adds three regression tests covering:
- OEM double-finish (the crash scenario)
- Normal single finish
- Exception during heartbeat work still finishes the broadcast

Closes the follow-up noted in ActivityWatch#360.

Git-Session-Id: 6d3fd149-5f73-4a7b-97c4-d50cc5ca7c5e
@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new actionable issues remain.

Summary

Guards AlarmReceiver broadcast completion with FinishOnce without adding a time limit to heartbeat work.

  • Alarm heartbeats finish through a guard that handles earlier framework completion.

Reviews (2) · Last reviewed commit: "test(watcher): exercise production alarm..." · Reviewed by Greptile

@TimeToBuildBob

TimeToBuildBob commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 AI code review

This PR extracts the goAsync() completion path in AlarmReceiver into a new internal function launchAlarmHeartbeats() that wraps the pendingResult.finish() call in AsyncBroadcast.FinishOnce, so a second finish attempt (e.g. from an OEM pre-finishing the broadcast) is caught and logged instead of escaping the coroutine. It also adds a new test file AlarmReceiverFinishOnceTest with three tests covering double-finish, normal finish, and exception-during-work scenarios.

Safe to merge — no P0/P1 findings

Confidence 5/5

✅ No findings. The diff looks correct to me on this pass.

Files changed (2) — the diff as I read it
  • mobile/src/main/java/net/activitywatch/android/watcher/AlarmReceiver.kt — Adds launchAlarmHeartbeats() helper that wraps finish() in FinishOnce and uses it in the LOG_DATA branch.
  • mobile/src/test/java/net/activitywatch/android/watcher/AlarmReceiverFinishOnceTest.kt — Adds three tests for the new launchAlarmHeartbeats() completion path.
Previous review passes
commit score findings engine when
82b4b281bd44 4/5 1 llm 2026-10-11 17:42 UTC
48dfd8bcf81f 4/5 2 llm 2026-10-11 18:03 UTC

Reviewed 48dfd8bcf81f · openrouter/deepseek/deepseek-v4-flash-0731 · llm engine · 26s · about this reviewer

Maintainer commands

@TimeToBuildBob review (own line) — fresh review · @TimeToBuildBob fix — a worker acts on the findings. Once per comment; 👀 = received.

Git-Session-Id: 0a1fa3a7-bab6-5521-8eb2-20f4461d0318
@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

@greptileai review

@TimeToBuildBob

Copy link
Copy Markdown
Contributor Author

CI-green and mergeable (Greptile 5/5) — waiting only on a maintainer click.

This PR is ready to merge, but the bot has pull-only access to this repo and can't self-merge — surfacing it here so it isn't lost. The monitoring loop will stop re-flagging it now that this note is posted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant