Something is working Finally

This commit is contained in:
Hariharan Mudaliar 2025-11-23 22:25:39 +05:30
commit 29b5111a86
10 changed files with 404 additions and 315 deletions

View file

@ -10,6 +10,7 @@ import Defaults
import EventKit
import Foundation
import CoreGraphics
import os
@MainActor
final class ReminderLiveActivityManager: ObservableObject {
@ -33,6 +34,22 @@ final class ReminderLiveActivityManager: ObservableObject {
@Published private(set) var upcomingEntries: [ReminderEntry] = []
@Published private(set) var activeWindowReminders: [ReminderEntry] = []
private let logger: os.Logger = os.Logger(subsystem: "com.ebullioscopic.Atoll", category: "ReminderLiveActivity")
private enum RefreshReason: String {
case initialization
case defaults
case manual
case eventStoreChange
case calendarListChange
case calendarEventsUpdate
case fallback
case evaluation
case hover
case ticker
case other
}
private var nextReminder: ReminderEntry?
private var cancellables = Set<AnyCancellable>()
private var tickerTask: Task<Void, Never>? { didSet { oldValue?.cancel() } }
@ -42,11 +59,28 @@ final class ReminderLiveActivityManager: ObservableObject {
private var pendingRefreshTask: Task<Void, Never>?
private var pendingRefreshForce = false
private var pendingRefreshToken = UUID()
private var pendingRefreshReason: RefreshReason = .other
private var lastKnownCalendarIDs = Set<String>()
private var lastRefreshDate: Date?
private var lastRefreshCompletionDate: Date?
private let minimumRefreshInterval: TimeInterval = 60
private let refreshDebounceInterval: TimeInterval = 0.3
private var refreshTaskToken = UUID()
private var hasShownCriticalSneakPeek = false
private let eventStoreChangeNoiseInterval: TimeInterval = 4
private let eventStoreForceThrottleInterval: TimeInterval = 20
private let manualRefreshSuppressionInterval: TimeInterval = 20
private var lastEventStoreForceRefreshDate: Date?
private var lastManualRefreshDate: Date?
private var pendingEventStoreChange: (date: Date, reason: RefreshReason)?
private var reminderCompletionSuppressionDeadline: Date?
private let reminderCompletionSuppressionInterval: TimeInterval = 15
private var lastUpcomingSignature: [String] = []
private var lastNoopEventStoreRefreshDate: Date?
private let noopEventStoreSuppressionInterval: TimeInterval = 30
private var lastCalendarEventSnapshot: [EventModel] = []
private var lastCalendarSnapshotDate: Date?
private var lastCalendarEventSnapshotSignature: [String] = []
private let calendarService: CalendarServiceProviding
private let calendarManager = CalendarManager.shared
@ -56,7 +90,7 @@ final class ReminderLiveActivityManager: ObservableObject {
private init(calendarService: CalendarServiceProviding = CalendarService()) {
self.calendarService = calendarService
setupObservers()
scheduleRefresh(force: true)
scheduleRefresh(force: true, reason: .initialization)
}
private func setupObservers() {
@ -64,7 +98,7 @@ final class ReminderLiveActivityManager: ObservableObject {
.sink { [weak self] change in
guard let self else { return }
if change.newValue {
self.scheduleRefresh(force: true)
self.scheduleRefresh(force: true, reason: .defaults)
} else {
self.deactivateReminder()
}
@ -74,7 +108,7 @@ final class ReminderLiveActivityManager: ObservableObject {
Defaults.publisher(.reminderLeadTime, options: [])
.sink { [weak self] _ in
guard let self else { return }
self.scheduleRefresh(force: true)
self.scheduleRefresh(force: true, reason: .defaults)
}
.store(in: &cancellables)
@ -89,23 +123,27 @@ final class ReminderLiveActivityManager: ObservableObject {
.store(in: &cancellables)
calendarManager.$allCalendars
.sink { [weak self] _ in
.sink { [weak self] calendars in
guard let self else { return }
self.scheduleRefresh(force: true)
let identifiers = Set(calendars.map { $0.id })
guard identifiers != self.lastKnownCalendarIDs else { return }
self.lastKnownCalendarIDs = identifiers
self.handleEventStoreDrivenChange(reason: .calendarListChange)
}
.store(in: &cancellables)
calendarManager.$events
.sink { [weak self] _ in
.receive(on: RunLoop.main)
.sink { [weak self] events in
guard let self else { return }
self.scheduleRefresh(force: false)
self.handleCalendarEventsUpdate(events)
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: .EKEventStoreChanged)
.sink { [weak self] _ in
guard let self else { return }
self.scheduleRefresh(force: true)
self.handleEventStoreDrivenChange(reason: .eventStoreChange)
}
.store(in: &cancellables)
}
@ -122,6 +160,7 @@ final class ReminderLiveActivityManager: ObservableObject {
refreshTask?.cancel()
refreshTask = nil
hasShownCriticalSneakPeek = false
pendingEventStoreChange = nil
}
private func deactivateReminder() {
@ -130,6 +169,8 @@ final class ReminderLiveActivityManager: ObservableObject {
upcomingEntries = []
activeWindowReminders = []
cancelAllTimers()
lastUpcomingSignature = []
lastNoopEventStoreRefreshDate = nil
}
private func selectedCalendarIDs() -> [String] {
@ -138,6 +179,24 @@ final class ReminderLiveActivityManager: ObservableObject {
.map { $0.id }
}
private func handleCalendarEventsUpdate(_ events: [EventModel]) {
let now = Date()
let signature = events.map { $0.id }
if signature == lastCalendarEventSnapshotSignature,
let snapshotDate = lastCalendarSnapshotDate,
now.timeIntervalSince(snapshotDate) < 5 {
return
}
lastCalendarEventSnapshot = events
lastCalendarSnapshotDate = now
lastCalendarEventSnapshotSignature = signature
guard Defaults[.enableReminderLiveActivity] else { return }
logger.debug("[Reminder] Applying calendar snapshot update (events=\(events.count, privacy: .public))")
refreshFromEvents(events, referenceDate: now, reason: .calendarEventsUpdate)
}
private func shouldHide(_ event: EventModel) -> Bool {
if event.isAllDay && Defaults[.hideAllDayEvents] {
return true
@ -174,18 +233,28 @@ final class ReminderLiveActivityManager: ObservableObject {
private func scheduleFallbackRefresh() {
fallbackRefreshTask?.cancel()
let delay: TimeInterval = 15 * 60
logger.debug("[Reminder] Scheduling fallback refresh in \(delay, format: .fixed(precision: 0))s")
fallbackRefreshTask = Task { [weak self] in
guard let self else { return }
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
await self.scheduleRefresh(force: true)
do {
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
} catch {
self.logger.debug("[Reminder] Cancelled pending fallback refresh before execution")
return
}
await self.scheduleRefresh(force: true, reason: .fallback)
}
}
private func scheduleRefresh(force: Bool) {
private func scheduleRefresh(force: Bool, reason: RefreshReason = .other) {
let wasPending = pendingRefreshTask != nil
pendingRefreshForce = pendingRefreshForce || force
pendingRefreshToken = UUID()
pendingRefreshReason = reason
let token = pendingRefreshToken
logger.debug("[Reminder] scheduleRefresh reason=\(reason.rawValue, privacy: .public) force=\(force, privacy: .public) pending=\(wasPending, privacy: .public)")
refreshTask?.cancel()
pendingRefreshTask?.cancel()
pendingRefreshTask = Task { [weak self] in
@ -193,6 +262,7 @@ final class ReminderLiveActivityManager: ObservableObject {
do {
try await Task.sleep(nanoseconds: UInt64(self.refreshDebounceInterval * 1_000_000_000))
} catch {
self.logger.debug("[Reminder] Debounced refresh for reason=\(reason.rawValue, privacy: .public) was cancelled")
return
}
await self.executeScheduledRefresh(token: token)
@ -204,15 +274,18 @@ final class ReminderLiveActivityManager: ObservableObject {
pendingRefreshTask = nil
if Task.isCancelled {
logger.debug("[Reminder] Cancelled refresh token=\(token.uuidString, privacy: .public)")
return
}
let reason = pendingRefreshReason
let now = Date()
let force = pendingRefreshForce
if !force, let last = lastRefreshDate {
let elapsed = now.timeIntervalSince(last)
if elapsed < minimumRefreshInterval {
let remaining = max(minimumRefreshInterval - elapsed, refreshDebounceInterval)
logger.debug("[Reminder] Deferring refresh reason=\(reason.rawValue, privacy: .public); next attempt in \(remaining, format: .fixed(precision: 2))s")
pendingRefreshToken = UUID()
let nextToken = pendingRefreshToken
pendingRefreshTask = Task { [weak self] in
@ -220,6 +293,7 @@ final class ReminderLiveActivityManager: ObservableObject {
do {
try await Task.sleep(nanoseconds: UInt64(remaining * 1_000_000_000))
} catch {
self.logger.debug("[Reminder] Deferred refresh for reason=\(reason.rawValue, privacy: .public) was cancelled")
return
}
await self.executeScheduledRefresh(token: nextToken)
@ -231,17 +305,24 @@ final class ReminderLiveActivityManager: ObservableObject {
pendingRefreshForce = false
lastRefreshDate = now
let startTime = Date()
logger.debug("[Reminder] Starting refresh reason=\(reason.rawValue, privacy: .public) force=\(force, privacy: .public)")
refreshTask?.cancel()
let taskToken = UUID()
refreshTaskToken = taskToken
let task = Task { [weak self] in
guard let self else { return }
await self.refreshUpcomingReminder(force: force)
await self.refreshUpcomingReminder(force: force, reason: reason)
}
refreshTask = task
defer {
if refreshTaskToken == taskToken {
refreshTask = nil
lastRefreshCompletionDate = Date()
let duration = Date().timeIntervalSince(startTime)
logger.debug("[Reminder] Finished refresh reason=\(reason.rawValue, privacy: .public) duration=\(duration, format: .fixed(precision: 2))s upcoming=\(self.upcomingEntries.count, privacy: .public)")
processPendingEventStoreChangeIfNeeded()
}
}
_ = try? await task.value
@ -262,6 +343,7 @@ final class ReminderLiveActivityManager: ObservableObject {
}
private func stopTicker() {
tickerTask?.cancel()
tickerTask = nil
}
@ -272,7 +354,7 @@ final class ReminderLiveActivityManager: ObservableObject {
Task { await self.evaluateCurrentState(at: referenceDate) }
}
private func refreshFromEvents(_ events: [EventModel], referenceDate: Date) {
private func refreshFromEvents(_ events: [EventModel], referenceDate: Date, reason: RefreshReason) {
let leadMinutes = Defaults[.reminderLeadTime]
let upcoming = events
.filter { !shouldHide($0) }
@ -282,42 +364,92 @@ final class ReminderLiveActivityManager: ObservableObject {
upcomingEntries = upcoming
updateActiveWindowReminders(for: referenceDate)
logger.debug("[Reminder] Reduced \(events.count, privacy: .public) events to \(upcoming.count, privacy: .public) upcoming reminders")
guard let first = upcoming.first else {
deactivateReminder()
logger.debug("[Reminder] No upcoming reminders found; scheduling fallback refresh")
scheduleFallbackRefresh()
return
}
logger.debug("[Reminder] Next reminder ‘\(first.event.title, privacy: .public)’ at \(first.triggerDate.timeIntervalSinceReferenceDate, privacy: .public)")
handleEntrySelection(first, referenceDate: referenceDate)
let signature = upcoming.map { $0.event.id }
if lastUpcomingSignature == signature {
if reason == .eventStoreChange {
lastNoopEventStoreRefreshDate = Date()
}
} else {
lastNoopEventStoreRefreshDate = nil
}
lastUpcomingSignature = signature
}
func requestRefresh(force: Bool = false) {
scheduleRefresh(force: force)
let reason: RefreshReason = force ? .manual : .other
if force {
let now = Date()
if let lastManualRefreshDate,
now.timeIntervalSince(lastManualRefreshDate) < manualRefreshSuppressionInterval {
let delta = now.timeIntervalSince(lastManualRefreshDate)
logger.debug("[Reminder] Skipping manual refresh; last manual was \(delta, format: .fixed(precision: 2))s ago")
return
}
lastManualRefreshDate = now
}
scheduleRefresh(force: force, reason: reason)
}
func refreshUpcomingReminder(force: Bool = false) async {
private func refreshUpcomingReminder(force: Bool = false, reason: RefreshReason = .other) async {
guard Defaults[.enableReminderLiveActivity] else {
deactivateReminder()
logger.debug("[Reminder] Refresh aborted (feature disabled)")
return
}
guard calendarManager.hasReminderAccess else {
deactivateReminder()
logger.debug("[Reminder] Refresh aborted; reminder permissions missing")
return
}
let now = Date()
if !force, let entry = nextReminder, entry.event.start > now {
logger.debug("[Reminder] Skipping fetch; cached entry \(entry.event.title, privacy: .public) still valid for reason=\(reason.rawValue, privacy: .public)")
await evaluateCurrentState(at: now)
return
}
if let snapshotDate = lastCalendarSnapshotDate,
now.timeIntervalSince(snapshotDate) < 5,
!lastCalendarEventSnapshot.isEmpty {
logger.debug("[Reminder] Using calendar snapshot (age=\(now.timeIntervalSince(snapshotDate), format: .fixed(precision: 2))s) for reason=\(reason.rawValue, privacy: .public)")
refreshFromEvents(lastCalendarEventSnapshot, referenceDate: now, reason: reason)
return
}
let calendars = selectedCalendarIDs()
guard !calendars.isEmpty else {
deactivateReminder()
logger.debug("[Reminder] Refresh aborted; no calendars selected")
return
}
let windowEnd = Calendar.current.date(byAdding: .hour, value: 24, to: now) ?? now.addingTimeInterval(24 * 60 * 60)
logger.debug("[Reminder] Fetching reminders (reason=\(reason.rawValue, privacy: .public), force=\(force, privacy: .public), calendars=\(calendars.count, privacy: .public))")
let fetchStart = Date()
let events = await calendarService.events(from: now, to: windowEnd, calendars: calendars)
let fetchDuration = Date().timeIntervalSince(fetchStart)
logger.debug("[Reminder] EventKit fetch completed (events=\(events.count, privacy: .public)) in \(fetchDuration, format: .fixed(precision: 2))s")
lastCalendarEventSnapshot = events
lastCalendarSnapshotDate = now
lastCalendarEventSnapshotSignature = events.map { $0.id }
await MainActor.run {
self.refreshFromEvents(events, referenceDate: now)
self.refreshFromEvents(events, referenceDate: now, reason: reason)
}
}
@ -331,8 +463,14 @@ final class ReminderLiveActivityManager: ObservableObject {
updateActiveWindowReminders(for: date)
guard var entry = nextReminder else {
activeReminder = nil
if activeReminder != nil {
activeReminder = nil
}
stopTicker()
hasShownCriticalSneakPeek = false
if fallbackRefreshTask == nil {
scheduleFallbackRefresh()
}
return
}
@ -341,7 +479,9 @@ final class ReminderLiveActivityManager: ObservableObject {
nextReminder = nil
stopTicker()
hasShownCriticalSneakPeek = false
scheduleRefresh(force: true)
logger.debug("[Reminder] Reminder reached start time; requesting evaluation refresh")
reminderCompletionSuppressionDeadline = Date().addingTimeInterval(reminderCompletionSuppressionInterval)
scheduleRefresh(force: true, reason: .evaluation)
return
}
@ -408,10 +548,88 @@ final class ReminderLiveActivityManager: ObservableObject {
entry.triggerDate <= date && entry.event.start >= date
}
if filtered != activeWindowReminders {
logger.debug("[Reminder] Active window reminder count -> \(filtered.count, privacy: .public)")
activeWindowReminders = filtered
}
}
private func handleEventStoreDrivenChange(reason: RefreshReason) {
processEventStoreChange(at: Date(), reason: reason)
}
private func processEventStoreChange(at date: Date, reason: RefreshReason) {
if let manual = lastManualRefreshDate {
let delta = date.timeIntervalSince(manual)
if delta < manualRefreshSuppressionInterval {
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public) change; manual refresh cooldown \(delta, format: .fixed(precision: 2))s")
return
}
}
if let lastRefreshStart = lastRefreshDate {
let sinceStart = date.timeIntervalSince(lastRefreshStart)
if sinceStart >= 0 && sinceStart < eventStoreChangeNoiseInterval {
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); \(sinceStart, format: .fixed(precision: 2))s since start")
return
}
}
if let lastCompletion = lastRefreshCompletionDate {
let sinceCompletion = abs(date.timeIntervalSince(lastCompletion))
if sinceCompletion < eventStoreChangeNoiseInterval {
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); \(sinceCompletion, format: .fixed(precision: 2))s since completion")
return
}
}
if reason == .eventStoreChange,
let suppressionDeadline = reminderCompletionSuppressionDeadline,
date <= suppressionDeadline {
let remaining = suppressionDeadline.timeIntervalSince(date)
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); reminder completion suppression \(remaining, format: .fixed(precision: 2))s")
return
}
if reason == .eventStoreChange,
let noopDate = lastNoopEventStoreRefreshDate,
date.timeIntervalSince(noopDate) < noopEventStoreSuppressionInterval {
let remaining = noopEventStoreSuppressionInterval - date.timeIntervalSince(noopDate)
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); identical upcoming entries \(remaining, format: .fixed(precision: 2))s window")
return
}
if let lastForce = lastEventStoreForceRefreshDate {
let sinceForce = date.timeIntervalSince(lastForce)
if sinceForce < self.eventStoreForceThrottleInterval {
logger.debug("[Reminder] Ignoring \(reason.rawValue, privacy: .public); throttle window remaining \(self.eventStoreForceThrottleInterval - sinceForce, format: .fixed(precision: 2))s")
return
}
}
if refreshTask != nil {
pendingEventStoreChange = (date, reason)
logger.debug("[Reminder] Queued \(reason.rawValue, privacy: .public) change while refresh is running")
return
}
triggerEventStoreForcedRefresh(at: date, reason: reason)
}
private func processPendingEventStoreChangeIfNeeded() {
guard let pendingChange = pendingEventStoreChange else { return }
pendingEventStoreChange = nil
processEventStoreChange(at: pendingChange.date, reason: pendingChange.reason)
}
private func triggerEventStoreForcedRefresh(at date: Date = Date(), reason: RefreshReason) {
lastEventStoreForceRefreshDate = date
if reason == .eventStoreChange, reminderCompletionSuppressionDeadline != nil {
reminderCompletionSuppressionDeadline = nil
}
logger.debug("[Reminder] Triggering event-store refresh for \(reason.rawValue, privacy: .public)")
scheduleRefresh(force: true, reason: reason)
}
static func additionalHeight(forRowCount rowCount: Int) -> CGFloat {
guard rowCount > 0 else { return 0 }
let rows = CGFloat(rowCount)