BGAppRefreshTask audio recovery; LA expiry notification; code quality#570
Merged
bjorkert merged 23 commits intoloopandlearn:live-activityfrom Mar 22, 2026
Merged
BGAppRefreshTask audio recovery; LA expiry notification; code quality#570bjorkert merged 23 commits intoloopandlearn:live-activityfrom
bjorkert merged 23 commits intoloopandlearn:live-activityfrom
Conversation
Registers com.loopfollow.audiorefresh with BGTaskScheduler so iOS can wake the app every ~15 min to check if the silent audio session is still alive and restart it if not. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'fetch' to UIBackgroundModes so BGTaskScheduler.submit() doesn't
throw notPermitted on every background transition
- Call stopBackgroundTask() before startBackgroundTask() in the refresh
handler to prevent accumulating duplicate AVAudioSession observers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- startBackgroundTask() now removes the old observer before adding, making it idempotent and preventing duplicate interrupt callbacks - Add 'audio restart initiated' log after restart so success is visible without debug mode - Temporarily make 'Silent audio playing' log always visible for testing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forgotten stub.
- When renewIfNeeded fails in the background (app can't start a new LA because it's not visible), schedule a local notification on the first failure: "Live Activity Expiring — Open LoopFollow to restart." Subsequent failures in the same cycle are suppressed. Notification is cancelled if renewal later succeeds or forceRestart is called. - In attachStateObserver, distinguish iOS force-dismiss (laRenewalFailed == true) from user swipe (laRenewalFailed == false). OS-dismissed LAs no longer set dismissedByUser, so opening the app triggers auto-restart as expected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Force-quitting an app kills its Live Activities, so cold-launch via LA tap only occurs when iOS terminates the app — in which case scene(_:openURLContexts:) already handles navigation correctly via DispatchQueue.main.async. The flag was never set and never needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BackgroundRefreshManager: all logs → .taskScheduler - AppDelegate: APNs registration/notification logs → .apns - APNSClient: all logs → .apns - BackgroundTaskAudio: restore isDebug:true on silent audio log; fix double blank line - LiveActivityManager: fix trailing whitespace; remove double blank line; SwiftFormat - GlucoseSnapshotBuilder: fix file header (date → standard LoopFollow header) - LoopFollowLiveActivity: remove dead commented-out activityID property - SwiftFormat applied across all reviewed LiveActivity/, Storage/, extension files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents truncation toward zero (e.g. 179.9 → 179); now correctly rounds to nearest integer. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BackgroundRefreshManager: guard against double setTaskCompleted if the expiration handler fires while the main-queue block is in-flight. Apple documents calling setTaskCompleted more than once as a programming error. LiveActivityManager.renewIfNeeded: write laRenewBy to Storage only after Activity.request succeeds, eliminating the narrow window where a crash between the write and the request could leave the deadline permanently stuck in the future. No rollback needed on failure. The fresh snapshot is built via withRenewalOverlay(false) directly rather than re-running the builder, since the caller already has a current snapshot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
BGAppRefreshTask — automatic silent audio recovery
BackgroundRefreshManager.swift(new): singleton that registers aBGAppRefreshTaskhandler (com.loopfollow.audiorefresh). When iOS wakes the app, it checks whether the silent audio player is still alive and restarts it if not, then reschedules itself for 15 minutes later.Info.plist: addedcom.loopfollow.audiorefreshtoBGTaskSchedulerPermittedIdentifiers; addedfetchtoUIBackgroundModes(required forBGTaskScheduler.submitto succeed).AppDelegate: callsBackgroundRefreshManager.shared.register()beforereturn trueindidFinishLaunchingWithOptions.MainViewController: callsBackgroundRefreshManager.shared.scheduleRefresh()afterbackgroundTask.startBackgroundTask().BackgroundTaskAudio: fixed idempotency —startBackgroundTask()now removes the interruption observer before re-adding it, preventing duplicate callbacks when audio restarts.setTaskCompletedrace fixed: the expiration handler and theDispatchQueue.main.asyncblock are now guarded by a sharedcompletedflag — whichever runs first wins; the second is a no-op. Apple documents callingsetTaskCompletedmore than once as a programming error.This adds a third recovery layer alongside the existing AVAudioSession interruption handler and BackgroundAlertManager watchdog notifications.
Extended data slots — all configurable metrics now populated
StorageCurrentGlucoseStateProviderpreviously only exposed core glucose, IOB, and COB. It now implements the fullCurrentGlucoseStateProvidingprotocol, sourcing all 20+ extended metrics fromStorage.sharedandObservable.shared:Observable.shared.overrideObservable.shared.deviceRecBolusObservable.shared.deviceBatteryLevelObservable.shared.pumpBatteryLevelStorage.shared.lastBasalStorage.shared.lastPumpReservoirUStorage.shared.lastAutosensStorage.shared.lastTddStorage.shared.lastTargetLowMgdl/HighStorage.shared.lastIsfMgdlPerUStorage.shared.lastCarbRatioStorage.shared.lastCarbsTodayStorage.shared.lastProfileNameStorage.shared.sageInsertTime/cageInsertTime/iageInsertTimeStorage.shared.lastMinBgMgdl/MaxBgMgdlisNotLoopingObservable.shared.isNotLoopingshowRenewalOverlaylaRenewByvsrenewalWarningThis means all 22 configurable Live Activity slots (IOB, COB, basal, pump, target, ISF, CR, SAGE/CAGE/IAGE, etc.) now display live data rather than "—".
Live Activity expiry notification + dismissed-by-user distinction
LiveActivityManager: on first renewal failure, schedules a local notification ("Live Activity Expiring — Open LoopFollow to restart"). Notification is cancelled on successful renewal orforceRestart().attachStateObserver: on.dismissed, checkslaRenewalFailedto distinguish iOS force-dismissing after the 8-hour limit (auto-restart allowed) from the user manually swiping away (auto-restart blocked until explicit restart via button or App Intent).laRenewByis now written to Storage only afterActivity.request()succeeds, eliminating the narrow window where a crash between the write and the request could leave the deadline permanently stuck in the future. No rollback needed on failure.GlucoseSnapshot.withRenewalOverlay(_:)helperReplaces the manual field-by-field copy in
renewIfNeededwith a single method call.startIfNeededseed snapshot improvementOn LA start, now attempts
GlucoseSnapshotBuilder.build()first (all extended fields populated), falls back to the persisted store, then to the zero seed.Dead code removal
SceneDelegate: removedpendingLATapNavigationflag — it was never set totrue.LoopFollowLiveActivity: removed commented-out/* let activityID: String */property.LiveActivitySlotConfig.swift: deleted (already removed upstream; resolved here).Log category fixes
BackgroundRefreshManager: all logs →.taskSchedulerAppDelegate+APNSClient: APNs-related logs →.apnsBug fix
DeviceStatusLoop:Int(prediction.last!)→Int(round(prediction.last!))— prevents truncation toward zero on prediction display values.SwiftFormat pass
Applied SwiftFormat (Swift 6.2) across all changed files.
Test plan
LoopFollowandLoopFollowLAExtensiontargets🤖 Generated with Claude Code