- 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>
62 lines
2.2 KiB
Swift
62 lines
2.2 KiB
Swift
//
|
|
// MonitoringPlan.swift
|
|
// OpenAppLock
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Naming conventions and event layouts shared by the app (which starts
|
|
/// DeviceActivity monitoring) and the monitor extension (which decodes what
|
|
/// fired).
|
|
enum MonitoringPlan {
|
|
private static let dailyPrefix = "rule-"
|
|
private static let sessionPrefix = "open-session-"
|
|
private static let minutePrefix = "minutes-"
|
|
|
|
/// Wall-clock length of one granted open. 15 minutes is DeviceActivity's
|
|
/// minimum schedule interval, so an "open" lasts at most this long before
|
|
/// the shield returns.
|
|
static let openSessionMinutes = 15
|
|
|
|
/// The always-on, midnight-to-midnight activity tracking a rule's day.
|
|
static func dailyActivityName(for ruleID: UUID) -> String {
|
|
dailyPrefix + ruleID.uuidString
|
|
}
|
|
|
|
/// The one-shot activity timing a granted open.
|
|
static func sessionActivityName(for ruleID: UUID) -> String {
|
|
sessionPrefix + ruleID.uuidString
|
|
}
|
|
|
|
static func ruleID(fromDailyActivityName name: String) -> UUID? {
|
|
guard name.hasPrefix(dailyPrefix) else { return nil }
|
|
return UUID(uuidString: String(name.dropFirst(dailyPrefix.count)))
|
|
}
|
|
|
|
static func ruleID(fromSessionActivityName name: String) -> UUID? {
|
|
guard name.hasPrefix(sessionPrefix) else { return nil }
|
|
return UUID(uuidString: String(name.dropFirst(sessionPrefix.count)))
|
|
}
|
|
|
|
static func minuteEventName(for minutes: Int) -> String {
|
|
minutePrefix + String(minutes)
|
|
}
|
|
|
|
static func minutes(fromEventName name: String) -> Int? {
|
|
guard name.hasPrefix(minutePrefix) else { return nil }
|
|
return Int(name.dropFirst(minutePrefix.count))
|
|
}
|
|
|
|
/// Cumulative-usage checkpoints for a time-limit rule: one event per
|
|
/// minute up to the budget so remaining time can be displayed live; the
|
|
/// final one doubles as the block trigger. (Budgets cap at 240 minutes,
|
|
/// comfortably inside DeviceActivity's event capacity.)
|
|
static func minuteEvents(forLimit limitMinutes: Int) -> [String: Int] {
|
|
Dictionary(
|
|
uniqueKeysWithValues: (1...max(1, limitMinutes)).map {
|
|
(minuteEventName(for: $0), $0)
|
|
}
|
|
)
|
|
}
|
|
}
|