feat: enforce time and open limits in the background via Screen Time extensions

- Shared/ layer compiled into the app and three new extension targets:
  rule snapshots in the app group, the usage ledger, monitoring-plan
  naming, and LimitEnforcement (shared, unit-tested event reactions)
- RuleScheduler mirrors rules to the app group and reconciles
  DeviceActivity monitoring: one daily 00:00-23:59 activity per limit
  rule, with a cumulative usage-threshold event per budget minute for
  time limits; activities restart only when their configuration changes
  (a restart resets threshold accounting)
- OpenAppLockMonitor (DeviceActivityMonitor): midnight budget resets,
  records usage minutes, shields at the budget, re-shields when a
  granted open session ends
- OpenAppLockShieldConfig: open-limit shields show 'Opened X of N times
  today' with an 'Open (Y left)' secondary button
- OpenAppLockShieldAction: an Open press spends one open, lifts the
  rule's shield, and starts the ~15-minute one-shot session
- extensions are classic NSExtension app extensions (the ExtensionKit
  product type expects an @main entry and made the app fail to install);
  shield-store tracking moved to app-group defaults so the app and
  extensions see one consistent set
- maps iOS 26's new .approvedWithDataAccess authorization status to
  approved (it previously fell through @unknown default to notDetermined)
- shares the OpenAppLock scheme (Xcode dropped the autocreated one when
  targets were added)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 21:06:08 -04:00
parent df6b7b689d
commit 443b37c5e1
29 changed files with 1686 additions and 24 deletions

View File

@@ -10,18 +10,32 @@ feature set is a clone of Opal's "Rules"; the presentation is bare native iOS
```
OpenAppLock/ App target (iOS 26, SwiftUI + SwiftData)
Models/ BlockingRule (@Model), RuleDraft, RuleKind,
Weekday, RulePreset
Models/ BlockingRule + AppList (@Model), RuleDraft,
RulePreset
Logic/ Pure, heavily unit-tested:
RuleSchedule (window math, incl. overnight),
RuleStatus (derived status + labels),
RulePolicy (Hard Mode gating, unblock/pause)
RuleStatus (derived status + labels, usage-aware),
RulePolicy (Hard Mode gating, unblock/pause,
app-list lock), UsageDisplay (Usage-section text)
Services/ ScreenTimeAuthorization (FamilyControls behind a
protocol + mock), ShieldController (ManagedSettings
shields + adult-content web filter + mock),
RuleEnforcer (active rules → shields),
LaunchConfiguration + SampleRules (UI-test harness)
protocol + mock), RuleEnforcer (rules → shields),
RuleScheduler (rules → DeviceActivity monitoring),
AppListMigration, LaunchConfiguration +
SampleRules (UI-test harness)
Views/ Native SwiftUI screens (see docs spec §6)
Shared/ Compiled into the app AND all three extensions:
RuleKind, Weekday, RuleSchedule, AppGroup,
UsageLedger (per-day minutes/opens),
RuleSnapshot(+Store) (rule mirror in the app
group), MonitoringPlan (activity/event naming),
LimitEnforcement (shared event reactions),
ShieldController, ShieldLookup
OpenAppLockMonitor/ DeviceActivityMonitor extension: midnight resets,
usage-minute checkpoints → shield at the limit,
open-session expiry
OpenAppLockShieldConfig/ ShieldConfiguration extension: "Opened X of N" +
Open button on open-limit shields
OpenAppLockShieldAction/ ShieldAction extension: Open press spends an open,
lifts the shield, starts the ~15-min session
OpenAppLockTests/ Swift Testing unit suites (@MainActor — the app
target defaults to MainActor isolation)
OpenAppLockUITests/ XCUITest flows (see harness below)
@@ -119,12 +133,16 @@ Gotchas learned the hard way:
## Known gaps / next steps
- **No DeviceActivityMonitor extension yet**: shields apply/clear only while
the app runs (launch, foreground 30s loop). Background window transitions,
and any real enforcement for Time Limit / Open Limit rules (usage
thresholds), require adding the extension target + app group.
- Time Limit / Open Limit rules are fully modeled, editable, and displayed,
but not enforced (see above).
- **On-device verification of limit enforcement is pending.** The
DeviceActivity monitor + shield extensions and the app group are in place,
but real blocking/usage tracking is only observable on a device (the
simulator neither tracks usage nor renders custom shields). Verify: time
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.
- `FamilyActivityPicker` shows few apps on the simulator; fine on device.
- `FamilyActivityPicker` **silently ignores selections** (binding never
updates, rows still show checkmarks) unless real FamilyControls
@@ -134,4 +152,6 @@ Gotchas learned the hard way:
`-ui-testing`, complete onboarding, and approve the system Screen Time
prompts ("Allow with Passcode" works on the simulator).
- Distribution (App Store) requires Apple's approval for the Family Controls
entitlement; development builds work with the dev entitlement.
entitlement **for the app and each extension bundle ID**
(`dev.bchen.OpenAppLock`, `.Monitor`, `.ShieldConfig`, `.ShieldAction`);
development builds work with the dev entitlement.