feat: enforce schedule rules in the background via DeviceActivity windows

Schedule (time-window) rules had no background enforcement: RuleScheduler.sync()
skipped them, so their shields were applied only by RuleEnforcer.refresh() — the
launch + 30s foreground loop. A window that began while the app was closed didn't
engage until the user reopened the app, which is why scheduled blocks could land
late or unevenly.

RuleScheduler now registers a repeating DeviceActivitySchedule per enabled
schedule rule's window (sched-<uuid>, plus sched2-<uuid> for windows that cross
midnight, since DeviceActivity can't express an interval whose end precedes its
start). The monitor extension routes these to a new ScheduleEnforcement.reconcile(),
which recomputes the rule's live schedule state from its snapshot
(RuleSchedule.isActive, honouring days, pause and the midnight-crossing rule) and
applies or clears the shield to match — the same logic RuleEnforcer.refresh runs
in the foreground, kept as the reconciliation safety net because interval callbacks
are known to fire late or not at all.

- RuleSnapshot gains startMinutes/endMinutes, with a tolerant decoder so snapshots
  written before these fields still load instead of failing the whole batch and
  blinding the extensions until the app reopens.
- Window activities are fingerprinted on their interval alone, so changing days,
  mode or apps no longer needlessly restarts monitoring.

On-device verification of the background transition is still pending (the simulator
does not deliver DeviceActivity callbacks); covered by new unit tests across the
scheduler, the snapshot codec and the new enforcement reactions.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-13 02:36:09 -04:00
parent 443b37c5e1
commit 05e0ee2755
8 changed files with 577 additions and 38 deletions

View File

@@ -140,9 +140,14 @@ Gotchas learned the hard way:
limits accrue in the Usage section and block at the budget; open-limit
apps shield immediately with an "Open (N left)" button; an open lasts
~15 minutes (DeviceActivity's minimum interval) before re-shielding.
- **Schedule-rule background transitions** still rely on the app running
(launch / foreground 30s loop); schedule rules have no DeviceActivity
monitoring yet — only limit rules do.
- **Schedule-rule background transitions** are now backed by DeviceActivity:
`RuleScheduler` registers a repeating window activity per schedule rule
(`sched-<uuid>`, plus `sched2-<uuid>` for midnight-crossing windows) and the
monitor extension recomputes + applies/clears the shield on interval
start/end. The foreground 30s loop remains as the reconciliation safety net
because interval callbacks are unreliable. On-device verification of the
background transition is still pending (the simulator does not deliver
DeviceActivity callbacks).
- `FamilyActivityPicker` shows few apps on the simulator; fine on device.
- `FamilyActivityPicker` **silently ignores selections** (binding never
updates, rows still show checkmarks) unless real FamilyControls