mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-28 15:44:32 +00:00
Working stuff
This commit is contained in:
parent
9bd445986d
commit
94a612f327
13 changed files with 913 additions and 150 deletions
|
|
@ -3,7 +3,7 @@ import Defaults
|
|||
import AtollExtensionKit
|
||||
import SwiftUI
|
||||
|
||||
struct ExtensionLiveActivityPayload: Identifiable, Hashable {
|
||||
struct ExtensionLiveActivityPayload: Identifiable, Hashable, Codable {
|
||||
let bundleIdentifier: String
|
||||
let descriptor: AtollLiveActivityDescriptor
|
||||
let receivedAt: Date
|
||||
|
|
@ -13,7 +13,7 @@ struct ExtensionLiveActivityPayload: Identifiable, Hashable {
|
|||
var priority: AtollLiveActivityPriority { descriptor.priority }
|
||||
}
|
||||
|
||||
struct ExtensionLockScreenWidgetPayload: Identifiable, Hashable {
|
||||
struct ExtensionLockScreenWidgetPayload: Identifiable, Hashable, Codable {
|
||||
let bundleIdentifier: String
|
||||
let descriptor: AtollLockScreenWidgetDescriptor
|
||||
let receivedAt: Date
|
||||
|
|
|
|||
147
DynamicIsland/managers/Extensions/ExtensionEventBridge.swift
Normal file
147
DynamicIsland/managers/Extensions/ExtensionEventBridge.swift
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import Foundation
|
||||
|
||||
/// Bridges extension payload updates between Atoll processes (main app, helpers, XPC services).
|
||||
/// Stores the latest snapshot on disk and delivers change notifications through DistributedNotificationCenter.
|
||||
final class ExtensionEventBridge {
|
||||
static let shared = ExtensionEventBridge()
|
||||
|
||||
private let notificationCenter = DistributedNotificationCenter.default()
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
private let processIdentifier = ProcessInfo.processInfo.processIdentifier
|
||||
private let ioQueue = DispatchQueue(label: "com.ebullioscopic.Atoll.extensions.bridge", qos: .utility)
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
func broadcastLiveActivitySnapshot(_ payloads: [ExtensionLiveActivityPayload]) {
|
||||
broadcast(payloads, channel: .liveActivities)
|
||||
}
|
||||
|
||||
func broadcastLockScreenWidgetSnapshot(_ payloads: [ExtensionLockScreenWidgetPayload]) {
|
||||
broadcast(payloads, channel: .lockScreenWidgets)
|
||||
}
|
||||
|
||||
func loadPersistedLiveActivities() -> [ExtensionLiveActivityPayload] {
|
||||
loadSnapshot(channel: .liveActivities)
|
||||
}
|
||||
|
||||
func loadPersistedLockScreenWidgets() -> [ExtensionLockScreenWidgetPayload] {
|
||||
loadSnapshot(channel: .lockScreenWidgets)
|
||||
}
|
||||
|
||||
func observeLiveActivitySnapshots(_ handler: @escaping ([ExtensionLiveActivityPayload], Int32) -> Void) -> NSObjectProtocol {
|
||||
observe(channel: .liveActivities, handler: handler)
|
||||
}
|
||||
|
||||
func observeLockScreenWidgetSnapshots(_ handler: @escaping ([ExtensionLockScreenWidgetPayload], Int32) -> Void) -> NSObjectProtocol {
|
||||
observe(channel: .lockScreenWidgets, handler: handler)
|
||||
}
|
||||
|
||||
func removeObserver(_ observer: NSObjectProtocol) {
|
||||
notificationCenter.removeObserver(observer)
|
||||
}
|
||||
|
||||
// MARK: - Snapshot Storage
|
||||
|
||||
private func broadcast<T: Codable>(_ payloads: [T], channel: Channel) {
|
||||
ioQueue.async { [weak self] in
|
||||
guard let self else { return }
|
||||
guard let data = try? self.encoder.encode(payloads) else { return }
|
||||
self.persistSnapshot(data, channel: channel)
|
||||
let userInfo: [String: Any] = [
|
||||
UserInfoKey.payloads: data,
|
||||
UserInfoKey.sourcePID: NSNumber(value: self.processIdentifier)
|
||||
]
|
||||
self.notificationCenter.postNotificationName(
|
||||
channel.notificationName,
|
||||
object: nil,
|
||||
userInfo: userInfo,
|
||||
deliverImmediately: true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSnapshot<T: Codable>(channel: Channel) -> [T] {
|
||||
let url = snapshotURL(for: channel)
|
||||
guard FileManager.default.fileExists(atPath: url.path),
|
||||
let data = try? Data(contentsOf: url) else {
|
||||
return []
|
||||
}
|
||||
return (try? decoder.decode([T].self, from: data)) ?? []
|
||||
}
|
||||
|
||||
private func persistSnapshot(_ data: Data, channel: Channel) {
|
||||
let url = snapshotURL(for: channel)
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(),
|
||||
withIntermediateDirectories: true)
|
||||
try data.write(to: url, options: .atomic)
|
||||
} catch {
|
||||
Logger.log("Failed to persist extension snapshot for \(channel): \(error.localizedDescription)", category: .extensions)
|
||||
}
|
||||
}
|
||||
|
||||
private func snapshotURL(for channel: Channel) -> URL {
|
||||
let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
|
||||
?? URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
let directory = appSupport.appendingPathComponent("AtollExtensions", isDirectory: true)
|
||||
switch channel {
|
||||
case .liveActivities:
|
||||
return directory.appendingPathComponent("live_activities.json", isDirectory: false)
|
||||
case .lockScreenWidgets:
|
||||
return directory.appendingPathComponent("lock_screen_widgets.json", isDirectory: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Observation
|
||||
|
||||
private func observe<T: Codable>(channel: Channel,
|
||||
handler: @escaping ([T], Int32) -> Void) -> NSObjectProtocol {
|
||||
notificationCenter.addObserver(forName: channel.notificationName, object: nil, queue: .main) { [weak self] notification in
|
||||
guard let self else { return }
|
||||
let sourcePID = (notification.userInfo?[UserInfoKey.sourcePID] as? NSNumber)?.int32Value ?? 0
|
||||
guard sourcePID != self.processIdentifier else { return }
|
||||
|
||||
if let data = notification.userInfo?[UserInfoKey.payloads] as? Data,
|
||||
let payloads = try? self.decoder.decode([T].self, from: data) {
|
||||
handler(payloads, sourcePID)
|
||||
return
|
||||
}
|
||||
|
||||
let snapshot: [T] = self.loadSnapshot(channel: channel)
|
||||
handler(snapshot, sourcePID)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private enum Channel: CustomStringConvertible {
|
||||
case liveActivities
|
||||
case lockScreenWidgets
|
||||
|
||||
var notificationName: Notification.Name {
|
||||
switch self {
|
||||
case .liveActivities:
|
||||
return Notification.Name("com.ebullioscopic.Atoll.extensions.liveActivitySnapshot")
|
||||
case .lockScreenWidgets:
|
||||
return Notification.Name("com.ebullioscopic.Atoll.extensions.lockScreenWidgetSnapshot")
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .liveActivities:
|
||||
return "live-activities"
|
||||
case .lockScreenWidgets:
|
||||
return "lock-screen-widgets"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum UserInfoKey {
|
||||
static let payloads = "payloads"
|
||||
static let sourcePID = "sourcePID"
|
||||
}
|
||||
}
|
||||
|
|
@ -10,21 +10,52 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
|||
|
||||
private let authorizationManager = ExtensionAuthorizationManager.shared
|
||||
private let maxCapacityKey = Defaults.Keys.extensionLiveActivityCapacity
|
||||
private let eventBridge = ExtensionEventBridge.shared
|
||||
private var liveActivityObserver: NSObjectProtocol?
|
||||
private var suppressBroadcast = false
|
||||
private let currentProcessID = ProcessInfo.processInfo.processIdentifier
|
||||
|
||||
private init() {}
|
||||
private init() {
|
||||
activeActivities = eventBridge.loadPersistedLiveActivities()
|
||||
sortActivities()
|
||||
liveActivityObserver = eventBridge.observeLiveActivitySnapshots { [weak self] payloads, sourcePID in
|
||||
self?.applySnapshot(payloads, sourcePID: sourcePID)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let token = liveActivityObserver {
|
||||
eventBridge.removeObserver(token)
|
||||
}
|
||||
}
|
||||
|
||||
func present(descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String) throws {
|
||||
guard authorizationManager.canProcessLiveActivityRequest(from: bundleIdentifier) else {
|
||||
logDiagnostics("Rejected live activity \(descriptor.id) from \(bundleIdentifier): scope disabled or bundle unauthorized")
|
||||
throw ExtensionValidationError.unauthorized
|
||||
}
|
||||
guard descriptor.isValid else {
|
||||
logDiagnostics("Rejected live activity \(descriptor.id) from \(bundleIdentifier): descriptor validation failed")
|
||||
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
|
||||
}
|
||||
guard activeActivities.count < Defaults[maxCapacityKey] else {
|
||||
throw ExtensionValidationError.exceedsCapacity
|
||||
|
||||
if let index = activeActivities.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) {
|
||||
let payload = ExtensionLiveActivityPayload(
|
||||
bundleIdentifier: bundleIdentifier,
|
||||
descriptor: descriptor,
|
||||
receivedAt: activeActivities[index].receivedAt
|
||||
)
|
||||
activeActivities[index] = payload
|
||||
sortActivities()
|
||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
||||
Logger.log("Replaced extension live activity \(descriptor.id) for \(bundleIdentifier)", category: .extensions)
|
||||
broadcastSnapshot()
|
||||
return
|
||||
}
|
||||
guard activeActivities.contains(where: { $0.descriptor.id == descriptor.id }) == false else {
|
||||
throw ExtensionValidationError.duplicateIdentifier
|
||||
|
||||
guard activeActivities.count < Defaults[maxCapacityKey] else {
|
||||
logDiagnostics("Rejected live activity \(descriptor.id) from \(bundleIdentifier): capacity limit \(Defaults[maxCapacityKey]) reached")
|
||||
throw ExtensionValidationError.exceedsCapacity
|
||||
}
|
||||
|
||||
let payload = ExtensionLiveActivityPayload(
|
||||
|
|
@ -35,10 +66,13 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
|||
activeActivities.append(payload)
|
||||
sortActivities()
|
||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
||||
logDiagnostics("Queued live activity \(descriptor.id) for \(bundleIdentifier); total activities: \(activeActivities.count)")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
|
||||
func update(descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String) throws {
|
||||
guard descriptor.isValid else {
|
||||
logDiagnostics("Rejected live activity update \(descriptor.id) from \(bundleIdentifier): descriptor validation failed")
|
||||
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
|
||||
}
|
||||
guard let index = activeActivities.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) else {
|
||||
|
|
@ -52,6 +86,8 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
|||
activeActivities[index] = payload
|
||||
sortActivities()
|
||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
||||
logDiagnostics("Updated live activity \(descriptor.id) for \(bundleIdentifier)")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
|
||||
func dismiss(activityID: String, bundleIdentifier: String) {
|
||||
|
|
@ -60,6 +96,8 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
|||
if previousCount != activeActivities.count {
|
||||
Logger.log("Dismissed extension live activity \(activityID) from \(bundleIdentifier)", category: .extensions)
|
||||
ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: activityID)
|
||||
logDiagnostics("Removed live activity \(activityID) for \(bundleIdentifier); remaining: \(activeActivities.count)")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,6 +107,10 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
|||
.map { $0.descriptor.id }
|
||||
activeActivities.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||
ids.forEach { ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) }
|
||||
if !ids.isEmpty {
|
||||
logDiagnostics("Removed all live activities for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
func sortedActivities(for coexistence: Bool = false) -> [ExtensionLiveActivityPayload] {
|
||||
|
|
@ -87,4 +129,23 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
|||
private func sortActivities() {
|
||||
activeActivities.sort(by: descriptorComparator)
|
||||
}
|
||||
|
||||
private func broadcastSnapshot() {
|
||||
guard !suppressBroadcast else { return }
|
||||
eventBridge.broadcastLiveActivitySnapshot(activeActivities)
|
||||
logDiagnostics("Broadcasted live activity snapshot (count: \(activeActivities.count))")
|
||||
}
|
||||
|
||||
private func applySnapshot(_ payloads: [ExtensionLiveActivityPayload], sourcePID: Int32) {
|
||||
guard sourcePID != currentProcessID else { return }
|
||||
suppressBroadcast = true
|
||||
activeActivities = payloads.sorted(by: descriptorComparator)
|
||||
suppressBroadcast = false
|
||||
logDiagnostics("Applied external live activity snapshot from PID \(sourcePID) (count: \(payloads.count))")
|
||||
}
|
||||
|
||||
private func logDiagnostics(_ message: String) {
|
||||
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
||||
Logger.log(message, category: .extensions)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,24 +11,54 @@ final class ExtensionLockScreenWidgetManager: ObservableObject {
|
|||
private let authorizationManager = ExtensionAuthorizationManager.shared
|
||||
private let maxCapacityKey = Defaults.Keys.extensionLockScreenWidgetCapacity
|
||||
private var presentationController: ExtensionLockScreenWidgetPresentationController!
|
||||
private let eventBridge = ExtensionEventBridge.shared
|
||||
private var widgetObserver: NSObjectProtocol?
|
||||
private var suppressBroadcast = false
|
||||
private let currentProcessID = ProcessInfo.processInfo.processIdentifier
|
||||
|
||||
private init() {
|
||||
activeWidgets = eventBridge.loadPersistedLockScreenWidgets()
|
||||
sortWidgets()
|
||||
presentationController = ExtensionLockScreenWidgetPresentationController(manager: self)
|
||||
presentationController.activate()
|
||||
widgetObserver = eventBridge.observeLockScreenWidgetSnapshots { [weak self] payloads, sourcePID in
|
||||
self?.applySnapshot(payloads, sourcePID: sourcePID)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let token = widgetObserver {
|
||||
eventBridge.removeObserver(token)
|
||||
}
|
||||
}
|
||||
|
||||
func present(descriptor: AtollLockScreenWidgetDescriptor, bundleIdentifier: String) throws {
|
||||
guard authorizationManager.canProcessLockScreenRequest(from: bundleIdentifier) else {
|
||||
logDiagnostics("Rejected lock screen widget \(descriptor.id) from \(bundleIdentifier): scope disabled or bundle unauthorized")
|
||||
throw ExtensionValidationError.unauthorized
|
||||
}
|
||||
guard descriptor.isValid else {
|
||||
logDiagnostics("Rejected lock screen widget \(descriptor.id) from \(bundleIdentifier): descriptor validation failed")
|
||||
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
|
||||
}
|
||||
guard activeWidgets.count < Defaults[maxCapacityKey] else {
|
||||
throw ExtensionValidationError.exceedsCapacity
|
||||
|
||||
if let index = activeWidgets.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) {
|
||||
let payload = ExtensionLockScreenWidgetPayload(
|
||||
bundleIdentifier: bundleIdentifier,
|
||||
descriptor: descriptor,
|
||||
receivedAt: activeWidgets[index].receivedAt
|
||||
)
|
||||
activeWidgets[index] = payload
|
||||
sortWidgets()
|
||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .lockScreenWidgets)
|
||||
Logger.log("Replaced extension widget \(descriptor.id) for \(bundleIdentifier)", category: .extensions)
|
||||
broadcastSnapshot()
|
||||
return
|
||||
}
|
||||
guard activeWidgets.contains(where: { $0.descriptor.id == descriptor.id }) == false else {
|
||||
throw ExtensionValidationError.duplicateIdentifier
|
||||
|
||||
guard activeWidgets.count < Defaults[maxCapacityKey] else {
|
||||
logDiagnostics("Rejected lock screen widget \(descriptor.id) from \(bundleIdentifier): capacity limit \(Defaults[maxCapacityKey]) reached")
|
||||
throw ExtensionValidationError.exceedsCapacity
|
||||
}
|
||||
|
||||
let payload = ExtensionLockScreenWidgetPayload(
|
||||
|
|
@ -39,10 +69,13 @@ final class ExtensionLockScreenWidgetManager: ObservableObject {
|
|||
activeWidgets.append(payload)
|
||||
sortWidgets()
|
||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .lockScreenWidgets)
|
||||
logDiagnostics("Queued lock screen widget \(descriptor.id) for \(bundleIdentifier); total widgets: \(activeWidgets.count)")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
|
||||
func update(descriptor: AtollLockScreenWidgetDescriptor, bundleIdentifier: String) throws {
|
||||
guard descriptor.isValid else {
|
||||
logDiagnostics("Rejected lock screen widget update \(descriptor.id) from \(bundleIdentifier): descriptor validation failed")
|
||||
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
|
||||
}
|
||||
guard let index = activeWidgets.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) else {
|
||||
|
|
@ -57,6 +90,8 @@ final class ExtensionLockScreenWidgetManager: ObservableObject {
|
|||
activeWidgets[index] = payload
|
||||
sortWidgets()
|
||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .lockScreenWidgets)
|
||||
logDiagnostics("Updated lock screen widget \(descriptor.id) for \(bundleIdentifier)")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
|
||||
func dismiss(widgetID: String, bundleIdentifier: String) {
|
||||
|
|
@ -65,6 +100,8 @@ final class ExtensionLockScreenWidgetManager: ObservableObject {
|
|||
if previousCount != activeWidgets.count {
|
||||
Logger.log("Dismissed extension widget \(widgetID) from \(bundleIdentifier)", category: .extensions)
|
||||
ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: widgetID)
|
||||
logDiagnostics("Removed lock screen widget \(widgetID) for \(bundleIdentifier); remaining: \(activeWidgets.count)")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +111,10 @@ final class ExtensionLockScreenWidgetManager: ObservableObject {
|
|||
.map { $0.descriptor.id }
|
||||
activeWidgets.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||
ids.forEach { ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: $0) }
|
||||
if !ids.isEmpty {
|
||||
logDiagnostics("Removed all lock screen widgets for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))")
|
||||
broadcastSnapshot()
|
||||
}
|
||||
}
|
||||
|
||||
private func sortWidgets() {
|
||||
|
|
@ -84,4 +125,28 @@ final class ExtensionLockScreenWidgetManager: ObservableObject {
|
|||
return lhs.priority > rhs.priority
|
||||
}
|
||||
}
|
||||
|
||||
private func broadcastSnapshot() {
|
||||
guard !suppressBroadcast else { return }
|
||||
eventBridge.broadcastLockScreenWidgetSnapshot(activeWidgets)
|
||||
logDiagnostics("Broadcasted lock screen widget snapshot (count: \(activeWidgets.count))")
|
||||
}
|
||||
|
||||
private func applySnapshot(_ payloads: [ExtensionLockScreenWidgetPayload], sourcePID: Int32) {
|
||||
guard sourcePID != currentProcessID else { return }
|
||||
suppressBroadcast = true
|
||||
activeWidgets = payloads.sorted { lhs, rhs in
|
||||
if lhs.priority == rhs.priority {
|
||||
return lhs.receivedAt < rhs.receivedAt
|
||||
}
|
||||
return lhs.priority > rhs.priority
|
||||
}
|
||||
suppressBroadcast = false
|
||||
logDiagnostics("Applied external lock screen widget snapshot from PID \(sourcePID) (count: \(payloads.count))")
|
||||
}
|
||||
|
||||
private func logDiagnostics(_ message: String) {
|
||||
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
||||
Logger.log(message, category: .extensions)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ final class ExtensionLockScreenWidgetPresentationController {
|
|||
private let windowPool = ExtensionLockScreenWidgetWindowPool()
|
||||
private var cachedPayloads: [ExtensionLockScreenWidgetPayload] = []
|
||||
private var isLocked: Bool = false
|
||||
private var lastVisibilityState: VisibilityState?
|
||||
|
||||
init(manager: ExtensionLockScreenWidgetManager) {
|
||||
self.manager = manager
|
||||
|
|
@ -55,6 +56,7 @@ final class ExtensionLockScreenWidgetPresentationController {
|
|||
self.refreshPresentation()
|
||||
} else {
|
||||
self.windowPool.hideAll()
|
||||
self.updateVisibilityLog(.hidden(reason: "feature-disabled"))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
|
@ -63,13 +65,37 @@ final class ExtensionLockScreenWidgetPresentationController {
|
|||
private func refreshPresentation() {
|
||||
guard isLocked, LockScreenManager.shared.currentLockStatus, Defaults[.enableExtensionLockScreenWidgets] else {
|
||||
windowPool.hideAll()
|
||||
updateVisibilityLog(.hidden(reason: "lock-state"))
|
||||
return
|
||||
}
|
||||
guard cachedPayloads.isEmpty == false else {
|
||||
windowPool.hideAll()
|
||||
updateVisibilityLog(.hidden(reason: "no-payloads"))
|
||||
return
|
||||
}
|
||||
windowPool.sync(with: cachedPayloads)
|
||||
updateVisibilityLog(.showing(count: cachedPayloads.count))
|
||||
}
|
||||
|
||||
private func updateVisibilityLog(_ state: VisibilityState) {
|
||||
guard state != lastVisibilityState else { return }
|
||||
lastVisibilityState = state
|
||||
switch state {
|
||||
case let .hidden(reason):
|
||||
logDiagnostics("Lock screen widgets hidden (reason: \(reason))")
|
||||
case let .showing(count):
|
||||
logDiagnostics("Presenting \(count) extension lock screen widget(s)")
|
||||
}
|
||||
}
|
||||
|
||||
private func logDiagnostics(_ message: String) {
|
||||
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
||||
Logger.log(message, category: .extensions)
|
||||
}
|
||||
|
||||
private enum VisibilityState: Equatable {
|
||||
case hidden(reason: String)
|
||||
case showing(count: Int)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,11 +137,13 @@ private final class ExtensionLockScreenWidgetWindowPool {
|
|||
record.window.contentView = nil
|
||||
}
|
||||
windows.removeAll()
|
||||
logDiagnostics("Cleared all lock screen widget windows")
|
||||
}
|
||||
|
||||
private func renderWindow(for payload: ExtensionLockScreenWidgetPayload) {
|
||||
guard let screen = currentScreen() else { return }
|
||||
let descriptor = payload.descriptor
|
||||
let isNewWindow = windows[payload.id] == nil
|
||||
let window = ensureWindow(for: payload.id)
|
||||
let hosting = NSHostingView(rootView: ExtensionLockScreenWidgetView(payload: payload))
|
||||
hosting.frame = NSRect(origin: .zero, size: descriptor.size)
|
||||
|
|
@ -126,6 +154,11 @@ private final class ExtensionLockScreenWidgetWindowPool {
|
|||
window.setFrame(frame(for: descriptor, on: screen), display: true)
|
||||
window.alphaValue = 1
|
||||
window.orderFrontRegardless()
|
||||
if isNewWindow {
|
||||
logDiagnostics("Created lock screen widget window for \(payload.bundleIdentifier) id=\(payload.id) style=\(descriptor.layoutStyle)")
|
||||
} else {
|
||||
logDiagnostics("Updated lock screen widget window for \(payload.bundleIdentifier) id=\(payload.id)")
|
||||
}
|
||||
}
|
||||
|
||||
private func ensureWindow(for id: String) -> NSWindow {
|
||||
|
|
@ -162,6 +195,7 @@ private final class ExtensionLockScreenWidgetWindowPool {
|
|||
record.window.orderOut(nil)
|
||||
record.window.contentView = nil
|
||||
windows.removeValue(forKey: id)
|
||||
logDiagnostics("Removed lock screen widget window id=\(id)")
|
||||
}
|
||||
|
||||
private func registerScreenChangeObservers() {
|
||||
|
|
@ -188,7 +222,7 @@ private final class ExtensionLockScreenWidgetWindowPool {
|
|||
private func handleScreenGeometryChange(reason: String) {
|
||||
guard windows.isEmpty == false else { return }
|
||||
repositionAll(animated: false)
|
||||
print("ExtensionLockScreenWidgetWindowPool: realigned windows due to \(reason)")
|
||||
logDiagnostics("Realigned lock screen widget windows due to \(reason)")
|
||||
}
|
||||
|
||||
private func repositionAll(animated: Bool) {
|
||||
|
|
@ -246,3 +280,8 @@ private final class ExtensionLockScreenWidgetWindowPool {
|
|||
LockScreenDisplayContextProvider.shared.contextSnapshot()?.screen ?? NSScreen.main
|
||||
}
|
||||
}
|
||||
|
||||
private func logDiagnostics(_ message: String) {
|
||||
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
||||
Logger.log(message, category: .extensions)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue