jsdw_ios/AppRestrictShared/AppRestrictShared.swift

265 lines
8.9 KiB
Swift

//
// 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<ApplicationToken>) -> Data? {
let datas = tokens.compactMap { encode($0) }
return try? PropertyListEncoder().encode(datas)
}
static func decodeTokenSet(_ data: Data) -> Set<ApplicationToken> {
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<ApplicationToken> {
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<ApplicationToken>) {
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))
}
}
}