Skip to content

fix: re-block timed unblocks at the end registered with iOS - #106

Merged
ykisana merged 1 commit into
mainfrom
fix/unblock-end-whole-second
Nov 15, 2026
Merged

ykisana merged 1 commit into
mainfrom
fix/unblock-end-whole-second

Conversation

@ykisana

@ykisana ykisana commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A timed unblock can stay lifted for up to 30 minutes past its end.

  • DeviceActivityScheduleFactory.window builds the interval from whole-second date components, so the end registered with iOS drops the fraction of a second.
  • TimedUnblockService stored the unshortened end, so for example 10:30:00.6 was saved while 10:30:00 was registered.
  • Since 1.6 (0837fa5), the monitor re-blocks by asking ShieldResolver whether endDate > now. Before that, it re-blocked any timedUnblock_ activity as soon as its interval ended.
  • When intervalDidEnd arrives inside that fraction of a second, the unblock still looks active. Nothing re-blocks until the heartbeat (:00 or :30), another callback, or opening Normal.

This affects unblocks of 20 minutes or more. 15-minute unblocks are registered 1 s longer to meet the minimum interval, so they always end after the saved end.

Fix

Round the saved end down to a whole second, the same way the end registered with iOS already is, so the saved end is never after the registered end. What is registered with iOS doesn't change. The start is unchanged, so blocks made just before an unblock still lose to it.

Down rather than up: the saved interval is never longer than the chosen duration, so the countdown never starts above it, and 15-minute unblocks keep their registered end 1 s after the saved end.

Tests

  • New: timedUnblockAllStartedMidSecondReblocksAtItsRegisteredEnd and groupTimedUnblockStartedMidSecondReblocksAtItsRegisteredEnd. Each starts an unblock at 10:00:00.6 and reconciles at the end registered with the fake DeviceActivityCenter. Both fail on main: apps stay unblocked.
  • customDurationSetsEndDateAndPersistsStartDate now allows the end to fall up to 1 s before start + duration.
  • Unit tests: 875/875 pass on the iOS 27.0 simulator.

Not verified

How often iOS actually delivers intervalDidEnd within the first second on a device. The callback log (Settings › Callback Log, debug builds) records each callback's time and shield change, so a 30-minute unblock on a debug build shows it.

The window registered with DeviceActivity drops the fraction of a
second from the end, but the stored end kept it. Since 1.6 the monitor
re-blocks by comparing the stored end to now, so when intervalDidEnd
arrived within that fraction the unblock still looked active and apps
stayed open until the next heartbeat (:00 or :30) or app launch.

Round the stored end down to a whole second, as the registered end
already is, so the stored end is never after the registered end.
@ykisana
ykisana force-pushed the fix/unblock-end-whole-second branch from c813deb to 011f054 Compare November 14, 2026 21:23
@ykisana
ykisana merged commit a112ec7 into main Nov 15, 2026
2 checks passed
@ykisana
ykisana deleted the fix/unblock-end-whole-second branch November 15, 2026 01:24
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