Skip to content

Fix installs on adopted primary storage - #1821

Open
danielbuva wants to merge 1 commit into
utkarshdalal:masterfrom
danielbuva:fix/adopted-storage-install-target
Open

Fix installs on adopted primary storage#1821
danielbuva wants to merge 1 commit into
utkarshdalal:masterfrom
danielbuva:fix/adopted-storage-install-target

Conversation

@danielbuva

@danielbuva danielbuva commented Aug 15, 2026

Copy link
Copy Markdown

Description

Android can migrate primary shared storage onto adopted SD media. GameNative currently rejects every primary StorageVolume in both Settings and DownloadService, so adopted-primary storage is hidden even though it is the device's large writable backing volume.

This change:

  • centralizes install-target eligibility in StorageUtils
  • uses StorageVolume.storageUuid on Android 12+ and StorageManager.getUuidForPath on Android 8-11 to distinguish adopted backing storage from UUID_DEFAULT
  • preserves existing portable SD and USB behavior, with conservative compatibility fallbacks when an OEM cannot map a path
  • applies the same predicate to the Settings selector and background install discovery, so existing installs remain discoverable after the toggle is changed
  • adds regression coverage for built-in primary, adopted primary, physical primary, legacy raw UUID fallback, and modern typed-UUID precedence

Related discussion: #1718

Related prior work: #1585 (volume enumeration), #809 (toggle-off discovery), and #1768 (modern external storage)

Android background: https://source.android.com/docs/core/storage/adoptable

Recording

Not available yet because this requires a physical device with adopted storage migrated to primary. Please run the Ad-hoc Signed Build workflow for this PR so affected users can install it over their existing GameNative app and validate the fix without clearing app data or redownloading their libraries.

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Testing

  • ./gradlew :app:testLegacyDebugUnitTest --tests app.gamenative.utils.StorageUtilsTest
  • ./gradlew :app:testModernDebugUnitTest --tests app.gamenative.utils.StorageUtilsTest
  • 7 tests passed in each variant
  • git diff --check

Real-device validation is still required before merge.

Scope and non-goals

This supports adopted media when Android exposes it as the backing volume for primary shared storage. It does not synthesize private paths such as /mnt/expand/<UUID>, request broader storage permissions, or claim support for an adopted private volume that Android does not return through the app's external-files APIs.

Device test plan

Test target: any GameNative-supported Android device where a microSD card has been formatted as adopted/internal storage and migrated to primary shared storage.

  1. Record the device model, Android version, OEM firmware or custom ROM, and whether primary storage was migrated to the adopted card.
  2. Open Settings and verify Write to external storage is enabled and the adopted-primary volume is offered.
  3. Select it and confirm the displayed free space corresponds to the card.
  4. Install a small game and verify its data is written to the selected volume.
  5. Restart GameNative and verify the game is still discovered and launchable.
  6. Disable external storage, restart, and verify the completed game remains discoverable, launchable, and deletable (regression coverage for fix: SD card games unrecoverable when external storage toggle off #809).
  7. Re-enable external storage, select the adopted volume again, uninstall the test game, and verify its files are removed from that volume.

Regression controls: a built-in-only primary volume must remain excluded, while existing portable SD and USB targets must remain available.

Checklist

  • If I have access to #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.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Fixes install discovery on devices where primary shared storage is migrated to adopted SD. Previously we hid all primary volumes; now we expose adopted-primary as an install target while keeping built-in primary hidden.

  • Centralizes eligibility in StorageUtils.isExternalInstallTarget, using StorageVolume.storageUuid on Android 12+ and StorageManager.getUuidForPath on Android 8–11, with fallbacks for physical primary and conservative behavior when UUIDs are unavailable.
  • Applies the same predicate to DownloadService and the settings volume selector, so selection and background discovery match and existing installs remain discoverable.
  • Keeps portable SD/USB behavior unchanged; adds unit coverage in StorageUtilsTest for built-in primary, adopted primary, physical primary, legacy fallback, and typed-UUID precedence.

Written for commit a94949d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved detection of available installation storage.
    • Adopted, removable, and other writable primary storage volumes are now correctly recognized as install destinations.
  • Tests

    • Added coverage for default, adopted, legacy, removable, emulated, and UUID-unavailable storage scenarios.

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6facf44d-4685-424a-8eb8-4a7c0eddd52d

📥 Commits

Reviewing files that changed from the base of the PR and between d85b230 and a94949d.

📒 Files selected for processing (4)
  • app/src/main/java/app/gamenative/service/DownloadService.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt
  • app/src/main/java/app/gamenative/utils/StorageUtils.kt
  • app/src/test/java/app/gamenative/utils/StorageUtilsTest.kt

📝 Walkthrough

Walkthrough

External volume discovery now uses shared eligibility checks. The checks include non-primary volumes and qualifying adopted or removable primary storage. UUID resolution varies by Android API level, with legacy fallback handling and unit test coverage.

Changes

External install target detection

Layer / File(s) Summary
Storage target classification
app/src/main/java/app/gamenative/utils/StorageUtils.kt, app/src/test/java/app/gamenative/utils/StorageUtilsTest.kt
StorageUtils classifies install targets using volume type, storage UUID, physical-storage status, and legacy UUID signals. Tests cover default, adopted, legacy, physical, emulated, and unidentified storage.
Install-target discovery integration
app/src/main/java/app/gamenative/service/DownloadService.kt, app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt
External volume discovery now uses StorageUtils.isExternalInstallTarget and includes qualifying adopted primary storage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to a9494

The change centralizes adopted-primary storage eligibility and keeps settings selection aligned with install discovery; no actionable merge-blocking risk remains beyond normal checks and review.

Possibly related PRs

Suggested reviewers: utkarshdalal, phobos665

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary fix for adopted primary storage installs.
Description check ✅ Passed The description explains the change, testing, scope, device test plan, and checklist status; the missing recording is clearly identified.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@danielbuva
danielbuva marked this pull request as ready for review August 15, 2026 21:20
Copilot AI lite review requested due to automatic review settings August 15, 2026 21:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes external-install target discovery on Android devices where primary shared storage has been migrated to adopted (SD-backed) storage. It centralizes the “is this a valid install target?” predicate in StorageUtils and uses storage UUID signals to allow adopted-primary volumes while continuing to hide built-in primary storage.

Changes:

  • Added StorageUtils.isExternalInstallTarget(...) and isNonDefaultPrimaryStorage(...) to consistently identify adopted-primary vs built-in primary storage.
  • Updated Settings volume selector and DownloadService init-time volume discovery to use the centralized predicate.
  • Added unit regression coverage for default primary, adopted primary, physical primary fallback, and legacy UUID fallback behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
app/src/main/java/app/gamenative/utils/StorageUtils.kt Adds centralized install-target eligibility logic based on StorageVolume UUID/flags with version-aware fallbacks.
app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt Switches Settings volume filtering to use StorageUtils.isExternalInstallTarget(...) so adopted-primary can be shown.
app/src/main/java/app/gamenative/service/DownloadService.kt Aligns background install discovery / path enumeration with the same install-target predicate as Settings.
app/src/test/java/app/gamenative/utils/StorageUtilsTest.kt Adds unit tests for adopted-primary detection and relevant fallback/precedence rules.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +157 to +170
fun isExternalInstallTarget(storageManager: StorageManager?, appFilesDir: File): Boolean {
val volume = storageManager?.getStorageVolume(appFilesDir)
?: return runCatching { Environment.isExternalStorageRemovable(appFilesDir) }.getOrDefault(false)
if (!volume.isPrimary) return true

val resolvedStorageUuid = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
volume.storageUuid
} else {
try {
storageManager.getUuidForPath(appFilesDir)
} catch (_: Exception) {
null
}
}

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 4 files

Re-trigger cubic

@utkarshdalal

Copy link
Copy Markdown
Owner

Thanks for making this. I don't think this is enough to make games actually run? Have you tested? Can you please provide a recording?

@danielbuva

Copy link
Copy Markdown
Author

@utkarshdalal Would it be possible to provide an officially signed test APK containing the changes from PR #1821?

I've successfully confirmed that the test build detects adopted storage, downloads games to it, and launches executables from the SD-backed path. However, my separately installed beta uses a different package ID. Some native GameNative components contain hardcoded references to /data/data/app.gamenative, causing permission failures and preventing a valid gameplay test.

Ideally, the test APK would:

  • Use the normal app.gamenative package ID
  • Be signed with the same certificate as the current release
  • Have a higher version code so it can be installed as an in-place update
  • Include the changes from PR Fix installs on adopted primary storage #1821

This would let me test the complete flow and provide a recording.

@utkarshdalal

Copy link
Copy Markdown
Owner

@danielbuva

Copy link
Copy Markdown
Author

@utkarshdalal The build succeeded, but the hosted runner lost communication during universal APK extraction, likely due to memory or disk pressure. Could you rerun it or build just the single variant needed for testing?

@utkarshdalal

Copy link
Copy Markdown
Owner

Damn it, I re-ran it now.

@DeviPotato

Copy link
Copy Markdown

I am interested in testing this PR as I have a RP6 with adopted storage. There still doesn't seem to be a signed APK available to test though, either via the linked action from @utkarshdalal or checking for more recently run actions. It seems like it still failed on the re-run. If I'm wrong and am just missing a completed build somewhere I'll gladly download it and give it a try.

@utkarshdalal

Copy link
Copy Markdown
Owner

https://github.com/utkarshdalal/GameNative/actions/runs/33328287574

@DeviPotato @danielbuva - i fixed the ad hoc action and rebuilt. please try

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants