ui: add argosy://launch deep link for external front-ends - #341
Conversation
Existing argosy:// deep links are keyed on the internal autoincrement game id, which an external caller cannot obtain. Add a launch host that accepts a ROM path, a RomM rom id or a game id, so front-ends like ES-DE can hand off a game while Argosy keeps ownership of the save/state sync lifecycle. Recognised intent extras are folded into the URI as query parameters, because ES-DE substitutes %ROM% only as a whole parameter value and cannot build the URI itself. handleDeepLink is now also called from onCreate; it was only wired to onNewIntent, so a deep link that started the process was dropped. Resolution reuses the existing pending-launch path into GameDetailScreen so the sync overlay and conflict prompts still render, which required threading the activity-scoped ArgosyViewModel through NavGraph: the screen previously resolved its own back-stack-scoped instance, so pendingLaunch was set on an object it never observed. Closes rommapp#339 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Im gonna need some time to unwrap this Russian nesting doll... probably won't be able to fully consider it until next weekend. What happens when you quit the game? Does it return to Argosy or ES-DE? ...or is there a short delay in Argosy before kicking back to ES-DE? What if you hit Home, doesn't it skip the save sync step? I feel like there's a lot of room for error with wrapping launcher calls like this rather than implementing it natively (though I understand ES-DE isn't so friendly to userland enhancement). Alternatively, you could toggle of Secure Saves and have Argosy attempt to preserve changes made when launching from other apps, though you'd lose session tracking data... |
|
My apologies for the delay on my end, I also got bit too busy these past few days. I wanted to record my screen so you can see how it's working end to end and I still intend to do that; Hoping to get back to you in the next couple fo days. |
Closes #339
Summary
Adds an
argosy://launchdeep link so an external front-end can hand a specific game to Argosy, while Argosy keeps ownership of the RomM save/state lifecycle.The three existing hosts (
game/,play/,apps) are all keyed on the internal autoincrement game id, which an outside caller has no way to obtain — a front-end only knows the ROM file it scanned.launchaccepts what such a caller actually has:Three things worth calling out, since they're the non-obvious parts:
Intent extras are folded into the URI. ES-DE substitutes its
%ROM%variable only when the variable is the entire value of a parameter, never interpolated inside a longer string —%DATA%=argosy://launch?path=%ROM%arrives with the literal text%ROM%. So callers pass the path as an intent extra andMainActivitypromotes it to a query parameter. Doing it throughUri.Builder.appendQueryParameteralso percent-encodes the value, which paths containing spaces need.Cold start.
handleDeepLinkwas only wired toonNewIntent, so a deep link that started the process was silently dropped. It's now called fromonCreatetoo. This also fixes the existinggame:///play:///appshosts on cold start.ViewModel scoping. Resolution deliberately reuses the existing pending-launch path (
initiateGameLaunch+ navigate toGameDetailScreen) rather than callingGameLaunchDelegatedirectly, so the sync overlay and the LocalModified / HardcoreConflict prompts still render. That required threading the activity-scopedArgosyViewModelthroughNavGraph:GameDetailScreenpreviously resolved its ownhiltViewModel()against the NavBackStackEntry, sopendingLaunchwas being set on an instance the screen never observed. I believe this meansargosy://play/{id}could not have worked either; nothing external could trigger it before, so it looks like it went unnoticed.Path resolution tries exact
getByPathfirst, then falls back to a file-name match, since two front-ends can reach the same ROM by different roots. A file name matching more than one installed game resolves toAmbiguousrather than picking one — launching the wrong game would write its save to the wrong server slot.Behavior changes
argosy://launchhost. No existing URI shape changes.argosy://game/{id},argosy://play/{id}andargosy://appswere dropped when the intent started the process; they now behave as they already did when the app was warm.argosy://play/{id}becomes functional. TheGameDetailScreenViewModel-scoping fix means the pending launch is now observed by the screen that consumes it.NavGraphtakes a new requiredargosyViewModelparameter. Internal signature change, single call site.launchlink that resolves to nothing (unknown path, or a file name matching several games) a toast is shown and nothing is launched.IllegalArgumentException: Navigation graph has not been set.Hot paths
Yes, on the launch path, and only when a
launchdeep link is present:getById,getByRommIdorgetByPath, all indexed. The file-name fallback callsgetGamesWithLocalPathInfo(), which is bounded by installed games (games with a non-nulllocalPath), not library size, and only runs when the exact path misses.awaitConnectionIfSyncing()suspends up to 10s forConnectionState.Connectedwhen save sync is enabled, so a cold-start deep link doesn't launch before the pre-launch pull can run. It issues no requests of its own; on timeout it proceeds anyway and logs. Skipped entirely when already connected or when save sync is off.Nothing added to frame or sync paths. No new network calls.
Testing evidence
Hardware: Pixel 9 Pro XL, Android 17, unrooted. RomM 5.1.0. ES-DE 3.4.1-58 driving Argosy, RetroArch
com.retroarch1.22.2 with the snes9x core, SNES pinned to external RetroArch.Flow driven end to end, repeatedly:
Plumbing, before touching real saves — a dummy ROM with spaces in the name (
Test Game With Spaces.zip) launched from ES-DE. ES-DE's log shows the substitution, Argosy shows what arrived:Confirms find-rule resolution, extras promotion, percent-encoding, spaces surviving, and the resolver correctly declining an unknown game.
Real launch — a downloaded game launched from ES-DE, cold start:
Save sync intact through the deep-link path — pre-launch pull, then session-end push:
Verified in the DB afterwards:
state_cacherowsyncStatus='SYNCED',pending_sync_queue = 0.Full cross-device round-trip — played on desktop RetroArch (synced to the same RomM), launched on Android via ES-DE and the state auto-loaded with desktop progress; played on Android, exited cleanly, then desktop picked up the Android state. Both directions, repeatedly.
Cold-start regression — the crash this fixes (
Navigation graph has not been set) reproduced reliably before the readiness gate; not reproducible after ~10 cold launches. Argosy takes ~19s to a composed nav graph on a 28k-game library, which is why the cap is generous.Existing hosts —
argosy://appsstill handled, verified from the log.Unit tests: 7 new cases on
ResolveDeepLinkGameUseCasecovering each identifier arm, the file-name fallback, and the ambiguous-match refusal.DeepLinkParseris not covered — it depends onandroid.net.Uriand there's no Robolectric in the JVM test setup; happy to add coverage if you'd like it wired up.AI assistance
Written primarily by Claude Code, directed and reviewed by me. Design decisions (routing through the existing pending-launch path rather than calling
GameLaunchDelegatedirectly; refusing ambiguous file-name matches instead of guessing) were discussed and chosen deliberately. All the on-hardware verification above is real: driven on my device against my RomM server, not inferred. The three bugs listed under Behavior changes were found by running it, not by reading the code.Checklist