265 lines
8.9 KiB
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))
|
|
}
|
|
}
|
|
}
|