// // AppRestrictShared.swift // Shared between main app and Screen Time extensions. // import Foundation import FamilyControls import ManagedSettings import UIKit enum AppRestrictShared { static let appGroupId = "group.cn.zuomeng.jisuloca" static let storeName = ManagedSettingsStore.Name("appRestrict") static let activityName = "appRestrict.monitoring" static let selectionKey = "appRestrict.selection" static let enabledTokensKey = "appRestrict.enabledTokens" static let linksKey = "appRestrict.links" static let shieldConfigKey = "appRestrict.shieldConfig" static let customImageFileName = "shield_custom.jpg" static var defaults: UserDefaults { UserDefaults(suiteName: appGroupId) ?? .standard } static var containerURL: URL? { FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) } static var customImageURL: URL? { containerURL?.appendingPathComponent(customImageFileName) } static func makeStore() -> ManagedSettingsStore { ManagedSettingsStore(named: storeName) } } struct AppRestrictLinkRecord: Codable, Equatable { let catalogId: String let tokenData: Data let displayName: String? let iconURL: String? init(catalogId: String, tokenData: Data, displayName: String? = nil, iconURL: String? = nil) { self.catalogId = catalogId self.tokenData = tokenData self.displayName = displayName self.iconURL = iconURL } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) catalogId = try container.decode(String.self, forKey: .catalogId) tokenData = try container.decode(Data.self, forKey: .tokenData) displayName = try container.decodeIfPresent(String.self, forKey: .displayName) iconURL = try container.decodeIfPresent(String.self, forKey: .iconURL) } } struct AppRestrictShieldConfig: Codable, Equatable { enum ImageSource: String, Codable { case presetDefault case presetFocus case presetCalm case album } var title: String var subtitle: String var primaryButtonLabel: String var imageSource: ImageSource static let `default` = AppRestrictShieldConfig( title: "该应用已被锁定", subtitle: "专注当下,稍后再回来吧", primaryButtonLabel: "知道了", imageSource: .presetDefault ) } enum AppRestrictTokenCodec { static func encode(_ token: ApplicationToken) -> Data? { try? PropertyListEncoder().encode(token) } static func decode(_ data: Data) -> ApplicationToken? { try? PropertyListDecoder().decode(ApplicationToken.self, from: data) } static func encodeSelection(_ selection: FamilyActivitySelection) -> Data? { try? PropertyListEncoder().encode(selection) } static func decodeSelection(_ data: Data) -> FamilyActivitySelection? { try? PropertyListDecoder().decode(FamilyActivitySelection.self, from: data) } static func encodeTokenSet(_ tokens: Set) -> Data? { let datas = tokens.compactMap { encode($0) } return try? PropertyListEncoder().encode(datas) } static func decodeTokenSet(_ data: Data) -> Set { guard let datas = try? PropertyListDecoder().decode([Data].self, from: data) else { return [] } return Set(datas.compactMap { decode($0) }) } } enum AppRestrictSharedStore { static var selection: FamilyActivitySelection { get { guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.selectionKey), let value = AppRestrictTokenCodec.decodeSelection(data) else { return FamilyActivitySelection() } return value } set { if let data = AppRestrictTokenCodec.encodeSelection(newValue) { AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.selectionKey) } } } static var enabledTokens: Set { get { guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.enabledTokensKey) else { return [] } return AppRestrictTokenCodec.decodeTokenSet(data) } set { if let data = AppRestrictTokenCodec.encodeTokenSet(newValue) { AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.enabledTokensKey) } else { AppRestrictShared.defaults.removeObject(forKey: AppRestrictShared.enabledTokensKey) } } } static var links: [AppRestrictLinkRecord] { get { guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.linksKey), let value = try? JSONDecoder().decode([AppRestrictLinkRecord].self, from: data) else { return [] } return value } set { if let data = try? JSONEncoder().encode(newValue) { AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.linksKey) } } } static var shieldConfig: AppRestrictShieldConfig { get { guard let data = AppRestrictShared.defaults.data(forKey: AppRestrictShared.shieldConfigKey), let value = try? JSONDecoder().decode(AppRestrictShieldConfig.self, from: data) else { return .default } return value } set { if let data = try? JSONEncoder().encode(newValue) { AppRestrictShared.defaults.set(data, forKey: AppRestrictShared.shieldConfigKey) } } } static func linkRecord(for token: ApplicationToken) -> AppRestrictLinkRecord? { guard let data = AppRestrictTokenCodec.encode(token) else { return nil } return links.first(where: { $0.tokenData == data }) } static func catalogId(for token: ApplicationToken) -> String? { linkRecord(for: token)?.catalogId } static func setLink( catalogId: String, token: ApplicationToken, displayName: String? = nil, iconURL: String? = nil ) { guard let data = AppRestrictTokenCodec.encode(token) else { return } var list = links.filter { $0.tokenData != data && $0.catalogId != catalogId } list.append(AppRestrictLinkRecord( catalogId: catalogId, tokenData: data, displayName: displayName, iconURL: iconURL )) links = list } static func removeLink(for token: ApplicationToken) { guard let data = AppRestrictTokenCodec.encode(token) else { return } links = links.filter { $0.tokenData != data } } static func applyShield(for tokens: Set) { let store = AppRestrictShared.makeStore() if tokens.isEmpty { store.clearAllSettings() } else { store.shield.applications = tokens } } static func clearShield() { AppRestrictShared.makeStore().clearAllSettings() } static func loadShieldImage() -> UIImage? { let config = shieldConfig switch config.imageSource { case .album: if let url = AppRestrictShared.customImageURL, let data = try? Data(contentsOf: url), let image = UIImage(data: data) { return image } fallthrough case .presetDefault: return UIImage(named: "AppRestrict/shield_preset_default") ?? UIImage(systemName: "lock.shield.fill") case .presetFocus: return UIImage(named: "AppRestrict/shield_preset_focus") ?? UIImage(systemName: "brain.head.profile") case .presetCalm: return UIImage(named: "AppRestrict/shield_preset_calm") ?? UIImage(systemName: "leaf.fill") } } static func saveCustomImage(_ image: UIImage) -> Bool { guard let url = AppRestrictShared.customImageURL else { return false } let resized = image.appRestrictResized(maxSide: 720) guard let data = resized.jpegData(compressionQuality: 0.82) else { return false } do { try data.write(to: url, options: .atomic) return true } catch { return false } } } private extension UIImage { func appRestrictResized(maxSide: CGFloat) -> UIImage { let maxCurrent = max(size.width, size.height) guard maxCurrent > maxSide, maxCurrent > 0 else { return self } let scale = maxSide / maxCurrent let newSize = CGSize(width: size.width * scale, height: size.height * scale) let renderer = UIGraphicsImageRenderer(size: newSize) return renderer.image { _ in draw(in: CGRect(origin: .zero, size: newSize)) } } }