Skip to content

Ring the Alarm style from a foreground service; roll a passed time forward - #161

Merged
mapgie merged 2 commits into
mainfrom
claude/code-discussion-no-repo-wvow46
Sep 26, 2026
Merged

mapgie merged 2 commits into
mainfrom
claude/code-discussion-no-repo-wvow46

Conversation

@mapgie

@mapgie mapgie commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Why

Alarm-style memos arrived as a silent heads-up (Samsung, vibrate mode, every permission row green), while the Settings test ring worked locked or unlocked.

The LESSONS #52 fix started AlarmActivity directly from AlarmReceiver. An alarm receiver has no background-activity-launch exemption. The start only succeeds within the few seconds after the app was on screen, which is exactly when the 10-second test ring fired. A real memo fires later, the launch is blocked without an exception, and the only sound left is the notification's, on the muted notification stream.

What

Ring service

  • New AlarmRingService, a foreground service with type systemExempted (reserved for exact-alarm holders that keep an alarm ringing). AlarmReceiver starts it through NotificationHelper.deliverOnTime. It posts the alert with startForeground and loops AlarmRinger on the alarm stream until the alarm is answered or swiped away, or the 2-minute timeout ends the ring. A timeout leaves the notification in the shade.
  • AlarmActivity is now the answer screen only. It rings itself only when the service isn't ringing, which covers late deliveries from BootWorker and a refused service start.
  • Every answer path silences the ring by one id (AlertIds): the notification's Done and Snooze, swiping it away (new delete intent), the ring screen, the in-app nudge, and opening the app from the alert.
  • If Android refuses the service start, the alert is posted as a plain notification, as before.
  • Manifest: adds FOREGROUND_SERVICE and FOREGROUND_SERVICE_SYSTEM_EXEMPTED, each with a comment. The permission list in CLAUDE.md is updated to match.

Test ring

  • Fires after 1 minute instead of 10 seconds, and asks the user to leave the app first, so it runs in the same conditions a real alarm does.

Edit sheet

  • Changing the time of a once-only memo to a time that has already passed moves the ring to that time's next occurrence (ringForPickedTime).
  • A ring moved into the past can't be saved (onceOnlyRingSavable). A memo that has already rung can still be saved as long as its ring is unchanged, so renaming it still works.

Docs

Tests

  • ReminderPickedTimeTest: roll-forward and save rules. All 8 cases also ran locally against the extracted functions.
  • DeliveryStyleTest: alert ids stay distinct between memos and tasks, and keep the values alerts already in the shade were posted under.
  • a11y_check.py and check_changelog_fragment.py are clean.

To verify on device

  1. Set a memo 3 minutes out, swipe the app away, and leave the phone unlocked in vibrate mode. It should ring on the alarm stream with a heads-up.
  2. Same again with the phone locked. The ring screen should show over the lock screen, and Done or Snooze should stop the ring.
  3. Swiping the ringing notification away should stop the ring.

🤖 Generated with Claude Code

https://claude.ai/code/session_012c2DoXy9u68tYVUnmziZPy

…rward

Alarm-style memos arrived as a silent heads-up on an unlocked phone in
vibrate mode, with every permission granted. The #52 fix started
AlarmActivity from AlarmReceiver, but an alarm receiver gets no
background-activity-launch exemption: the start only went through inside
the few seconds after the app was on screen, which is exactly when the
Settings test ring fired, so the test passed while real alarms were
blocked (silently: a blocked startActivity does not throw).

- AlarmRingService: a foreground service (type systemExempted) started
  from AlarmReceiver via NotificationHelper.deliverOnTime. It posts the
  alert with startForeground and loops AlarmRinger on the alarm stream,
  locked or not, until answered, swiped, or the 2 minute timeout.
- AlarmActivity is now the answer screen; it rings itself only when the
  service is not ringing (late BootWorker deliveries, refused starts).
- Every answer path silences by the same id (AlertIds): notification
  Done/Snooze, swipe (delete intent), the ring screen, the in-app nudge,
  and opening the app from the alert.
- Manifest: FOREGROUND_SERVICE and FOREGROUND_SERVICE_SYSTEM_EXEMPTED,
  documented; CLAUDE.md permission list updated.
- The test ring now fires after a minute so it runs in the conditions a
  real alarm does.
- Edit sheet: changing the time of a once-only memo to one already gone
  rolls it to the next occurrence (ringForPickedTime), and a ring moved
  into the past cannot be saved (onceOnlyRingSavable).

LESSONS #65 records the background-start trap; #52 points to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012c2DoXy9u68tYVUnmziZPy
@mapgie
mapgie marked this pull request as ready for review September 25, 2026 17:50
Main added its own LESSONS #65 (failed action vs failed load), so the
foreground-service lesson moves to #66 and its references follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012c2DoXy9u68tYVUnmziZPy
@mapgie
mapgie merged commit 3037d1c into main Sep 26, 2026
6 checks passed
@mapgie
mapgie deleted the claude/code-discussion-no-repo-wvow46 branch September 26, 2026 01:20
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.

2 participants