From 21ef85545b5f8f865a4cacfe4aafcf6e43b4c1d9 Mon Sep 17 00:00:00 2001 From: t-regbs Date: Fri, 6 Nov 2026 20:23:37 +0000 Subject: [PATCH 1/3] Remove unused native navigation and activity state --- iosApp/iosApp/NativeRootSessions.swift | 4 ---- .../mathalarm/interactors/IosAlarmAudioManager.kt | 5 ----- .../mathalarm/platform/PlatformServices.ios.kt | 8 +------- 3 files changed, 1 insertion(+), 16 deletions(-) diff --git a/iosApp/iosApp/NativeRootSessions.swift b/iosApp/iosApp/NativeRootSessions.swift index 0bdb7b0..70026e9 100644 --- a/iosApp/iosApp/NativeRootSessions.swift +++ b/iosApp/iosApp/NativeRootSessions.swift @@ -218,10 +218,6 @@ final class NativeWindowSessions: ObservableObject { selectedEditorID == id && navigationObservers[id] == observer } - func endNavigation(id: String, observer: Int) { - if navigationObservers[id] == observer { navigationObservers.removeValue(forKey: id) } - } - func setEditorPath(_ path: [NativeEditorDestination], id: String, observer: Int? = nil) { // SwiftUI can write an outgoing stack binding during detail teardown. // An old observer must not clear the retained path of an inactive draft. diff --git a/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/interactors/IosAlarmAudioManager.kt b/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/interactors/IosAlarmAudioManager.kt index f97056b..d3434aa 100644 --- a/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/interactors/IosAlarmAudioManager.kt +++ b/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/interactors/IosAlarmAudioManager.kt @@ -196,11 +196,6 @@ object IosAlarmAudioManager { } } - /** - * Check if alarm is currently active - */ - fun isPlaying(): Boolean = isAlarmActive - /** Silent PiP still needs a playback session. Real alarms and tone previews win. */ fun beginSettingsGuideAudio(ownerId: String, onInterrupted: () -> Unit): Boolean { require(ownerId.isNotBlank()) diff --git a/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/platform/PlatformServices.ios.kt b/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/platform/PlatformServices.ios.kt index 41c110b..1ac3eef 100644 --- a/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/platform/PlatformServices.ios.kt +++ b/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/platform/PlatformServices.ios.kt @@ -12,21 +12,15 @@ import platform.UIKit.UIImpactFeedbackStyle @OptIn(ExperimentalForeignApi::class) actual class PlatformVibrator actual constructor() { private val feedbackGenerator = UIImpactFeedbackGenerator(style = UIImpactFeedbackStyle.UIImpactFeedbackStyleHeavy) - private var isVibrating = false actual fun startWaveform(pattern: LongArray, repeat: Int) { - isVibrating = true - // Use system vibration AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) - // Also trigger haptic feedback for modern devices feedbackGenerator.prepare() feedbackGenerator.impactOccurred() } actual fun cancel() { - isVibrating = false - // iOS doesn't have a way to cancel vibration mid-vibrate - // but we can stop the loop by setting flag + // System vibration and impact feedback are one-shot and cannot be cancelled. } } From 51a1f5995513f2446c1f4dc6b1764b5e6101fa4f Mon Sep 17 00:00:00 2001 From: t-regbs Date: Fri, 6 Nov 2026 20:23:46 +0000 Subject: [PATCH 2/3] Drop unused AlarmKit backend result flag --- .../mathalarm/alarm/AlarmSchedulerBridge.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/alarm/AlarmSchedulerBridge.kt b/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/alarm/AlarmSchedulerBridge.kt index d1706bf..5991a1b 100644 --- a/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/alarm/AlarmSchedulerBridge.kt +++ b/shared/src/iosMain/kotlin/com/timilehinaregbesola/mathalarm/alarm/AlarmSchedulerBridge.kt @@ -19,7 +19,6 @@ data class AlarmScheduleRequest( data class AlarmScheduleResult( val success: Boolean, - val usedAlarmKit: Boolean, val errorMessage: String? = null ) @@ -50,12 +49,12 @@ object AlarmSchedulerBridge { fun registerScheduler(scheduler: NativeAlarmScheduler) { nativeScheduler = scheduler } fun isAlarmKitAvailable(): Boolean = nativeScheduler?.isAlarmKitAvailable() == true suspend fun scheduleWithAlarmKit(request: AlarmScheduleRequest): AlarmScheduleResult { - val scheduler = nativeScheduler ?: return AlarmScheduleResult(false, false) - if (!scheduler.isAlarmKitAvailable()) return AlarmScheduleResult(false, false) + val scheduler = nativeScheduler ?: return AlarmScheduleResult(false) + if (!scheduler.isAlarmKitAvailable()) return AlarmScheduleResult(false) return suspendCoroutine { continuation -> scheduler.scheduleAlarm(request, object : AlarmScheduleCompletion { override fun complete(success: Boolean, error: String?) { - continuation.resume(AlarmScheduleResult(success, true, error)) + continuation.resume(AlarmScheduleResult(success, error)) } }) } From fb1ce70bd45b85c6c533fb1f6144b690d9add372 Mon Sep 17 00:00:00 2001 From: t-regbs Date: Fri, 6 Nov 2026 20:23:52 +0000 Subject: [PATCH 3/3] Inline native AlarmKit scheduling and cancellation --- iosApp/iosApp/AlarmKitWrapper.swift | 202 +++++++++++++--------------- 1 file changed, 93 insertions(+), 109 deletions(-) diff --git a/iosApp/iosApp/AlarmKitWrapper.swift b/iosApp/iosApp/AlarmKitWrapper.swift index 6ae55be..a3c0442 100644 --- a/iosApp/iosApp/AlarmKitWrapper.swift +++ b/iosApp/iosApp/AlarmKitWrapper.swift @@ -445,73 +445,7 @@ class AlarmKitWrapperImpl: NSObject { } } - /// Schedule an alarm using AlarmKit - /// - Returns: true if successfully scheduled, false otherwise func scheduleAlarm(request: AlarmScheduleRequest, completion: AlarmScheduleCompletion) { - scheduleWithAlarmKit(request: request, completion: completion) - } - - private func occurrenceUUID(alarmId: Int64, key: String) -> UUID { - let bytes = Array(SHA256.hash(data: Data("mathalarm/\(alarmId)/\(key)".utf8))) - return UUID(uuid: (bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], - bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15])) - } - - private func recoveryIDs(alarmId: Int64) -> [UUID] { - ["recovery", "recovery_standby"].map { occurrenceUUID(alarmId: alarmId, key: $0) } - } - - func cancelOccurrence(alarmId: Int64, occurrenceKey: String) -> String? { - let priorRecovery = occurrenceKey == "recovery" ? AlarmRecoveryStore.shared.current(alarmId: alarmId) : nil - if occurrenceKey == "recovery", !AlarmRecoveryStore.shared.cancel(alarmId: alarmId) { - IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) - return "Could not persist recovery cancellation" - } - let ids = occurrenceKey == "recovery" ? recoveryIDs(alarmId: alarmId) : - [occurrenceUUID(alarmId: alarmId, key: occurrenceKey)] - do { - let manager = AlarmManager.shared - let nativeIDs = Set(try manager.alarms.map(\.id)) - var failures: [String] = [] - for id in ids { - do { - if nativeIDs.contains(id) { try manager.cancel(id: id) } - if !AlarmDataStore.shared.remove(alarmUUID: id.uuidString) { - failures.append("Could not persist native cancellation") - } - } catch { failures.append(error.localizedDescription) } - } - if !failures.isEmpty { - if let priorRecovery { - AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) - IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) - } - return failures.joined(separator: "; ") - } - return nil - } catch { - if let priorRecovery { - AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) - IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) - } - print("Failed to cancel occurrence: \(error)") - return error.localizedDescription - } - } - - /// Cancel an alarm scheduled with AlarmKit - func cancelAlarm(alarmId: Int64) -> String? { - return cancelAlarmKitAlarm(alarmId: alarmId) - } - - // MARK: - AlarmKit Implementation (iOS 26+) - - // Typealias for AlarmConfiguration with our metadata type - @available(iOS 26, *) - typealias MathAlarmConfiguration = AlarmManager.AlarmConfiguration - - @available(iOS 26, *) - private func scheduleWithAlarmKit(request: AlarmScheduleRequest, completion: AlarmScheduleCompletion) { let alarmId = request.alarmId let hour = request.hour let minute = request.minute @@ -625,6 +559,99 @@ class AlarmKitWrapperImpl: NSObject { } + private func occurrenceUUID(alarmId: Int64, key: String) -> UUID { + let bytes = Array(SHA256.hash(data: Data("mathalarm/\(alarmId)/\(key)".utf8))) + return UUID(uuid: (bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15])) + } + + private func recoveryIDs(alarmId: Int64) -> [UUID] { + ["recovery", "recovery_standby"].map { occurrenceUUID(alarmId: alarmId, key: $0) } + } + + func cancelOccurrence(alarmId: Int64, occurrenceKey: String) -> String? { + let priorRecovery = occurrenceKey == "recovery" ? AlarmRecoveryStore.shared.current(alarmId: alarmId) : nil + if occurrenceKey == "recovery", !AlarmRecoveryStore.shared.cancel(alarmId: alarmId) { + IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) + return "Could not persist recovery cancellation" + } + let ids = occurrenceKey == "recovery" ? recoveryIDs(alarmId: alarmId) : + [occurrenceUUID(alarmId: alarmId, key: occurrenceKey)] + do { + let manager = AlarmManager.shared + let nativeIDs = Set(try manager.alarms.map(\.id)) + var failures: [String] = [] + for id in ids { + do { + if nativeIDs.contains(id) { try manager.cancel(id: id) } + if !AlarmDataStore.shared.remove(alarmUUID: id.uuidString) { + failures.append("Could not persist native cancellation") + } + } catch { failures.append(error.localizedDescription) } + } + if !failures.isEmpty { + if let priorRecovery { + AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) + IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) + } + return failures.joined(separator: "; ") + } + return nil + } catch { + if let priorRecovery { + AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) + IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) + } + print("Failed to cancel occurrence: \(error)") + return error.localizedDescription + } + } + + func cancelAlarm(alarmId: Int64) -> String? { + let priorRecovery = AlarmRecoveryStore.shared.current(alarmId: alarmId) + let recoveryID = occurrenceUUID(alarmId: alarmId, key: "recovery") + let priorRecoveryMetadata = AlarmDataStore.shared.retrieve(alarmUUID: recoveryID.uuidString) + guard AlarmRecoveryStore.shared.cancel(alarmId: alarmId) else { + IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) + return "Could not persist recovery cancellation" + } + do { + let manager = AlarmManager.shared + // Query OS alarms and persisted metadata so cancellation also works after relaunch. + let keys = (0...6).map { "day_\($0)" } + ["snooze", "recovery", "recovery_standby"] + let ids = Set(keys.map { occurrenceUUID(alarmId: alarmId, key: $0) }) + var failures: [String] = [] + for alarm in try manager.alarms { + if ids.contains(alarm.id) || AlarmDataStore.shared.retrieve(alarmUUID: alarm.id.uuidString)?.alarmId == alarmId { + do { + try manager.cancel(id: alarm.id) + if !AlarmDataStore.shared.remove(alarmUUID: alarm.id.uuidString) { + failures.append("\(alarm.id): Could not persist native cancellation") + } + } catch { + failures.append("\(alarm.id): \(error.localizedDescription)") + } + } + } + if !failures.isEmpty, let priorRecovery { + AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) + if let priorRecoveryMetadata { AlarmDataStore.shared.store(alarmUUID: recoveryID, data: priorRecoveryMetadata) } + IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) + } + return failures.isEmpty ? nil : failures.joined(separator: "; ") + } catch { + if let priorRecovery { + AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) + IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) + } + print("Failed to list alarms for cancellation: \(error)") + return error.localizedDescription + } + } + + @available(iOS 26, *) + typealias MathAlarmConfiguration = AlarmManager.AlarmConfiguration + private func configuration(schedule: AlarmKit.Alarm.Schedule, metadata: MathAlarmData, id: UUID) -> MathAlarmConfiguration { let alert = AlarmPresentation.Alert( title: metadata.title.isEmpty ? LocalizedStringResource("Solve Math") : LocalizedStringResource(stringLiteral: metadata.title), @@ -932,49 +959,6 @@ class AlarmKitWrapperImpl: NSObject { } } - @available(iOS 26, *) - private func cancelAlarmKitAlarm(alarmId: Int64) -> String? { - let priorRecovery = AlarmRecoveryStore.shared.current(alarmId: alarmId) - let recoveryID = occurrenceUUID(alarmId: alarmId, key: "recovery") - let priorRecoveryMetadata = AlarmDataStore.shared.retrieve(alarmUUID: recoveryID.uuidString) - guard AlarmRecoveryStore.shared.cancel(alarmId: alarmId) else { - IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) - return "Could not persist recovery cancellation" - } - do { - let manager = AlarmManager.shared - // Query OS alarms and persisted metadata so cancellation also works after relaunch. - let keys = (0...6).map { "day_\($0)" } + ["snooze", "recovery", "recovery_standby"] - let ids = Set(keys.map { occurrenceUUID(alarmId: alarmId, key: $0) }) - var failures: [String] = [] - for alarm in try manager.alarms { - if ids.contains(alarm.id) || AlarmDataStore.shared.retrieve(alarmUUID: alarm.id.uuidString)?.alarmId == alarmId { - do { - try manager.cancel(id: alarm.id) - if !AlarmDataStore.shared.remove(alarmUUID: alarm.id.uuidString) { - failures.append("\(alarm.id): Could not persist native cancellation") - } - } catch { - failures.append("\(alarm.id): \(error.localizedDescription)") - } - } - } - if !failures.isEmpty, let priorRecovery { - AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) - if let priorRecoveryMetadata { AlarmDataStore.shared.store(alarmUUID: recoveryID, data: priorRecoveryMetadata) } - IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) - } - return failures.isEmpty ? nil : failures.joined(separator: "; ") - } catch { - if let priorRecovery { - AlarmRecoveryStore.shared.restoreAfterFailedCancellation(alarmId: alarmId, session: priorRecovery) - IosApplication.shared.reportRecoveryFailure(alarmId: alarmId) - } - print("Failed to list alarms for cancellation: \(error)") - return error.localizedDescription - } - } - /// Convert repeat days string to array of Locale.Weekday /// Input: "TFFFTFF" where T=true, F=false, index 0=Sunday /// Output: Set