Files
OpenAppLock/OpenAppLockMonitor/DeviceActivityMonitorExtension.swift
Brendan Chen 1652c2f410 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)
2026-06-12 21:06:08 -04:00

47 lines
1.7 KiB
Swift

//
// DeviceActivityMonitorExtension.swift
// OpenAppLockMonitor
//
import DeviceActivity
import Foundation
/// Background half of limit-rule enforcement. The app schedules a daily
/// activity per limit rule (with per-minute usage checkpoints for time
/// limits); this extension reacts: it resets shields at midnight, records
/// usage minutes, blocks at the budget, and ends granted open sessions.
final class DeviceActivityMonitorExtension: DeviceActivityMonitor {
private var enforcement: LimitEnforcement {
LimitEnforcement(
snapshots: RuleSnapshotStore(),
ledger: UsageLedger(),
shields: ManagedSettingsShieldController()
)
}
override func intervalDidStart(for activity: DeviceActivityName) {
super.intervalDidStart(for: activity)
if let ruleID = MonitoringPlan.ruleID(fromDailyActivityName: activity.rawValue) {
enforcement.handleDayStart(ruleID: ruleID)
}
}
override func intervalDidEnd(for activity: DeviceActivityName) {
super.intervalDidEnd(for: activity)
if let ruleID = MonitoringPlan.ruleID(fromSessionActivityName: activity.rawValue) {
enforcement.handleOpenSessionEnded(ruleID: ruleID)
DeviceActivityCenter().stopMonitoring([activity])
}
}
override func eventDidReachThreshold(
_ event: DeviceActivityEvent.Name, activity: DeviceActivityName
) {
super.eventDidReachThreshold(event, activity: activity)
guard let ruleID = MonitoringPlan.ruleID(fromDailyActivityName: activity.rawValue),
let minutes = MonitoringPlan.minutes(fromEventName: event.rawValue)
else { return }
enforcement.handleUsageMinutes(minutes, ruleID: ruleID)
}
}