feat: sync force-quit Flow teardown and polish macOS local ASR UX
End Live Activities and release the audio session synchronously on applicationWillTerminate, and continue macOS local-model install, onboarding, and settings polish on this branch.
This commit is contained in:
@@ -59,6 +59,14 @@ final class AppURLHandler: NSObject, UIApplicationDelegate {
|
||||
configuration.delegateClass = AppSceneDelegate.self
|
||||
return configuration
|
||||
}
|
||||
|
||||
/// 后台音频会话仍 running 时,用户强杀常会进入此回调(约 5 秒清理窗口)。
|
||||
/// 同步释放麦克风 + 结束 Live Activity,避免重开后「麦克风被占用」。
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
MainActor.assumeIsolated {
|
||||
FlowTerminationCoordinator.performSynchronousTerminationCleanup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class AppSceneDelegate: NSObject, UIWindowSceneDelegate {
|
||||
|
||||
@@ -116,4 +116,21 @@ enum FlowLiveActivityController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `applicationWillTerminate` 专用:阻塞到所有 `end` 完成,避免进程先退出而锁屏卡片残留。
|
||||
nonisolated static func endAllSynchronouslyOnTerminate() {
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
Task.detached(priority: .userInitiated) {
|
||||
let activities = Activity<FlowActivityAttributes>.activities
|
||||
let count = activities.count
|
||||
for activity in activities {
|
||||
await activity.end(activity.content, dismissalPolicy: .immediate)
|
||||
}
|
||||
FlowDiagnostics.log("Live Activity ended synchronously on terminate (count=\(count))")
|
||||
semaphore.signal()
|
||||
}
|
||||
semaphore.wait()
|
||||
currentPhase = .idle
|
||||
currentActivity = nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ final class FlowSessionManager: ObservableObject {
|
||||
capture.onEngineLiveChanged = { [weak self] _ in
|
||||
self?.refreshHostReady()
|
||||
}
|
||||
FlowTerminationCoordinator.register(self)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -210,6 +211,73 @@ final class FlowSessionManager: ObservableObject {
|
||||
AppPermissions.openSystemSettings()
|
||||
}
|
||||
|
||||
/// 强杀专用同步 teardown:不等待 ASR/LLM;Live Activity 由
|
||||
/// `FlowTerminationCoordinator` 同步 `end`。
|
||||
func prepareForProcessTermination() {
|
||||
debug("prepareForProcessTermination")
|
||||
|
||||
coldStartContext = nil
|
||||
isColdStartHandoff = false
|
||||
coldStartRecoveryTask?.cancel()
|
||||
coldStartRecoveryTask = nil
|
||||
startTask?.cancel()
|
||||
startTask = nil
|
||||
commandObserver = nil
|
||||
pollingTask?.cancel()
|
||||
pollingTask = nil
|
||||
heartbeatTask?.cancel()
|
||||
heartbeatTask = nil
|
||||
expiryTask?.cancel()
|
||||
expiryTask = nil
|
||||
levelTask?.cancel()
|
||||
levelTask = nil
|
||||
finalizeTask?.cancel()
|
||||
finalizeTask = nil
|
||||
utteranceSafetyTask?.cancel()
|
||||
utteranceSafetyTask = nil
|
||||
asrTask?.cancel()
|
||||
asrTask = nil
|
||||
chunkedPipeline = nil
|
||||
|
||||
if isUtteranceRecording || isUtteranceProcessing {
|
||||
capture.cancelUtterance()
|
||||
asr.cancel()
|
||||
}
|
||||
|
||||
capture.cancelUtterance()
|
||||
if capture.running {
|
||||
capture.stop()
|
||||
}
|
||||
|
||||
endBackgroundKeepAlive()
|
||||
ScreenWakeLock.release()
|
||||
|
||||
sessionASR?.cancel()
|
||||
sessionASR = nil
|
||||
sessionASREngineMode = nil
|
||||
sessionASRWarmedLocaleID = nil
|
||||
|
||||
if isActive || FlowSessionBridge.isSessionActive() {
|
||||
FlowSessionBridge.markSessionInactive()
|
||||
FlowSessionDarwin.postSessionChanged()
|
||||
}
|
||||
|
||||
activeSessionId = nil
|
||||
currentUtteranceId = nil
|
||||
currentCommandSeq = 0
|
||||
lastHandledCommandSeq = 0
|
||||
isUtteranceRecording = false
|
||||
isUtteranceProcessing = false
|
||||
isActive = false
|
||||
isStarting = false
|
||||
sessionExpiresAt = nil
|
||||
sessionWarning = nil
|
||||
currentPartial = ""
|
||||
lastFinal = ""
|
||||
chunkWarnings = []
|
||||
FlowSessionBridge.setHostReady(false)
|
||||
}
|
||||
|
||||
func endSession() {
|
||||
guard isActive else { return }
|
||||
debug("Flow session ended")
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// FlowTerminationCoordinator.swift
|
||||
// OSGKeyboard · Main App
|
||||
//
|
||||
// 桥接 `UIApplicationDelegate.applicationWillTerminate` 与 `FlowSessionManager`。
|
||||
// SwiftUI 里 `FlowSessionManager` 是 `@StateObject`,AppDelegate 无法直接持有;
|
||||
// 此处用弱引用在进程退出窗口(约 5 秒)内同步释放麦克风与 Live Activity。
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
enum FlowTerminationCoordinator {
|
||||
private static weak var sessionManager: FlowSessionManager?
|
||||
|
||||
/// `FlowSessionManager.init()` 注册当前实例。
|
||||
static func register(_ manager: FlowSessionManager) {
|
||||
sessionManager = manager
|
||||
}
|
||||
|
||||
/// 强杀 / 系统终止时调用。必须在主线程执行(`applicationWillTerminate` 保证)。
|
||||
static func performSynchronousTerminationCleanup() {
|
||||
sessionManager?.prepareForProcessTermination()
|
||||
FlowLiveActivityController.endAllSynchronouslyOnTerminate()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user