@@ -87,12 +87,18 @@ final class LiveActivityManager {
8787
8888 @objc private func handleDidBecomeActive( ) {
8989 guard Storage . shared. laEnabled. value else { return }
90- if skipNextDidBecomeActive {
91- LogManager . shared. log ( category: . general, message: " [LA] didBecomeActive: skipped (handleForeground owns restart) " , isDebug: true )
92- skipNextDidBecomeActive = false
90+ let appState = UIApplication . shared. applicationState. rawValue
91+ let existing = Activity < GlucoseLiveActivityAttributes > . activities. count
92+ if pendingForegroundRestart {
93+ pendingForegroundRestart = false
94+ LogManager . shared. log (
95+ category: . general,
96+ message: " [LA] didBecomeActive: running deferred foreground restart (appState= \( appState) , activities= \( existing) ) "
97+ )
98+ performForegroundRestart ( )
9399 return
94100 }
95- LogManager . shared. log ( category: . general, message: " [LA] didBecomeActive: calling startFromCurrentState, dismissedByUser= \( dismissedByUser) " , isDebug: true )
101+ LogManager . shared. log ( category: . general, message: " [LA] didBecomeActive: startFromCurrentState (appState= \( appState ) , activities= \( existing ) , current= \( current ? . id ?? " nil " ) , dismissedByUser=\( dismissedByUser) ) " , isDebug: true )
96102 Task { @MainActor in
97103 self . startFromCurrentState ( )
98104 }
@@ -105,24 +111,30 @@ final class LiveActivityManager {
105111 let renewBy = Storage . shared. laRenewBy. value
106112 let now = Date ( ) . timeIntervalSince1970
107113 let overlayIsShowing = renewBy > 0 && now >= renewBy - LiveActivityManager. renewalWarning
114+ let appState = UIApplication . shared. applicationState. rawValue
115+ let existing = Activity < GlucoseLiveActivityAttributes > . activities. count
108116
109117 LogManager . shared. log (
110118 category: . general,
111- message: " [LA] foreground: renewalFailed= \( renewalFailed) , overlayShowing= \( overlayIsShowing) , current= \( current? . id ?? " nil " ) , dismissedByUser= \( dismissedByUser) , renewBy= \( renewBy) , now= \( now) "
119+ message: " [LA] foreground: appState= \( appState ) , activities= \( existing ) , renewalFailed=\( renewalFailed) , overlayShowing= \( overlayIsShowing) , current= \( current? . id ?? " nil " ) , dismissedByUser= \( dismissedByUser) , renewBy= \( renewBy) , now= \( now) "
112120 )
113121
114122 guard renewalFailed || overlayIsShowing else {
115123 LogManager . shared. log ( category: . general, message: " [LA] foreground: no action needed (not in renewal window) " )
116124 return
117125 }
118126
127+ // willEnterForegroundNotification fires before the scene reaches
128+ // foregroundActive — Activity.request() returns `visibility` during
129+ // this window. Defer the actual restart to didBecomeActive.
130+ pendingForegroundRestart = true
119131 LogManager . shared. log (
120132 category: . general,
121- message: " [LA] ending stale LA and restarting (renewalFailed= \( renewalFailed) , overlayShowing= \( overlayIsShowing) ) "
133+ message: " [LA] foreground: scheduling restart on next didBecomeActive (renewalFailed= \( renewalFailed) , overlayShowing= \( overlayIsShowing) ) "
122134 )
135+ }
123136
124- skipNextDidBecomeActive = true
125-
137+ private func performForegroundRestart( ) {
126138 // Mark restart intent BEFORE clearing storage flags, so any late .dismissed
127139 // from the old activity is never misclassified as a user swipe.
128140 endingForRestart = true
@@ -249,14 +261,22 @@ final class LiveActivityManager {
249261 /// a .dismissed delivery triggered by our own end() call is never misclassified as a
250262 /// user swipe — regardless of the order in which the MainActor executes the two writes.
251263 private var endingForRestart = false
252- /// Set by handleForeground() when it takes ownership of the restart sequence.
253- /// Prevents handleDidBecomeActive() from racing with an in-flight end+restart.
254- private var skipNextDidBecomeActive = false
264+ /// Set by handleForeground() when the renewal window has been detected.
265+ /// The actual end+restart is run from handleDidBecomeActive() because
266+ /// Activity.request() returns `visibility` during willEnterForeground.
267+ private var pendingForegroundRestart = false
255268
256269 // MARK: - Public API
257270
258271 func startIfNeeded( ) {
259- guard ActivityAuthorizationInfo ( ) . areActivitiesEnabled else {
272+ let authorized = ActivityAuthorizationInfo ( ) . areActivitiesEnabled
273+ let existingCount = Activity < GlucoseLiveActivityAttributes > . activities. count
274+ LogManager . shared. log (
275+ category: . general,
276+ message: " [LA] startIfNeeded: authorized= \( authorized) , activities= \( existingCount) , current= \( current? . id ?? " nil " ) , dismissedByUser= \( dismissedByUser) , laEnabled= \( Storage . shared. laEnabled. value) " ,
277+ isDebug: true
278+ )
279+ guard authorized else {
260280 LogManager . shared. log ( category: . general, message: " Live Activity not authorized " )
261281 return
262282 }
@@ -335,7 +355,12 @@ final class LiveActivityManager {
335355 Storage . shared. laRenewalFailed. value = false
336356 LogManager . shared. log ( category: . general, message: " Live Activity started id= \( activity. id) " )
337357 } catch {
338- LogManager . shared. log ( category: . general, message: " Live Activity failed to start: \( error) " )
358+ let ns = error as NSError
359+ let scene = isAppVisibleForLiveActivityStart ( )
360+ LogManager . shared. log (
361+ category: . general,
362+ message: " Live Activity failed to start: \( error) domain= \( ns. domain) code= \( ns. code) — authorized= \( ActivityAuthorizationInfo ( ) . areActivitiesEnabled) , sceneActive= \( scene) , activities= \( Activity < GlucoseLiveActivityAttributes > . activities. count) "
363+ )
339364 }
340365 }
341366
@@ -344,6 +369,10 @@ final class LiveActivityManager {
344369 /// Does not clear laEnabled — the user's preference is preserved for relaunch.
345370 func endOnTerminate( ) {
346371 guard let activity = current else { return }
372+ // Flag the end as system-initiated so the state observer does not
373+ // classify the resulting `.dismissed` as a user swipe (laRenewBy is
374+ // cleared below, which would otherwise make pastDeadline=false).
375+ endingForRestart = true
347376 current = nil
348377 Storage . shared. laRenewBy. value = 0
349378 LALivenessStore . clear ( )
@@ -399,6 +428,10 @@ final class LiveActivityManager {
399428 func forceRestart( ) {
400429 guard Storage . shared. laEnabled. value else { return }
401430 LogManager . shared. log ( category: . general, message: " [LA] forceRestart called " )
431+ // Mark as system-initiated so any residual `.dismissed` delivered from
432+ // the cancelled state observer stream cannot flip dismissedByUser=true
433+ // and spoil the freshly started LA.
434+ endingForRestart = true
402435 dismissedByUser = false
403436 Storage . shared. laRenewBy. value = 0
404437 Storage . shared. laRenewalFailed. value = false
@@ -515,7 +548,11 @@ final class LiveActivityManager {
515548 // Renewal failed — deadline was never written, so no rollback needed.
516549 let isFirstFailure = !Storage. shared. laRenewalFailed. value
517550 Storage . shared. laRenewalFailed. value = true
518- LogManager . shared. log ( category: . general, message: " [LA] renewal failed, keeping existing LA: \( error) " )
551+ let ns = error as NSError
552+ LogManager . shared. log (
553+ category: . general,
554+ message: " [LA] renewal failed, keeping existing LA: \( error) domain= \( ns. domain) code= \( ns. code) — authorized= \( ActivityAuthorizationInfo ( ) . areActivitiesEnabled) , activities= \( Activity < GlucoseLiveActivityAttributes > . activities. count) "
555+ )
519556 if isFirstFailure {
520557 scheduleRenewalFailedNotification ( )
521558 }
@@ -557,9 +594,17 @@ final class LiveActivityManager {
557594 // WatchConnectivityManager.shared.send(snapshot: snapshot)
558595
559596 // LA update: gated on LA being active, snapshot having changed, and activities enabled.
560- guard Storage . shared. laEnabled. value, !dismissedByUser else { return }
597+ if !Storage. shared. laEnabled. value {
598+ LogManager . shared. log ( category: . general, message: " [LA] refresh: LA update skipped — laEnabled=false reason= \( reason) " , isDebug: true )
599+ return
600+ }
601+ if dismissedByUser {
602+ LogManager . shared. log ( category: . general, message: " [LA] refresh: LA update skipped — dismissedByUser=true reason= \( reason) " )
603+ return
604+ }
561605 guard !snapshotUnchanged || forceRefreshNeeded else { return }
562606 guard ActivityAuthorizationInfo ( ) . areActivitiesEnabled else {
607+ LogManager . shared. log ( category: . general, message: " [LA] refresh: LA update skipped — areActivitiesEnabled=false reason= \( reason) " )
563608 return
564609 }
565610 if current == nil , let existing = Activity < GlucoseLiveActivityAttributes > . activities. first {
@@ -632,6 +677,12 @@ final class LiveActivityManager {
632677
633678 if isForeground {
634679 await activity. update ( content)
680+ } else {
681+ LogManager . shared. log (
682+ category: . general,
683+ message: " [LA] update seq= \( nextSeq) — app backgrounded, direct ActivityKit update skipped, relying on APNs " ,
684+ isDebug: true
685+ )
635686 }
636687
637688 if Task . isCancelled { return }
@@ -646,6 +697,11 @@ final class LiveActivityManager {
646697
647698 if let token = pushToken {
648699 await APNSClient . shared. sendLiveActivityUpdate ( pushToken: token, state: state)
700+ } else {
701+ LogManager . shared. log (
702+ category: . general,
703+ message: " [LA] update seq= \( nextSeq) reason= \( reason) — no push token yet, APNs skipped "
704+ )
649705 }
650706 }
651707 }
@@ -668,25 +724,45 @@ final class LiveActivityManager {
668724 private func bind( to activity: Activity < GlucoseLiveActivityAttributes > , logReason: String ) {
669725 if current? . id == activity. id { return }
670726 current = activity
727+ let wasEndingForRestart = endingForRestart
671728 dismissedByUser = false
672729 endingForRestart = false
673730 attachStateObserver ( to: activity)
674- LogManager . shared. log ( category: . general, message: " Live Activity bound id= \( activity. id) ( \( logReason) ) " , isDebug: true )
731+ LogManager . shared. log (
732+ category: . general,
733+ message: " Live Activity bound id= \( activity. id) state= \( activity. activityState) ( \( logReason) ) — endingForRestart cleared (was \( wasEndingForRestart) ) " ,
734+ isDebug: true
735+ )
675736 observePushToken ( for: activity)
676737 }
677738
678739 private func observePushToken( for activity: Activity < GlucoseLiveActivityAttributes > ) {
679740 tokenObservationTask? . cancel ( )
741+ let activityID = activity. id
680742 tokenObservationTask = Task {
681743 for await tokenData in activity. pushTokenUpdates {
682744 let token = tokenData. map { String ( format: " %02x " , $0) } . joined ( )
745+ let previousTail = self . pushToken. map { String ( $0. suffix ( 8 ) ) } ?? " nil "
746+ let tail = String ( token. suffix ( 8 ) )
683747 self . pushToken = token
684- LogManager . shared. log ( category: . general, message: " Live Activity push token received " , isDebug: true )
748+ LogManager . shared. log (
749+ category: . general,
750+ message: " [LA] push token received id= \( activityID) token=… \( tail) (prev=… \( previousTail) ) "
751+ )
685752 }
686753 }
687754 }
688755
689756 func handleExpiredToken( ) {
757+ let existing = Activity < GlucoseLiveActivityAttributes > . activities. count
758+ LogManager . shared. log (
759+ category: . general,
760+ message: " [LA] handleExpiredToken: current= \( current? . id ?? " nil " ) , activities= \( existing) , dismissedByUser= \( dismissedByUser) — marking endingForRestart and ending "
761+ )
762+ // Mark as system-initiated so the `.dismissed` delivered by end()
763+ // is not classified as a user swipe — that would set dismissedByUser=true
764+ // and block the auto-restart promised by the comment below.
765+ endingForRestart = true
690766 end ( )
691767 // Activity will restart on next BG refresh via refreshFromCurrentState()
692768 }
0 commit comments