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.
63 lines
2.6 KiB
Swift
63 lines
2.6 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()
|
|
)
|
|
}
|
|
|
|
private var scheduleEnforcement: ScheduleEnforcement {
|
|
ScheduleEnforcement(
|
|
snapshots: RuleSnapshotStore(),
|
|
shields: ManagedSettingsShieldController()
|
|
)
|
|
}
|
|
|
|
override func intervalDidStart(for activity: DeviceActivityName) {
|
|
super.intervalDidStart(for: activity)
|
|
if let ruleID = MonitoringPlan.ruleID(fromDailyActivityName: activity.rawValue) {
|
|
enforcement.handleDayStart(ruleID: ruleID)
|
|
} else if let ruleID = MonitoringPlan.ruleID(fromScheduleWindowName: activity.rawValue) {
|
|
// A schedule window opened: shield it (the recompute honours days,
|
|
// pause and the midnight-crossing rule).
|
|
scheduleEnforcement.reconcile(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])
|
|
} else if let ruleID = MonitoringPlan.ruleID(fromScheduleWindowName: activity.rawValue) {
|
|
// A schedule window closed (or its evening half ended at 23:59):
|
|
// recompute so a still-active window stays shielded and a finished
|
|
// one clears.
|
|
scheduleEnforcement.reconcile(ruleID: ruleID)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|