Skip to content

Add configurable timer ring duration and repeat reminders - #861

Closed
Marcus2626 wants to merge 5 commits into
Ebullioscopic:devfrom
Marcus2626:codex/timer-repeat-alerts
Closed

Marcus2626 wants to merge 5 commits into
Ebullioscopic:devfrom
Marcus2626:codex/timer-repeat-alerts

Conversation

@Marcus2626

@Marcus2626 Marcus2626 commented Sep 29, 2026 •

Copy link
Copy Markdown

Closes #859

Summary

Atoll's built-in timer currently keeps ringing after it expires until the timer is stopped. This change lets users set how long each ring lasts and how many minutes to wait before the next reminder. The timer stays in overtime between rings and continues reminding until it is stopped.

Root cause

On expiry, TimerManager started an AVAudioPlayer with unlimited looping. It had no ringing/silent phases or saved settings for an automatic stop and later reminder.

What changed

  • Added ring-duration controls in minutes and seconds, and a repeat interval in minutes, to Timer settings. Defaults are 30 seconds and 5 minutes. Added English and Simplified Chinese text.
  • Added TimerAlertController to alternate ringing and silence. The silent interval begins when playback actually stops; changing settings also affects an active reminder.
  • Stop pending reminders when a timer is stopped, reset, or replaced. Pausing silences the alert; resuming an overtime timer starts a new ring. Sleep/wake handling avoids a burst of missed reminders.
  • Added a standalone Swift regression test and its runner to CI.

Code scope and function changes

The runtime changes are confined to DynamicIsland/managers/TimerManager.swift (timer lifecycle), the new DynamicIsland/managers/TimerAlertController.swift (ring/silence scheduling), and DynamicIsland/components/Settings/SettingsView.swift (Timer settings). DynamicIsland/models/Constants.swift adds the 30-second and 5-minute defaults; DynamicIsland/Localizable.xcstrings adds English and Simplified Chinese text. The remaining changes are the Swift regression test, its runner and CI entry, the .gitignore exception for that runner, and a settings screenshot.

Existing code changed

File and function Change
TimerManager.init() Observe system sleep and wake: silence the current ring before sleep and update the alert phase once after wake.
TimerManager.startTimer() Stop any previous alert when replacing a timer, then use the shared countdown scheduler.
TimerManager.stopTimer(), forceStopTimer(), and adoptExternalTimer() Cancel pending repeat reminders and stop timer audio when a timer is dismissed or replaced.
TimerManager.pauseTimer() and resumeTimer() Pause cancels the scheduled countdown and silences the alert. Resume schedules a fresh countdown and, if already in overtime, begins a new ring.
TimerManager.beginTimerSession() and endTimerSession() Invalidate callbacks queued by an older countdown and clear its pending alert.
TimerManager.playTimerSound() Replace any existing audio player before playback. The player loops only until the alert controller ends the current ringing phase.
TimerSettings in SettingsView.swift Read the two saved values and insert the new alert controls into the existing Timer settings view.

New code added

File and function Purpose
TimerManager.scheduleCountdown() Share one countdown path between start and resume. A generation ID prevents a previously queued tick from changing a stopped or replaced timer; expiry enters overtime once and starts the alert controller.
TimerAlertController.init(...) and deinit Accept clock, settings, and audio callbacks so the phase logic can be tested without real playback; invalidate the RunLoop timer and silence audio during cleanup.
TimerAlertController.start() and stop() Start or cancel the ringing/silent cycle and its RunLoop timer; stop() also silences playback.
TimerAlertController.update() Compare the current time with the start of the active phase. End ringing after the configured seconds, then start the next ring after the configured silent minutes. It reads settings on each update so edits affect an active alert.
TimerAlertController.prepareForSleep() Silence an active ring before sleep and leave the controller in the waiting phase, avoiding a burst of missed reminders on wake.
TimerAlertController.duration(minutes:seconds:) and clamp(_:to:) Convert the two editor fields to one saved number of seconds and bound saved settings before use.
TimerSettings.timerAlertSection and TimerAlertConfiguration Show the new controls. Bindings keep the minute and second fields in sync with the saved total; the seconds range excludes 0:00 and durations over one hour.
TimerAlertRegression.main() and its advance(_:) helper Exercise phase boundaries, cancellation, settings changes, replacement, simulated sleep/wake, delayed updates, RunLoop scheduling, and cleanup. tests/run_timer_alert_regression.sh builds and runs this test in CI.

At expiry, scheduleCountdown() changes the timer to overtime and calls TimerAlertController.start(). The controller loops the selected sound, stops it when the ring duration elapses, and records that stop time as the start of the silent interval. When that interval elapses, it starts another ring. Stopping or replacing the timer calls stop(), which cancels the cycle. The existing countdown continues to display overtime throughout the silent intervals.

Related short-timer UI check

In the earlier separate-window version, the overlap was reproducible with a 1-second timer: hover over the Dynamic Island, then move the pointer away just before expiry. At expiry, the old X cancel button and the square Stop button could appear together on the right while the timer title scrolled on the left; the controls obscured the overtime digits. Hovering over the island and moving away again refreshed the layout, leaving one square Stop button and readable overtime digits.

The same short-timer sequence did not reproduce the overlap on this dev-based branch. dev had already replaced the separate control window with inline controls in 56ff960, before this branch was created. This PR contains no Dynamic Island layout change; the result is recorded here as a related UI regression check, not as a fix made by this patch.

Testing done

Tested on macOS 26.7 / Apple Silicon with Xcode 26.6:

Check Result
App build Debug arm64 build passed. The app launched locally; codesign --verify --deep --strict passed.
Timer alert module regression bash tests/run_timer_alert_regression.sh passed. Checks minute/second conversion and bounds; exact ring/silence boundaries; cancellation; changes to an active alert; replacement; simulated sleep/wake; delayed updates; RunLoop scheduling; and cleanup. Audio callbacks are simulated in this test.
Project regression tests python3 -m unittest discover -s tests -v passed 7/7: six privacy-configuration checks and the existing timer lifecycle regression.
Manual behavior For example, set the ring duration to 10 seconds and the repeat interval to 1 minute: the timer rings at expiry, silences itself after about 10 seconds, rings again after about 1 minute of silence, and stops when manually dismissed. I also varied the ring duration in 10-second steps and changed the minute-based repeat interval, completing at least 10 manual runs. Automatic silencing, repeat reminders, and manual stopping behaved as expected.
Diff check git diff --check passed; the working tree is clean.

Settings screenshot

Timer alert settings

Summary by CodeRabbit

  • New Features
    • Configure how long timer alerts ring and how often they repeat in Timer Settings. Alerts alternate between ringing and silence until the timer is stopped, and setting changes apply to active reminders.
  • Bug Fixes
    • Timer alerts now stop or adjust appropriately when a timer is paused, stopped, replaced, or the device sleeps and wakes. Resuming an overtime timer restarts its alert.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7b233817-ad73-414f-993a-d0f68fc00f9f

📥 Commits

Reviewing files that changed from the base of the PR and between 959dd03 and 415ed1b.

📒 Files selected for processing (9)
  • .github/workflows/ci.yml
  • .gitignore
  • DynamicIsland/Localizable.xcstrings
  • DynamicIsland/components/Settings/SettingsView.swift
  • DynamicIsland/managers/TimerAlertController.swift
  • DynamicIsland/managers/TimerManager.swift
  • DynamicIsland/models/Constants.swift
  • tests/TimerAlertRegression.swift
  • tests/run_timer_alert_regression.sh

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The built-in timer supports configurable alert ring durations and repeat intervals. Alerts alternate between ringing and silence until stopped. Timer settings, timer lifecycle handling, localized text, regression checks, and CI are updated.

Changes

Built-in timer alerts

Layer / File(s) Summary
Timer alert settings
DynamicIsland/models/Constants.swift, DynamicIsland/components/Settings/SettingsView.swift, DynamicIsland/Localizable.xcstrings
Adds persisted defaults of 30 seconds for ringing and 5 minutes for the repeat interval. Settings provide bounded duration and interval controls, with English and Simplified Chinese text.
Alert cycle and timer integration
DynamicIsland/managers/TimerAlertController.swift, DynamicIsland/managers/TimerManager.swift
Adds ringing and silent phases. TimerManager updates alert playback during timer lifecycle events and guards queued countdown callbacks.
Regression checks and CI
tests/TimerAlertRegression.swift, tests/run_timer_alert_regression.sh, .gitignore, .github/workflows/ci.yml
Adds checks for alert timing, setting changes, sleep and wake behavior, delayed ticks, bounds, and cleanup. The runner compiles and runs the checks, and CI invokes it after timer lifecycle validation.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant TimerManager
  participant TimerAlertController
  participant SoundPlayer
  TimerManager->>TimerAlertController: Update alert with configured duration and interval
  TimerAlertController-->>TimerManager: Return ringing state when a phase changes
  TimerManager->>SoundPlayer: Play sound while ringing
  TimerManager->>SoundPlayer: Stop sound while silent
  TimerManager->>TimerAlertController: Stop alert on timer lifecycle changes
Loading

Suggested reviewers: ebullioscopic

Merge Risk: ⚪ Minimal · up to 415ed

Configurable repeating alerts have bounded settings and lifecycle handling for stopping, pausing, replacement, and sleep. No actionable merge-blocking risk is established; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 415ed

The change is confined to local timer alerts, with bounded settings and safeguards against stale reminders. No introduced security issue was established. Remaining uncertainty concerns concurrent lifecycle callers and actual audio behavior during interruption and recovery.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated production effect is on the application's shared local timer session and audio playback. The supplied high-fanout ranges concern the regression harness; they do not establish broader production-service or tenant exposure.

Trust Boundaries and Controls

  • observed — The new preference inputs affect timing rather than file identity or privilege. Runtime clamping independently limits stored values, and queued countdown work checks identity and ownership state before it can advance an alert or start playback.

Resilience and Maintainability Implications

  • observed — Sleep handling stops player playback on the main queue, and a delayed update advances only one phase with a fresh timestamp rather than replaying missed reminders. Playback setup failure falls back to a one-shot system beep, which has no AVAudioPlayer cancellation handle; that fallback predates the PR.

Hardening Proposals

  • proposed — Consider encoding main-actor ownership in the shared timer API so future callers cannot race alert transitions against cancellation or replacement. This would strengthen an existing ownership assumption, not remedy an established PR-introduced vulnerability.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 6 files. (3 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes: configurable timer ring duration and repeated reminders.
Linked Issues check ✅ Passed The PR meets the coding requirements in directly linked issue [#859]. TimerSettings adds persisted ring-duration and interval controls with the required defaults and bounds. TimerAlertController i…
Out of Scope Changes check ✅ Passed The changed files stay within issue [#859]. The settings UI, localization, timer controller, timer integration, lifecycle safeguards, regression test, test runner, and CI configuration implement or ve…
Full details: Docstring Coverage

Explanation

Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 22 functions across 6 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@Marcus2626
Marcus2626 marked this pull request as draft September 29, 2026 13:10
@Marcus2626
Marcus2626 marked this pull request as ready for review September 29, 2026 13:53
@Marcus2626

Copy link
Copy Markdown
Author

Simplified the timer reminder implementation to fit the existing timer structure more closely.

  • Reused TimerManager’s existing one-second tick instead of running a separate 0.25-second polling timer. The timer now uses the common run-loop mode so reminders continue during UI tracking.
  • Reduced TimerAlertController to a small value type that tracks ringing/silent phases. Removed its timer, injected callbacks, and cleanup lifecycle; audio playback remains in TimerManager.
  • Replaced the additional countdown-generation UUID with a check against the current Timer instance, preserving protection against queued callbacks from a stopped or replaced timer.
  • Consolidated reminder cleanup in the existing session and pause methods.
  • Moved the alert settings directly into TimerSettings, reusing the existing controls and removing the single-use wrapper view.

The existing timer method signatures and external Clock integration remain unchanged. Configurable ring duration, repeat intervals, live setting changes, pause/resume behavior, and sleep/wake handling are preserved. Alert transitions are now checked once per second instead of every 0.25 seconds; delayed ticks advance only one phase and do not replay missed reminders.

Against the same dev baseline (959dd03), application Swift additions decreased from 265 to 168 lines (about 37%). The alert controller decreased from 112 to 41 lines, and total patch additions—including localization and tests—decreased from 487 to 383 lines.

Validation:

  • Full Debug arm64 application build passed.
  • Timer alert regression checks passed, covering phase boundaries, setting changes, replacement, cancellation, simulated sleep/wake, delayed updates, and persisted-value bounds.
  • All 7 existing Python-discovered tests passed.
  • A temporary integration harness using the actual countdown/update methods and a real RunLoop passed queued-callback rejection, expiry, automatic silencing, and stop checks with simulated audio.
  • git diff --check passed.

@Marcus2626

Copy link
Copy Markdown
Author

@coderabbitai full review

Please review the simplified timer reminder implementation, especially timer reuse, stale callback protection, pause/resume behavior, and sleep/wake handling.

@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

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.

Add configurable repeat alerts to the built-in timer

1 participant