- 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)
69 lines
2.6 KiB
Swift
69 lines
2.6 KiB
Swift
//
|
|
// RuleEnforcer.swift
|
|
// OpenAppLock
|
|
//
|
|
|
|
import Foundation
|
|
import Observation
|
|
|
|
/// Turns the current set of rules into shield state: schedule rules with an
|
|
/// active, un-paused window are shielded, and limit rules whose daily budget
|
|
/// is spent (per the usage ledger) are shielded until midnight; everything
|
|
/// else is cleared.
|
|
///
|
|
/// Background transitions (and usage tracking itself) belong to the
|
|
/// DeviceActivity monitor extension; this keeps shields correct while the
|
|
/// app runs.
|
|
@Observable
|
|
final class RuleEnforcer {
|
|
private(set) var blockingRuleIDs: Set<UUID> = []
|
|
private let shields: ShieldApplying
|
|
/// Mirrors rules to the app group and keeps DeviceActivity monitoring in
|
|
/// step; nil in UI-test launches.
|
|
private let scheduler: RuleScheduler?
|
|
/// Day-usage source consulted for limit rules; also exposed to views for
|
|
/// the Usage section.
|
|
let usageReader: UsageReading
|
|
|
|
init(
|
|
shields: ShieldApplying, usage: UsageReading = UsageLedger(),
|
|
scheduler: RuleScheduler? = nil
|
|
) {
|
|
self.shields = shields
|
|
self.usageReader = usage
|
|
self.scheduler = scheduler
|
|
}
|
|
|
|
/// The day's usage for a rule (nil for schedule rules, which don't track).
|
|
func usage(
|
|
for rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
|
) -> RuleUsage? {
|
|
guard rule.kind != .schedule else { return nil }
|
|
return usageReader.usage(for: rule.id, onDayContaining: now, calendar: calendar)
|
|
}
|
|
|
|
/// Recomputes shields from scratch. Call on launch, on any rule change,
|
|
/// and periodically while the app is visible. Also expires stale pauses.
|
|
func refresh(rules: [BlockingRule], at now: Date = .now, calendar: Calendar = .current) {
|
|
var active: Set<UUID> = []
|
|
for rule in rules {
|
|
if let pausedUntil = rule.pausedUntil, pausedUntil <= now {
|
|
rule.pausedUntil = nil
|
|
}
|
|
let usage = usage(for: rule, at: now, calendar: calendar)
|
|
guard rule.status(at: now, calendar: calendar, usage: usage).isActive else { continue }
|
|
active.insert(rule.id)
|
|
shields.applyShield(
|
|
ruleID: rule.id,
|
|
selectionData: rule.appList?.selectionData,
|
|
// Allow Only is a schedule-rule concept; limit blocks always Block.
|
|
mode: rule.kind == .schedule ? rule.selectionMode : .block,
|
|
blockAdultContent: rule.blockAdultContent
|
|
)
|
|
}
|
|
shields.clearShields(except: active)
|
|
blockingRuleIDs = active
|
|
scheduler?.sync(rules: rules, at: now)
|
|
}
|
|
}
|