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:
44
Shared/ShieldLookup.swift
Normal file
44
Shared/ShieldLookup.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// ShieldLookup.swift
|
||||
// OpenAppLock
|
||||
//
|
||||
|
||||
import FamilyControls
|
||||
import Foundation
|
||||
import ManagedSettings
|
||||
|
||||
/// Matches shielded tokens back to the rule that shields them, so the shield
|
||||
/// UI can offer "Open" with the right counts for open-limit rules.
|
||||
enum ShieldLookup {
|
||||
static func openLimitSnapshot(
|
||||
containingApplication token: ApplicationToken, in snapshots: [RuleSnapshot]
|
||||
) -> RuleSnapshot? {
|
||||
snapshots.first { snapshot in
|
||||
guard snapshot.kind == .openLimit, snapshot.isEnabled else { return false }
|
||||
return AppSelectionCodec.decode(snapshot.selectionData)
|
||||
.applicationTokens.contains(token)
|
||||
}
|
||||
}
|
||||
|
||||
static func openLimitSnapshot(
|
||||
containingCategory token: ActivityCategoryToken, in snapshots: [RuleSnapshot]
|
||||
) -> RuleSnapshot? {
|
||||
snapshots.first { snapshot in
|
||||
guard snapshot.kind == .openLimit, snapshot.isEnabled else { return false }
|
||||
return AppSelectionCodec.decode(snapshot.selectionData)
|
||||
.categoryTokens.contains(token)
|
||||
}
|
||||
}
|
||||
|
||||
/// Any enabled rule whose selection includes the application (for naming
|
||||
/// plain blocked shields).
|
||||
static func snapshot(
|
||||
containingApplication token: ApplicationToken, in snapshots: [RuleSnapshot]
|
||||
) -> RuleSnapshot? {
|
||||
snapshots.first { snapshot in
|
||||
guard snapshot.isEnabled else { return false }
|
||||
return AppSelectionCodec.decode(snapshot.selectionData)
|
||||
.applicationTokens.contains(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user