/* * Atoll (DynamicIsland) * Copyright (C) 2024-2026 Atoll Contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import AppKit import Combine import Foundation enum FullDiskAccessAuthorization { private static let probeURLs: [URL] = [ FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Application Support/com.apple.TCC/TCC.db"), FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/DoNotDisturb/DB/ModeConfigurations.json") ] static func hasPermission() -> Bool { for url in probeURLs { if canReadProtectedResource(at: url) { return true } } return false } private static func canReadProtectedResource(at url: URL) -> Bool { var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory), !isDirectory.boolValue else { return false } do { let handle = try FileHandle(forReadingFrom: url) try handle.close() return true } catch { return false } } } @MainActor final class FullDiskAccessPermissionStore: ObservableObject { static let shared = FullDiskAccessPermissionStore() @Published private(set) var isAuthorized: Bool = FullDiskAccessAuthorization.hasPermission() private var pollingTask: Task? private init() {} deinit { pollingTask?.cancel() } func refreshStatus() { updateAuthorizationStatus(to: FullDiskAccessAuthorization.hasPermission()) } func requestAccessPrompt() { #if os(macOS) let alert = NSAlert() alert.messageText = "Full Disk Access Required" alert.informativeText = "Dynamic Island needs Full Disk Access to detect custom Focus indicators and power the Shelf. Click Continue to open Full Disk Access settings, then press the + button and select Dynamic Island (we'll reveal it in Finder for you)." alert.alertStyle = .informational alert.addButton(withTitle: "Continue") alert.addButton(withTitle: "Cancel") if alert.runModal() == .alertFirstButtonReturn { openSystemSettings() revealAppBundleInFinder() } #endif beginPollingForStatusChanges() } func openSystemSettings() { #if os(macOS) if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles") { NSWorkspace.shared.open(url) } #endif } private func revealAppBundleInFinder() { #if os(macOS) let bundleURL = Bundle.main.bundleURL NSWorkspace.shared.activateFileViewerSelecting([bundleURL]) #endif } private func beginPollingForStatusChanges() { pollingTask?.cancel() pollingTask = Task { [weak self] in guard let self else { return } for _ in 0..<40 { try? await Task.sleep(nanoseconds: 500_000_000) let status = FullDiskAccessAuthorization.hasPermission() await MainActor.run { self.updateAuthorizationStatus(to: status) } if status { break } } } } private func updateAuthorizationStatus(to newValue: Bool) { guard newValue != isAuthorized else { return } isAuthorized = newValue } }