feat:Add support for installing native Android game builds (Steam Frame) - #1742
feat:Add support for installing native Android game builds (Steam Frame)#1742moi952 wants to merge 6 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis change adds native Android game support through Android depot compatibility, persistent container platform selection, APK installation and launch handling, Android-specific UI behavior, and platform-aware download and uninstall flows. ChangesAndroid platform support
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Player
participant MainViewModel
participant SteamService
participant AndroidGameLauncher
participant UpdateInstaller
Player->>MainViewModel: launch Android container
MainViewModel->>AndroidGameLauncher: installAndLaunch(gameId)
AndroidGameLauncher->>UpdateInstaller: install APK when package is absent
AndroidGameLauncher-->>MainViewModel: InstallStarted, Launched, or Failed
MainViewModel-->>Player: show installation or launch status
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt (2)
55-66: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winOBB placement failure is silently swallowed.
copyObbFilesonly logs a warning on failure;installAndLaunchcalls it and proceeds withUpdateInstaller.installApkregardless of the outcome. Per the kdoc, this failure mode ("games that need their OBB will fail to start") is expected on Android 11+ without broad storage access, but the user gets zero indication why the freshly-installed game won't launch — this is partial error propagation into a confusing dead end.Returning a status from
copyObbFilesand reflecting it inResult(or at least a distinct log/snackbar) would let the caller surface something more actionable than a silent black-box failure.♻️ Sketch
- private fun copyObbFiles(gameId: Int, packageName: String) { + private fun copyObbFiles(gameId: Int, packageName: String): Boolean { val obbFiles = findObbFiles(gameId) - if (obbFiles.isEmpty()) return + if (obbFiles.isEmpty()) return true try { val obbRoot = File(Environment.getExternalStorageDirectory(), "Android/obb/$packageName") obbRoot.mkdirs() obbFiles.forEach { src -> src.copyTo(File(obbRoot, src.name), overwrite = true) } Timber.tag(TAG).i("Copied ${obbFiles.size} OBB file(s) to ${obbRoot.absolutePath}") + return true } catch (e: Exception) { Timber.tag(TAG).w(e, "Could not place OBB files for $packageName (needs broader storage access on Android 11+)") + return false } }Also applies to: 102-106
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt` around lines 55 - 66, Update copyObbFiles to return an explicit success/failure status instead of swallowing placement errors, and have installAndLaunch inspect that result before or alongside UpdateInstaller.installApk. Surface a distinct actionable failure through the existing Result or user-facing error path, while preserving the current successful-copy behavior and warning details.
20-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRepeated disk walk +
PackageManagerparse with no caching.
findApkFile/findObbFilesrecursively walk the entire game install directory, andresolvePackageName/isGameInstalledre-run this walk plusgetPackageArchiveInfoparsing on every call. These are non-suspend, synchronous functions;SteamAppScreen.isInstalled()andgetAndroidPackageName()call into them directly and are the kind of method typically invoked repeatedly from UI/state-observation code (e.g. on everyonStateChanged()inobserveGameState). For large asset trees this becomes a non-trivial, repeated I/O cost on every check.Also, if a game directory ever contains more than one
.apk(leftover from an update, etc.),firstOrNullpicks an arbitrary one with no validation.Consider memoizing the resolved package name per
gameId(invalidate when a new APK is installed/re-downloaded).♻️ Example caching approach
+ private val packageNameCache = mutableMapOf<Int, String?>() + fun resolvePackageName(context: Context, gameId: Int): String? { - val apk = findApkFile(gameId) ?: return null - return getApkPackageName(context, apk) + return packageNameCache.getOrPut(gameId) { + val apk = findApkFile(gameId) ?: return@getOrPut null + getApkPackageName(context, apk) + } } + + /** Call after a fresh download/uninstall so a stale cached name isn't reused. */ + fun invalidateCache(gameId: Int) { + packageNameCache.remove(gameId) + }Also applies to: 68-78
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt` around lines 20 - 30, Memoize the resolved package name per gameId in the AndroidGameLauncher flow, reusing the cached value from resolvePackageName/isGameInstalled instead of repeating findApkFile, findObbFiles, and getPackageArchiveInfo during UI state checks. Invalidate the corresponding cache entry whenever APK installation or re-download completes. Replace arbitrary firstOrNull APK selection with deterministic validation that handles multiple APKs safely before resolving the package.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 1422-1427: Update SteamService.isAndroidPlatform to avoid
force-unwrapping instance when creating ContainerManager. Safely access the
service context and return false when instance is null, while preserving the
existing platform comparison when the service is running.
In
`@app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt`:
- Around line 1248-1254: Update the onPreviewKeyEvent handler’s
Key.ButtonR1/Key.ButtonR2 and Key.ButtonL1/Key.ButtonL2 branches to ignore
shoulder-button navigation when isAndroidPlatform is true, while preserving
event consumption and existing tab cycling for non-Android platforms.
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt`:
- Around line 872-897: Update saveContainerConfig so that when platformChanged
and the existing container platform is Android and the app is installed, call
AndroidGameLauncher.requestUninstall before SteamService.deleteApp; preserve the
existing reinstall and redownload flow for other platform switches.
In `@app/src/main/res/xml/file_provider_paths.xml`:
- Around line 6-9: Replace the broad root-path entry in the FileProvider paths
configuration with narrowly scoped provider paths for the internal app data
directory, external app directory, and external SD-volume install roots used by
SteamService.allInstallPaths. Remove the catch-all access while preserving
support for downloaded Android game APK installation.
---
Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt`:
- Around line 55-66: Update copyObbFiles to return an explicit success/failure
status instead of swallowing placement errors, and have installAndLaunch inspect
that result before or alongside UpdateInstaller.installApk. Surface a distinct
actionable failure through the existing Result or user-facing error path, while
preserving the current successful-copy behavior and warning details.
- Around line 20-30: Memoize the resolved package name per gameId in the
AndroidGameLauncher flow, reusing the cached value from
resolvePackageName/isGameInstalled instead of repeating findApkFile,
findObbFiles, and getPackageArchiveInfo during UI state checks. Invalidate the
corresponding cache entry whenever APK installation or re-download completes.
Replace arbitrary firstOrNull APK selection with deterministic validation that
handles multiple APKs safely before resolving the package.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 65e44b76-89db-40cc-9924-61b27bd95227
📒 Files selected for processing (23)
app/src/main/AndroidManifest.xmlapp/src/main/java/app/gamenative/data/DepotInfo.ktapp/src/main/java/app/gamenative/enums/OS.ktapp/src/main/java/app/gamenative/service/DownloadService.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/PluviaMain.ktapp/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigState.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.ktapp/src/main/java/app/gamenative/ui/model/MainViewModel.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/AndroidGameLauncher.ktapp/src/main/java/app/gamenative/utils/ContainerUtils.ktapp/src/main/java/app/gamenative/utils/UpdateInstaller.ktapp/src/main/java/com/winlator/container/Container.javaapp/src/main/java/com/winlator/container/ContainerData.ktapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values/strings.xmlapp/src/main/res/xml/file_provider_paths.xmlapp/src/modern/AndroidManifest.xmlapp/src/modernXr/AndroidManifest.xml
There was a problem hiding this comment.
1 issue found across 23 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/enums/OS.kt">
<violation number="1" location="app/src/main/java/app/gamenative/enums/OS.kt:11">
P3: Android depot selection depends on this enum being recognized both from Steam's `"android"` metadata and from the persisted bitmask, but neither path has regression coverage. A focused test for `OS.from("android")` and `OS.from(OS.code(EnumSet.of(OS.android)))` would protect the new opt-in flow from parser or serialization regressions.</violation>
</file>
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
Re-trigger cubic
|
While testing, we found that if you tapped "Uninstall" on a Steam Frame / Lepton (Android) game and then cancelled the system's confirmation popup, GameNative would get stuck showing a "Deleting..." popup — and it would follow you to whatever game page you opened next, effectively freezing that part of the app for a couple of minutes. Two things were wrong: GameNative had no way to know right away that you'd hit "Cancel", it could only wait a fixed 2 minutes to give up, so cancelling looked like a freeze. Metro Awakening works, but maybe they have a license verification system, or perhaps they released a version that prevents play until a certain date? I don't know, but in any case, THE CONTROLLERS WORK!!! |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt (1)
84-95: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winPrefer
externalCacheDirfor large game APKs to prevent storage exhaustion.Game APKs can be gigabytes in size. Staging them in
context.cacheDir(internal storage) risks filling up the device's internal storage and causingOutOfSpaceException. Consider preferringcontext.externalCacheDir, falling back tocontext.cacheDirif it's unavailable.♻️ Proposed fix
private fun stageApkForInstall(context: Context, gameId: Int, apk: File): File? { return try { - val stagingDir = File(context.cacheDir, "android_game_installs").apply { mkdirs() } + val cacheBase = context.externalCacheDir ?: context.cacheDir + val stagingDir = File(cacheBase, "android_game_installs").apply { mkdirs() } val staged = File(stagingDir, "$gameId.apk") apk.copyTo(staged, overwrite = true) staged🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt` around lines 84 - 95, Update stageApkForInstall to use context.externalCacheDir as the staging directory when available, with context.cacheDir as the fallback, while preserving the existing android_game_installs subdirectory, copy behavior, and error handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt`:
- Around line 1417-1426: Update the nested ExposedDropdownMenu in the dropdown
component to use expanded && enabled, and reset expanded whenever enabled
becomes false so a disabled dropdown cannot retain or reopen its menu.
In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt`:
- Around line 174-178: Update the activity lookup in the uninstall flow around
requestUninstall to unwrap ContextWrapper instances recursively until locating
the underlying ComponentActivity. Use the unwrapped activity for prompting,
while preserving the existing error log and false return when no
ComponentActivity can be found.
---
Nitpick comments:
In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt`:
- Around line 84-95: Update stageApkForInstall to use context.externalCacheDir
as the staging directory when available, with context.cacheDir as the fallback,
while preserving the existing android_game_installs subdirectory, copy behavior,
and error handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5a5b8c7d-fca9-4671-8975-4f05b7ef170f
📒 Files selected for processing (15)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/PluviaMain.ktapp/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.ktapp/src/main/java/app/gamenative/ui/model/MainViewModel.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/AndroidGameLauncher.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/utils/UpdateInstaller.ktapp/src/main/java/com/winlator/container/ContainerManager.javaapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values/strings.xmlapp/src/main/res/xml/file_provider_paths.xmlapp/src/test/java/app/gamenative/enums/OSTest.kt
🚧 Files skipped from review as they are similar to previous changes (8)
- app/src/main/res/values/strings.xml
- app/src/main/java/app/gamenative/ui/PluviaMain.kt
- app/src/main/res/values-fr/strings.xml
- app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
- app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
- app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
- app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
- app/src/main/java/app/gamenative/service/SteamService.kt
There was a problem hiding this comment.
1 issue found across 15 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/service/SteamService.kt">
<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:951">
P3: Android depot selection now depends on this language-resolution flag, but the test suite has no Android-mode coverage for preferred-language, English fallback, or neutral-depot cases, allowing a later change to silently produce zero Android depots. A focused Android parameterized/helper path in `SteamUtilsDepotLanguageTest` would lock down these three call-site changes.
(Based on your team's feedback about tests for complex depot-selection logic.) [FEEDBACK_USED]</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| val dlcAppIdsWithSingleDepots = getDlcAppIdsWithSingleDepot(depots) | ||
| val effectiveLanguage = SteamUtils.effectiveDepotLanguage( | ||
| depots, preferredLanguage, ownedDlc, licensedDepotIds, hasSteamUnlockedBranch, | ||
| depots, preferredLanguage, ownedDlc, licensedDepotIds, hasSteamUnlockedBranch, wantAndroid, |
There was a problem hiding this comment.
P3: Android depot selection now depends on this language-resolution flag, but the test suite has no Android-mode coverage for preferred-language, English fallback, or neutral-depot cases, allowing a later change to silently produce zero Android depots. A focused Android parameterized/helper path in SteamUtilsDepotLanguageTest would lock down these three call-site changes.
(Based on your team's feedback about tests for complex depot-selection logic.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamService.kt, line 951:
<comment>Android depot selection now depends on this language-resolution flag, but the test suite has no Android-mode coverage for preferred-language, English fallback, or neutral-depot cases, allowing a later change to silently produce zero Android depots. A focused Android parameterized/helper path in `SteamUtilsDepotLanguageTest` would lock down these three call-site changes.
(Based on your team's feedback about tests for complex depot-selection logic.) </comment>
<file context>
@@ -948,7 +948,7 @@ class SteamService : Service(), IChallengeUrlChanged {
val dlcAppIdsWithSingleDepots = getDlcAppIdsWithSingleDepot(depots)
val effectiveLanguage = SteamUtils.effectiveDepotLanguage(
- depots, preferredLanguage, ownedDlc, licensedDepotIds, hasSteamUnlockedBranch,
+ depots, preferredLanguage, ownedDlc, licensedDepotIds, hasSteamUnlockedBranch, wantAndroid,
)
val eligible = eligibleDepots(depots, effectiveLanguage, ownedDlc, licensedDepotIds, wantAndroid)
</file context>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt (1)
66-76: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPropagate OBB placement failures instead of reporting installation success.
copyObbFiles()swallows copy/storage failures, butinstallAndLaunch()still returnsInstallStarted. Games requiring OBB data can therefore become installed-but-unlaunchable; subsequent launches skip OBB copying because the package is already installed. Android 11+ scoped storage further restricts direct writes outside app-specific storage. (developer.android.com)Return a success/failure result from OBB placement, fail or clearly report the installation when placement fails, and provide a repair path for already-installed packages with missing OBBs.
Also applies to: 145-149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt` around lines 66 - 76, Update copyObbFiles() to return an explicit success/failure result instead of swallowing placement exceptions, and have installAndLaunch() propagate that failure rather than returning InstallStarted. Ensure already-installed packages with missing OBB data still invoke the placement/repair flow before launch, and clearly report failure when Android storage restrictions prevent copying.Source: MCP tools
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@app/src/main/java/app/gamenative/utils/AndroidGameLauncher.kt`:
- Around line 66-76: Update copyObbFiles() to return an explicit success/failure
result instead of swallowing placement exceptions, and have installAndLaunch()
propagate that failure rather than returning InstallStarted. Ensure
already-installed packages with missing OBB data still invoke the
placement/repair flow before launch, and clearly report failure when Android
storage restrictions prevent copying.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bb6d8319-255a-4ad3-9103-5fded78b6795
📒 Files selected for processing (5)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/AndroidGameLauncher.ktapp/src/main/java/app/gamenative/utils/ContainerUtils.ktapp/src/main/java/app/gamenative/utils/UpdateInstaller.kt
🚧 Files skipped from review as they are similar to previous changes (4)
- app/src/main/java/app/gamenative/utils/ContainerUtils.kt
- app/src/main/java/app/gamenative/utils/UpdateInstaller.kt
- app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt
- app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
There was a problem hiding this comment.
1 issue found across 8 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.kt">
<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.kt:276">
P2: A user upgrading from a Legacy build with the Android filter enabled can retain that persisted flag on a Modern build, where this line removes the only visible way to disable it. The library can then remain restricted to Android-compatible Steam depots (or appear empty) even though native Android installation is unavailable; the Modern initialization path should clear or ignore `AppFilter.ANDROID` when restoring/applying filters.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| add(AppFilter.FIVE_STAR) | ||
| add(AppFilter.FIVE_STAR_GPU) | ||
| add(AppFilter.PROVEN_GPU) | ||
| if (!BuildConfig.MODERN_ANDROID) add(AppFilter.ANDROID) |
There was a problem hiding this comment.
P2: A user upgrading from a Legacy build with the Android filter enabled can retain that persisted flag on a Modern build, where this line removes the only visible way to disable it. The library can then remain restricted to Android-compatible Steam depots (or appear empty) even though native Android installation is unavailable; the Modern initialization path should clear or ignore AppFilter.ANDROID when restoring/applying filters.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/screen/library/components/LibraryOptionsPanel.kt, line 276:
<comment>A user upgrading from a Legacy build with the Android filter enabled can retain that persisted flag on a Modern build, where this line removes the only visible way to disable it. The library can then remain restricted to Android-compatible Steam depots (or appear empty) even though native Android installation is unavailable; the Modern initialization path should clear or ignore `AppFilter.ANDROID` when restoring/applying filters.</comment>
<file context>
@@ -259,18 +260,24 @@ fun LibraryOptionsPanel(
+ add(AppFilter.FIVE_STAR)
+ add(AppFilter.FIVE_STAR_GPU)
+ add(AppFilter.PROVEN_GPU)
+ if (!BuildConfig.MODERN_ANDROID) add(AppFilter.ANDROID)
+ }
+ }
</file context>
|
@utkarshdalal I think I’ve addressed all the relevant points raised by Cubic AI. |
14667cf to
6799694
Compare
Steam now lets some games be installed as a native Android app instead of the usual Windows build run through Wine (ahead of Valve's Steam Frame headset). This adds a per-game Version choice in Edit Container: picking Android downloads, installs, and launches the game as a real Android app, bypassing Wine entirely for that game.
…cases, staleness bugs, uninstall freeze)
…lt, fix receiver race, clean up staged APKs
… on the main thread
…, add Android-mode depot language test coverage
6799694 to
654e7d9
Compare
Steam now lets some games be installed as a native Android app instead of the usual Windows build run through Wine (ahead of Valve's Steam Frame headset). This adds a per-game Version choice in Edit Container: picking Android downloads, installs, and launches the game as a real Android app, bypassing Wine entirely for that game.
Build : https://www.transfernow.net/dl/20260723bHOQ5wLl
Description
Some games on Steam now ship a native Android build alongside their usual Windows build — this is in preparation for Valve's upcoming Steam Frame VR headset, but it also just works as a regular Android app on any device.
This PR adds the ability to pick that Android version instead of the Windows one, per game:
New "Version" dropdown in Edit Container (only shows up for games that actually have an Android build available).
Picking "Android" downloads the right files, installs the app through Android's normal system installer, and launches it directly — no Wine, no Proton, no Box64 involved at all for that game.
Uninstalling now also removes the installed Android app itself, not just the downloaded files.
Everything defaults to the current Windows/Wine behavior — nothing changes for existing games unless you explicitly pick Android for one.
One thing worth knowing about the build variants: this only works on the Legacy builds (legacy / legacyXr) for now. The Modern builds (modern / modernXr) intentionally have the "install/uninstall other apps" permissions stripped out, because the Meta Horizon Store rejected a previous submission over exactly those permissions. Since modernXr is the variant meant for store distribution, adding this feature there would risk the same rejection again — so for now, Android games are a Legacy-only feature. Worth a deliberate call later if we want this on Modern too.
New android filter
Recording
Type of Change
Checklist
#code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.CONTRIBUTING.md.Summary by cubic
Adds per-game support for installing and launching native Android (Steam Frame/Lepton) builds. You can choose Android per game; it skips Wine, auto-prompts system install after download, and the UI updates live.
New Features
modern/modernXrand stripped if persisted; non‑Steam sources are excluded when the Android filter is active.Bug Fixes
REQUEST_DELETE_PACKAGESkept in legacy, removed inmodern/modernXr.Written for commit 654e7d9. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements
Permissions/Packaging
UI/Localization
Tests