Harden shared services for headless (CarPlay-only) launches#114
Open
einsteinx2 wants to merge 9 commits into
Open
Harden shared services for headless (CarPlay-only) launches#114einsteinx2 wants to merge 9 commits into
einsteinx2 wants to merge 9 commits into
Conversation
CarPlay adds a second scene that can connect before - or without - the phone's window scene, so the shared layer must not assume one exists: - AppBootstrap.sceneDidConnect() is idempotent and runs for whichever scene connects first (download-lane restore + playback resume) - SceneDelegate.shared is optional and filters for the window scene instead of force-casting connectedScenes.first - ServerSwitcher takes NetworkMonitor via its initializer instead of reaching up through SceneDelegate.shared, and optional-chains its remaining UI cleanup - HUD.show skips when there is no key window, and a new HUD.banner wraps the ProgressHUD banners reachable from headless code paths (NetworkMonitor, PlaybackCoordinator, DownloadEngine) - NowPlayingService reports playbackRate 0 while paused (the CarPlay/ lock-screen progress bar no longer advances when paused), renders artwork at the requested size, and exposes currentNowPlayingInfo() for tests Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
New CPTemplateApplicationScene role wired into both targets: - com.apple.developer.carplay-audio in both entitlements files, now referenced via CODE_SIGN_ENTITLEMENTS in the app build configs - CarPlay scene config in both Info.plists; multiple scenes enabled (required so the phone scene survives the car connecting); AppDelegate routes the .carTemplateApplication role to CarPlaySceneDelegate The car UI is a template layer over the existing Store reads and PlaybackCoordinator calls the phone uses - no phone UI changes: - Tabs: Library (Folders/Artists with A-Z index and media-folder picker, Bookmarks), Playlists (Play Queue/Local/Server), Downloads (Folders/ Artists/Albums/Songs), Discover (quick album lists, Shuffle All) - Now Playing: CPNowPlayingTemplate with repeat/shuffle buttons and an Up Next queue screen; metadata/transport ride the existing NowPlayingService surfaces (single MPNowPlayingInfoCenter writer) - Screens are CP-free section builders (cache-first, async loader refresh, offline row disabling via isAvailableOffline); a factory maps them to CPListItems, re-rendering art for the car display scale and clamping to the head unit's runtime list limits - CarPlayManager routes row taps to PlaybackCoordinator, refreshes live templates on playback/queue/download notifications, rebuilds on server switch and offline transitions (Downloads-first tab order offline), and auto-disables jukebox mode on connect (the car would be silent) - CPInterfaceController is wrapped in a protocol seam so the manager and screens are unit-testable without a car session Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
…phone scene The launch offline check and the goOnline/goOffline transition handling lived in the phone SceneDelegate, so a CarPlay-only launch never entered offline mode and never ran the periodic server check. They now live in OfflineModeCoordinator, constructed with the service graph and invoked by whichever scene appears (phone sceneDidBecomeActive or CarPlayManager.connect): - performLaunchOfflineCheckIfNeeded() runs once per process and stashes the explanatory alert text; the phone scene presents it whenever it appears (possibly long after a car-first launch) - ServerChecker moves behind the coordinator (created lazily because the coordinator itself is built before container registrations exist) - SceneDelegate keeps only alert presentation and the static launchOfflineAlertMessage truth table the tests reference; its unused service injections are dropped Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
CarPlay audio apps can't show keyboard search (CPSearchTemplate is navigation-only), so search in the car is voice-driven: 'Hey Siri, play <song/album/artist/playlist> in iSub'. Works anywhere Siri does, not just in the car. - PlayMediaIntentHandler resolves INMediaSearch against the server (search/search2/search3 chosen by the same capability flags the phone search screen uses) with a local fallback over downloaded songs/ albums/artists and playlists when offline; matched songs are persisted so the play-queue JOIN can hydrate them - handle() maps the match onto the existing play paths (single song, tag album, recursive folder/tag-artist gather, local/server playlist), honoring playShuffled and supporting bare resume requests - AppDelegate returns the handler from application(_:handlerFor:); INIntentsSupported declared in both Info.plists (in-app handling also requires multi-scene support, already enabled for CarPlay) - Siri entitlement added to both entitlements files - Library tab shows the 'Hey Siri' assistant cell (CPAssistantCellConfiguration, .playMedia) Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
- FakeCarPlayInterface records the template stack so CarPlayManagerTests can drive a real manager: tab order online vs offline (Downloads-first), live reorder on the offline transition, jukebox auto-disable on connect, play actions filling the queue and landing on Now Playing, back- navigation pruning, server-switch pop-to-root, the template depth guard, and the Up Next pop-back-to-Now-Playing path - CarPlayScreenTests cover the CP-free section builders against a seeded in-memory store: row content and tap payloads, video filtering with position alignment, offline row disabling, internal queues excluded from local playlists, downloads root states, and the list-clamping math - OfflineModeCoordinatorTests cover the once-per-process launch check, the pending-alert handoff to the phone scene, and the goOffline jukebox side effect; OfflineModeCoordinator now takes the NetworkStatus seam instead of the concrete NetworkMonitor so tests inject FakeNetworkStatus - NowPlayingServiceTests assert currentNowPlayingInfo()'s paused rate 0 - docs/carplay.md describes the architecture, non-goals, dev/test flow, and gotchas Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
The depth-guard pop inside push(screen:) fires the stack-changed prune; recording the (screen, template) pair before the template is actually in the stack let that prune sweep the new entry away, cancelling its load and dropping its refresh wiring while its template stayed visible. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
A song queued twice lit up both rows; the queue screen knows each row's exact queue position, so pass it through as an override. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
XCUITest cannot drive the real car screen: CarPlay templates render out-of-process in the system's CarPlay host, there are no public automation APIs for it, and headless CI simulators cannot attach the CarPlay display at all. The testable boundary is everything up to that hand-off, so a -CARPLAY launch argument (UI test mode only) now runs the real CarPlayManager in the app process against the same interface seam the unit tests use and renders its live template stack in a native mirror window (CarPlayUITestMirror.swift). The mirror shows the tab strip, real CPListItem rows (titles, details, disabled state, playing indicator, empty-state variants) and a Now Playing panel; tapping a row invokes the actual CPListItem handler, driving the real coordinator, stores, loaders, and audio engine. Phone/Car toggle buttons let a test hop between both UIs in one launch. Dormant outside UI test mode. CarPlayUITests (9 tests) covers: online tab order and Library rows; Folders drill-down to song playback with Up Next queue jumps and phone-player agreement; the media-folder picker; tag artist/album play all; server playlist playback plus the Play Queue screen; Discover quick albums with Load More and Shuffle All; offline Downloads-first reorder and empty states; jukebox auto-disable with a request-log proof that playback streams locally; and bookmark playback from the car. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
The mirror's 0.5s poll called reloadData() unconditionally, so cells were
destroyed and recreated continuously; XCUITest taps raced the swaps and
failed intermittently ('activation point invalid', eaten taps) in 6 of
the 9 CarPlay tests. The poll now renders only when a content signature
(template identity, rows' text/detail/enabled/playing, tabs, depth,
now-playing title — images excluded on purpose) actually changes, so the
table is static between real updates.
Belt-and-braces on the test side: car row taps retry once if the row
stays front-and-center (every car tap navigates or starts playback), with
a no-retry variant for the media-folder picker flow where the tapped text
legitimately recurs on the destination screen.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS
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.
CarPlay adds a second scene that can connect before - or without - the
phone's window scene, so the shared layer must not assume one exists:
scene connects first (download-lane restore + playback resume)
instead of force-casting connectedScenes.first
reaching up through SceneDelegate.shared, and optional-chains its
remaining UI cleanup
wraps the ProgressHUD banners reachable from headless code paths
(NetworkMonitor, PlaybackCoordinator, DownloadEngine)
lock-screen progress bar no longer advances when paused), renders
artwork at the requested size, and exposes currentNowPlayingInfo()
for tests
Co-Authored-By: Claude Fable 5 [email protected]
Claude-Session: https://claude.ai/code/session_01JCT7pNgGdjGuwFehDWnVfS