// // AppRestrictManager.swift // QuickLocation // import DeviceActivity import FamilyControls import Foundation import ManagedSettings import RxSwift import SwiftyUserDefaults import UIKit @available(iOS 16.0, *) final class AppRestrictManager: NSObject { static let shared = AppRestrictManager() private let center = AuthorizationCenter.shared private let activityCenter = DeviceActivityCenter() private let activityName = DeviceActivityName(AppRestrictShared.activityName) private let permissionConfirmationDelay: TimeInterval = 1 private var isPermissionMonitoringStarted = false private var permissionCheckGeneration = 0 private var permissionCheckWorkItem: DispatchWorkItem? private var isDeletingAllApps = false private var deleteAllDisposable: Disposable? private override init() { super.init() } var authorizationStatus: AuthorizationStatus { center.authorizationStatus } var isAuthorized: Bool { center.authorizationStatus == .approved } var selection: FamilyActivitySelection { get { let stored = AppRestrictSharedStore.selection let normalized = applicationOnlySelection(stored) if !stored.categoryTokens.isEmpty || !stored.webDomainTokens.isEmpty { AppRestrictSharedStore.selection = normalized } return normalized } set { let normalized = applicationOnlySelection(newValue) AppRestrictSharedStore.selection = normalized // Drop enabled tokens that are no longer in selection let apps = normalized.applicationTokens AppRestrictSharedStore.enabledTokens = AppRestrictSharedStore.enabledTokens.intersection(apps) recordPairingOwnerIfNeeded() refreshMonitoringAndShield() } } var applicationTokens: [ApplicationToken] { Array(selection.applicationTokens) } var enabledTokens: Set { get { AppRestrictSharedStore.enabledTokens } set { AppRestrictSharedStore.enabledTokens = newValue refreshMonitoringAndShield() } } func requestAuthorization() async throws { defer { synchronizePermissionState() } try await center.requestAuthorization(for: .individual) } func startPermissionMonitoring() { guard !isPermissionMonitoringStarted else { return } isPermissionMonitoringStarted = true NotificationCenter.default.addObserver( self, selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(currentAccountDidChange), name: .RefreshUserConfigNotification, object: nil ) NotificationCenter.default.addObserver( self, selector: #selector(protectedDataDidBecomeAvailable), name: UIApplication.protectedDataDidBecomeAvailableNotification, object: nil ) } /// 登录/恢复会话后立刻检查屏幕使用时间权限;已关闭则删除服务端配对。 func checkScreenTimePermissionAfterLogin() { guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in self?.checkScreenTimePermissionAfterLogin() } return } let currentUserId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !currentUserId.isEmpty else { return } if center.authorizationStatus == .approved { var approvedUserIds = Set(Defaults[\.appRestrictApprovedUserIds]) approvedUserIds.insert(currentUserId) Defaults[\.appRestrictApprovedUserIds] = Array(approvedUserIds) if AppRestrictSharedStore.hasPairingData, Defaults[\.appRestrictPairingOwnerUserId].isEmpty { Defaults[\.appRestrictPairingOwnerUserId] = currentUserId } retryPendingDeleteIfNeeded(for: currentUserId) return } addPendingDeleteUserId(currentUserId) clearLocalPairingData() retryPendingDeleteIfNeeded(for: currentUserId) } func mergeSelection(_ incoming: FamilyActivitySelection) { var current = selection current.applicationTokens.formUnion(incoming.applicationTokens) selection = current } private func applicationOnlySelection(_ source: FamilyActivitySelection) -> FamilyActivitySelection { var result = FamilyActivitySelection(includeEntireCategory: true) result.applicationTokens = source.applicationTokens return result } func isEnabled(_ token: ApplicationToken) -> Bool { enabledTokens.contains(token) } func setEnabled(_ token: ApplicationToken, enabled: Bool) { var set = enabledTokens if enabled { set.insert(token) } else { set.remove(token) } enabledTokens = set } func catalogId(for token: ApplicationToken) -> String? { AppRestrictSharedStore.catalogId(for: token) } func link(catalogId: String, token: ApplicationToken, displayName: String? = nil, iconURL: String? = nil) { AppRestrictSharedStore.setLink( catalogId: catalogId, token: token, displayName: displayName, iconURL: iconURL ) recordPairingOwnerIfNeeded() } func catalogItem(for token: ApplicationToken) -> AppCatalogItem? { guard let record = AppRestrictSharedStore.linkRecord(for: token) else { return nil } return AppCatalogStore.resolve(link: record) } func unlink(_ token: ApplicationToken) { AppRestrictSharedStore.removeLink(for: token) } func removeApplication(_ token: ApplicationToken) { unlink(token) var set = enabledTokens set.remove(token) enabledTokens = set var current = selection current.applicationTokens.remove(token) selection = current if !AppRestrictSharedStore.hasPairingData { Defaults[\.appRestrictPairingOwnerUserId] = "" } notifyPairingDataChanged() } var shieldConfig: AppRestrictShieldConfig { get { AppRestrictSharedStore.shieldConfig } set { AppRestrictSharedStore.shieldConfig = newValue } } @discardableResult func saveCustomShieldImage(_ image: UIImage) -> Bool { AppRestrictSharedStore.saveCustomImage(image) } func applyRemoteLock(tokens: [String], iconIndex: Int, message: String, groupName: String) { applyRemoteLockAppearance(iconIndex: iconIndex, message: message, groupName: groupName) guard isAuthorized else { print("[AppRestrict] remote lock ignored: Screen Time authorization is not approved") return } let incomingTokens = decodedRemoteTokens(tokens) guard !incomingTokens.isEmpty else { print("[AppRestrict] remote lock ignored: no decodable application tokens") return } enabledTokens = enabledTokens.union(incomingTokens) recordPairingOwnerIfNeeded() print("[AppRestrict] remote lock applied: \(incomingTokens.count) application(s)") } func applyRemoteUnlock(tokens: [String]) { if tokens.isEmpty { enabledTokens = [] return } let incomingTokens = decodedRemoteTokens(tokens) guard !incomingTokens.isEmpty else { print("[AppRestrict] remote unlock ignored: no decodable application tokens") return } enabledTokens = enabledTokens.subtracting(incomingTokens) } private func decodedRemoteTokens(_ values: [String]) -> Set { Set(values.compactMap(AppRestrictTokenCodec.decodeBase64)) } func applyRemoteLockAppearance(iconIndex: Int, message: String, groupName: String) { let name = groupName.trimmingCharacters(in: .whitespacesAndNewlines) let displayName = name.isEmpty ? "圈子" : name var config = shieldConfig config.title = "APP已被 \(displayName) 锁定" config.subtitle = message config.primaryButtonLabel = "打开 极速定位 解锁" config.imageSource = .album let index = min(max(iconIndex, 0), 6) + 1 if let image = UIImage(named: "LockDistract/lock_icon_\(index)") { _ = saveCustomShieldImage(image) } shieldConfig = config } func refreshMonitoringAndShield() { let tokens = enabledTokens AppRestrictSharedStore.applyShield(for: tokens) activityCenter.stopMonitoring([activityName]) guard !tokens.isEmpty else { return } // Near-daily schedule so Monitor can re-apply after reboot / schedule boundaries. let schedule = DeviceActivitySchedule( intervalStart: DateComponents(hour: 0, minute: 0), intervalEnd: DateComponents(hour: 23, minute: 59), repeats: true ) do { try activityCenter.startMonitoring(activityName, during: schedule) } catch { // Shield already applied above; monitoring is best-effort. print("[AppRestrict] startMonitoring failed: \(error)") } } @objc private func applicationDidBecomeActive() { synchronizePermissionState() } @objc private func currentAccountDidChange() { synchronizePermissionState() } @objc private func protectedDataDidBecomeAvailable() { synchronizePermissionState() } private func synchronizePermissionState() { guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in self?.synchronizePermissionState() } return } guard UIApplication.shared.applicationState == .active, UIApplication.shared.isProtectedDataAvailable else { return } let currentUserId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) var approvedUserIds = Set(Defaults[\.appRestrictApprovedUserIds]) if center.authorizationStatus == .approved { cancelPendingPermissionCheck() if !currentUserId.isEmpty { approvedUserIds.insert(currentUserId) Defaults[\.appRestrictApprovedUserIds] = Array(approvedUserIds) if AppRestrictSharedStore.hasPairingData, Defaults[\.appRestrictPairingOwnerUserId].isEmpty { Defaults[\.appRestrictPairingOwnerUserId] = currentUserId } } retryPendingDeleteIfNeeded(for: currentUserId) return } retryPendingDeleteIfNeeded(for: currentUserId) schedulePermissionConfirmation() } private func schedulePermissionConfirmation() { permissionCheckWorkItem?.cancel() permissionCheckGeneration += 1 let generation = permissionCheckGeneration let workItem = DispatchWorkItem { [weak self] in guard let self, self.permissionCheckGeneration == generation else { return } self.permissionCheckWorkItem = nil self.confirmNonApprovedPermissionState() } permissionCheckWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + permissionConfirmationDelay, execute: workItem) } private func cancelPendingPermissionCheck() { permissionCheckWorkItem?.cancel() permissionCheckWorkItem = nil permissionCheckGeneration += 1 } private func confirmNonApprovedPermissionState() { guard UIApplication.shared.applicationState == .active, UIApplication.shared.isProtectedDataAvailable else { return } guard center.authorizationStatus != .approved else { synchronizePermissionState() return } let currentUserId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) var approvedUserIds = Set(Defaults[\.appRestrictApprovedUserIds]) let pairingOwnerUserId = Defaults[\.appRestrictPairingOwnerUserId] .trimmingCharacters(in: .whitespacesAndNewlines) let hasPairingData = AppRestrictSharedStore.hasPairingData let cleanupUserId: String if hasPairingData, !pairingOwnerUserId.isEmpty { cleanupUserId = pairingOwnerUserId } else if !currentUserId.isEmpty, hasPairingData || approvedUserIds.contains(currentUserId) { cleanupUserId = currentUserId } else if hasPairingData, approvedUserIds.count == 1 { cleanupUserId = approvedUserIds.first ?? "" } else if !pairingOwnerUserId.isEmpty, approvedUserIds.contains(pairingOwnerUserId) { cleanupUserId = pairingOwnerUserId } else { cleanupUserId = "" } let permissionWasApproved = !cleanupUserId.isEmpty && approvedUserIds.contains(cleanupUserId) if hasPairingData || permissionWasApproved { print("[AppRestrict] permission confirmed unavailable; clearing local pairing data") if !cleanupUserId.isEmpty { approvedUserIds.remove(cleanupUserId) Defaults[\.appRestrictApprovedUserIds] = Array(approvedUserIds) addPendingDeleteUserId(cleanupUserId) } clearLocalPairingData() } retryPendingDeleteIfNeeded(for: currentUserId) } private func recordPairingOwnerIfNeeded() { guard AppRestrictSharedStore.hasPairingData, Defaults[\.appRestrictPairingOwnerUserId].isEmpty else { return } let userId = AppContextManager.shared.userId.trimmingCharacters(in: .whitespacesAndNewlines) guard !userId.isEmpty else { return } Defaults[\.appRestrictPairingOwnerUserId] = userId } private func clearLocalPairingData() { activityCenter.stopMonitoring([activityName]) AppRestrictSharedStore.clearPairingData() PhoneLockSession.currentLocks = [] Defaults[\.appRestrictPairingOwnerUserId] = "" notifyPairingDataChanged() } private func notifyPairingDataChanged() { NotificationCenter.default.post(name: .appRestrictPairingDataDidChange, object: nil) } private func addPendingDeleteUserId(_ userId: String) { var pendingUserIds = Set(Defaults[\.appRestrictPendingDeleteUserIds]) pendingUserIds.insert(userId) Defaults[\.appRestrictPendingDeleteUserIds] = Array(pendingUserIds) } private func retryPendingDeleteIfNeeded(for userId: String) { guard !userId.isEmpty, !isDeletingAllApps, Defaults[\.appRestrictPendingDeleteUserIds].contains(userId) else { return } isDeletingAllApps = true deleteAllDisposable = UserService.phoneLockAppsDelete(tokens: []) .observe(on: MainScheduler.instance) .subscribe(onNext: { [weak self] response in guard let self else { return } if response.code == "0" { var pendingUserIds = Set(Defaults[\.appRestrictPendingDeleteUserIds]) pendingUserIds.remove(userId) Defaults[\.appRestrictPendingDeleteUserIds] = Array(pendingUserIds) } else { print("[AppRestrict] delete all paired apps failed: \(response.message ?? "unknown error")") } self.finishDeleteAllRequest() }, onError: { [weak self] error in print("[AppRestrict] delete all paired apps failed: \(error.gatewayMessage ?? error.localizedDescription)") self?.finishDeleteAllRequest() }) } private func finishDeleteAllRequest() { isDeletingAllApps = false deleteAllDisposable = nil } }