- 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)
86 lines
3.5 KiB
Swift
86 lines
3.5 KiB
Swift
//
|
|
// ShieldConfigurationExtension.swift
|
|
// OpenAppLockShieldConfig
|
|
//
|
|
|
|
import ManagedSettings
|
|
import ManagedSettingsUI
|
|
import UIKit
|
|
|
|
/// Customizes the system shield. Apps under an open-limit rule show how many
|
|
/// opens were used and offer an "Open" secondary button while opens remain;
|
|
/// everything else gets a plain blocked shield named after its rule.
|
|
final class ShieldConfigurationExtension: ShieldConfigurationDataSource {
|
|
override func configuration(shielding application: Application) -> ShieldConfiguration {
|
|
guard let token = application.token else { return blockedConfiguration(ruleName: nil) }
|
|
return configuration(forApplicationToken: token)
|
|
}
|
|
|
|
override func configuration(
|
|
shielding application: Application, in category: ActivityCategory
|
|
) -> ShieldConfiguration {
|
|
guard let token = application.token else { return blockedConfiguration(ruleName: nil) }
|
|
return configuration(forApplicationToken: token)
|
|
}
|
|
|
|
override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration {
|
|
blockedConfiguration(ruleName: nil)
|
|
}
|
|
|
|
override func configuration(
|
|
shielding webDomain: WebDomain, in category: ActivityCategory
|
|
) -> ShieldConfiguration {
|
|
blockedConfiguration(ruleName: nil)
|
|
}
|
|
|
|
private func configuration(
|
|
forApplicationToken token: ApplicationToken
|
|
) -> ShieldConfiguration {
|
|
let snapshots = RuleSnapshotStore().load()
|
|
guard
|
|
let snapshot = ShieldLookup.openLimitSnapshot(
|
|
containingApplication: token, in: snapshots)
|
|
else {
|
|
let name = ShieldLookup.snapshot(containingApplication: token, in: snapshots)?.name
|
|
return blockedConfiguration(ruleName: name)
|
|
}
|
|
|
|
let usage = UsageLedger().usage(for: snapshot.id, onDayContaining: .now)
|
|
let remaining = max(0, snapshot.maxOpens - usage.opensUsed)
|
|
guard remaining > 0 else {
|
|
return ShieldConfiguration(
|
|
backgroundBlurStyle: .systemMaterial,
|
|
title: .init(text: snapshot.name, color: .label),
|
|
subtitle: .init(
|
|
text: "No opens left today — the block lifts tomorrow.",
|
|
color: .secondaryLabel),
|
|
primaryButtonLabel: .init(text: "OK", color: .white),
|
|
primaryButtonBackgroundColor: .systemBlue
|
|
)
|
|
}
|
|
return ShieldConfiguration(
|
|
backgroundBlurStyle: .systemMaterial,
|
|
title: .init(text: snapshot.name, color: .label),
|
|
subtitle: .init(
|
|
text: "Opened \(usage.opensUsed) of \(snapshot.maxOpens) times today. "
|
|
+ "Each open lasts \(MonitoringPlan.openSessionMinutes) minutes.",
|
|
color: .secondaryLabel),
|
|
primaryButtonLabel: .init(text: "OK", color: .white),
|
|
primaryButtonBackgroundColor: .systemBlue,
|
|
secondaryButtonLabel: .init(
|
|
text: remaining == 1 ? "Open (1 left)" : "Open (\(remaining) left)",
|
|
color: .systemBlue)
|
|
)
|
|
}
|
|
|
|
private func blockedConfiguration(ruleName: String?) -> ShieldConfiguration {
|
|
ShieldConfiguration(
|
|
backgroundBlurStyle: .systemMaterial,
|
|
title: .init(text: ruleName ?? "Blocked", color: .label),
|
|
subtitle: .init(text: "This app is blocked by OpenAppLock.", color: .secondaryLabel),
|
|
primaryButtonLabel: .init(text: "OK", color: .white),
|
|
primaryButtonBackgroundColor: .systemBlue
|
|
)
|
|
}
|
|
}
|