Conversation
📝 WalkthroughWalkthroughAdds GameManagerDialog UI and per-game dialog state; implements DLC-aware multi-depot download orchestration with per-app persistence in SteamService; introduces DownloadingAppInfo entity/DAO and DB version bump; extends DownloadInfo with gameId/downloadingAppIds and message-aware cancellation; updates SteamUtils, localized strings, and dependency coordinates. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User
participant UI as GameManagerDialog / SteamAppScreen
participant Service as SteamService
participant DB as Room DAOs
participant DL as DepotDownloader
participant Marker as MarkerUtils
Note over UI,Service: User opens dialog, selects base + DLCs
User->>UI: open dialog / select DLCs
UI->>Service: getDownloadableDepots(appId)
Service->>DB: getInstalledApp / findDownloadableDLCApps / findHiddenDLCApps / getDownloadingApp
DB-->>Service: AppInfo / DLC lists / downloading info
Service->>Service: assemble main + DLC depot maps
UI->>Service: downloadApp(appId, downloadableDepots, dlcIds, branch)
Service->>DB: insert DownloadingAppInfo(appId, dlcIds)
Service->>DL: create/enqueue DepotDownloader(s) with per‑app listeners
DL->>Service: progress / completion callbacks
Service->>DB: completeAppDownload(...) persist results and remove DownloadingAppInfo
Service->>Marker: write/remove markers
Service-->>UI: notify completion
UI-->>User: show install/complete state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 7
🧹 Nitpick comments (6)
app/src/main/java/app/gamenative/data/DownloadInfo.kt (1)
11-14: Consider thread-safety fordownloadingAppIds.The
downloadingAppIdsproperty is declared asvar List<Int>, making it reassignable but not thread-safe. SinceDownloadInfois used in concurrent download scenarios (as evidenced byCopyOnWriteArrayListforspeedSamples), concurrent reads/writes to this property could cause visibility issues.If this list is expected to be modified during the download lifecycle, consider using a thread-safe structure or synchronization. If it's only set once at construction time, making it
valwould be safer.🔎 Suggested fix if modification is needed
data class DownloadInfo( val gameId: Int, - var downloadingAppIds: List<Int>, + val downloadingAppIds: CopyOnWriteArrayList<Int>, val jobCount: Int = 1, ) {Or if immutable after construction:
data class DownloadInfo( val gameId: Int, - var downloadingAppIds: List<Int>, + val downloadingAppIds: List<Int>, val jobCount: Int = 1, ) {app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt (1)
4-4: Remove unused import.
LibraryItemis imported but not used in this file.🔎 Suggested fix
package app.gamenative.ui.component.dialog.state import androidx.compose.runtime.saveable.mapSaver -import app.gamenative.data.LibraryItem data class GameManagerDialogState(app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (2)
91-94: Side-effect in composable body.Calling
SteamService.getDownloadableDlcAppsOf()andgetHiddenDlcAppsOf()directly in the composable body executes on every recomposition. These should be wrapped inrememberwith appropriate keys or moved into theLaunchedEffect.🔎 Suggested fix
- val indirectDlcAppIds = SteamService.getDownloadableDlcAppsOf(gameId).orEmpty().map { it.id } - - // Filter out DLCs that are not in the appInfo, this can happen for DLCs that are not in the appInfo - val hiddenDlcIds = SteamService.getHiddenDlcAppsOf(gameId).orEmpty().map { it.id }.filter { id -> appInfo.depots[id] == null } + val indirectDlcAppIds = remember(gameId) { + SteamService.getDownloadableDlcAppsOf(gameId).orEmpty().map { it.id } + } + + val hiddenDlcIds = remember(gameId, appInfo) { + SteamService.getHiddenDlcAppsOf(gameId).orEmpty().map { it.id }.filter { id -> appInfo.depots[id] == null } + }
139-173: Recalculating size info on every recomposition.
getInstallSizeInfo()is a local function that performs calculations on every call. Since it's used in multiple places (installSizeDisplay(),installButtonEnabled()), consider caching the result withrememberorderivedStateOf.🔎 Suggested fix
val installSizeInfo by remember(downloadableDepots.keys.toSet(), selectedAppIds.toMap()) { derivedStateOf { getInstallSizeInfo() } }Then use
installSizeInfodirectly instead of calling the function.app/src/main/java/app/gamenative/service/SteamService.kt (2)
481-482: Consider removing commented-out code.The commented-out line with the TODO-style explanation should be removed if the game manager is now handling optional DLCs. Leaving dead code in comments can lead to confusion over time.
Proposed fix
/* Optional DLC depots are skipped */ - /* This line is commented out because game manager will now handle those optional DLCs */ - //depot.optionalDlcId == depot.dlcAppId -> false
1104-1117: Complex edge-case handling for DLC without explicit depot mapping.This logic handles games where DLC depots lack explicit
dlcAppIddata. The conditions (single depot, OS.none, hidden DLC) are quite specific.Consider adding a code comment explaining which game(s) or scenario this addresses to aid future maintainers.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
app/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/db/dao/AppInfoDao.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/enums/Marker.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.ktapp/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/res/values-da/strings.xmlapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values-pt-rBR/strings.xmlapp/src/main/res/values-zh-rCN/strings.xmlapp/src/main/res/values-zh-rTW/strings.xmlapp/src/main/res/values/strings.xml
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/db/dao/AppInfoDao.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/enums/Marker.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
🔇 Additional comments (22)
app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt (1)
22-23: LGTM! Clean enum addition.The new
ManageGameContentenum constant is properly defined and consistent with existing patterns. The trailing comma on line 22 follows Kotlin best practices and makes future additions easier.app/src/main/res/values-zh-rTW/strings.xml (1)
15-16: steam_install_space zh-TW entry looks correct and consistentThe new
steam_install_spacestring mirrorssteam_install_space_promptandlibrary_download_prompt_messagein both wording and placeholder ordering; newline usage and%1$s/%2$s/%3$sare correct. No changes needed.app/src/main/res/values-fr/strings.xml (1)
15-16: steam_install_space FR entry is aligned with existing promptThe
steam_install_spacestring reuses the same labels, order, and%1$s/%2$s/%3$splaceholders as the existing prompt/Library dialog. Looks good as a standalone size block.app/src/main/res/values-zh-rCN/strings.xml (1)
15-16: steam_install_space zh-CN entry is consistent and well-formedThe
steam_install_spacestring correctly mirrors the prompt variant and Library dialog (same labels, order, and%1$s/%2$s/%3$splaceholders). No issues from an i18n or formatting standpoint.app/src/main/res/values/strings.xml (1)
15-16: Base steam_install_space string is correctly factored for reuseThe new
steam_install_spaceresource cleanly extracts the size block from the existing prompt, with identical text and%1$s/%2$s/%3$sordering. This should work well as a shared string for the new dialog.app/src/main/res/values-pt-rBR/strings.xml (1)
728-728: LGTM! Localization string correctly added.The Portuguese (Brazil) translation is accurate, format placeholders are properly positioned, and the string provides the space information in a clean standalone format that complements the existing
steam_install_space_prompt.app/src/main/java/app/gamenative/enums/Marker.kt (1)
3-9: LGTM!The new
MODIFYING_MARKERenum constant follows the existing naming convention and integrates cleanly with the marker system used for tracking app installation states.app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
23-27: Rename looks good, but note potential redundancy.The rename from
getInstalledDepotstogetInstalledAppbetter reflects the return type (AppInfo). However,getInstalledAppandgetnow have identical queries. If there's no semantic distinction intended, consider consolidating them.app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt (1)
6-22: LGTM!The state class and Saver implementation follow Compose best practices for dialog state persistence across configuration changes.
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)
762-785: LGTM!The refactored DLC handling correctly:
- Renames to
getInstalledDlcDepotsOffor clarity- Adds
getDownloadableDlcAppsOfto retrieve DLC apps- Tracks
appendedDlcIdsto prevent duplicate entries- Iterates through downloadable DLC apps to get their installed depots
- Only adds hidden DLCs if not already present
The duplicate-prevention logic using
appendedDlcIdsis a good safeguard.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (4)
135-147: LGTM!The per-game dialog state management follows the existing pattern used for
installDialogStatesanduninstallDialogAppIds, maintaining consistency across the codebase.
457-462: Install flow now uses GameManagerDialog.The change from the simple install prompt to
GameManagerDialogenables DLC selection before installation. This is the expected behavior per the PR objectives.
1220-1228: Marker management during DLC modification looks correct.When installing DLCs on an already-installed app:
MODIFYING_MARKERis added to indicate ongoing modification- DRM-related markers are removed to force re-setup after modification
This ensures the DRM layer is properly reconfigured after DLC installation.
1211-1242: No action needed. TheSteamService.downloadApp(gameId, dlcAppIds)call correctly matches the existing method signaturefun downloadApp(appId: Int, dlcAppIds: List<Int>): DownloadInfo?in SteamService. The two-parameter overload with DLC app IDs is properly defined.Likely an incorrect or invalid review comment.
app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt (1)
47-63: Performance concern: LIKE with wildcards cannot use indexes.The queries use
LIKE '%,' || app.id || ',%'to check if an app ID exists in theapp_idscolumn. While the string manipulation (REPLACE) approach works correctly with Kotlin's compact JSON serialization format (e.g.,[1,2,3]), the LIKE pattern with wildcards on both sides cannot use database indexes. This forces a full table scan onsteam_licensefor each query, which may impact performance when dealing with many licenses.Consider alternative approaches such as normalizing the data into a separate junction table to enable indexed queries, or implementing this check at the application level if the license set is manageable.
app/src/main/java/app/gamenative/service/SteamService.kt (7)
435-453: LGTM!The new helper methods are clean delegations to the DAO layer. The pattern of using
runBlockingfor synchronous access to suspend functions is consistent with the existing codebase pattern (per learnings).
536-582: LGTM! Clean extraction of filter logic.The
filterForDownloadableDepotsfunction properly encapsulates the depot filtering criteria (manifests, OS, architecture, DLC ownership, language). The architecture fallback logic correctly handles 32-bit depots only when no 64-bit alternative exists.
596-630: LGTM! Good approach to DLC depot association.The logic correctly:
- Starts with main app depots
- Iterates through indirect DLC apps
- Creates new
DepotInfoobjects with the correctdlcAppIdattributionThis ensures DLC depots are properly tracked back to their respective DLC apps.
883-898: LGTM!The overload pattern is clean. The parameterless version correctly delegates to the version with DLC app IDs, using the current download info's
downloadingAppIdsfor resuming downloads with the same DLC selection.
1216-1254: LGTM! Multi-app download setup.The listener pattern correctly handles:
- Main app listener with its depot-to-index mapping
- Separate listeners for each DLC app
- Each DLC app added as a separate
AppItemto the downloaderThe cleanup at lines 1265-1268 properly removes all listeners after completion.
1369-1381: Verify fail-fast behavior is intentional for DLC download failures.When a single DLC download fails, the entire download job is removed (line 1371). If other DLCs or the main app are still downloading, this will stop them without proper cleanup markers.
If fail-fast is intentional, consider:
- Cancelling the main download job explicitly
- Adding a
DOWNLOAD_FAILED_MARKERto distinguish from user cancellation
1089-1094: LGTM! Incremental download logic.The
MODIFYING_MARKERcheck correctly filters out already-downloaded depots when updating an existing installation. This enables incremental DLC downloads without re-downloading the base game.
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (2)
89-90: Null-safety: Non-null assertion could crash.This issue was already flagged in a previous review. Using
!!onSteamService.getAppInfoOf(gameId)will throwNullPointerExceptionif the app info is not found.
107-109: Potential crash on empty depot map.This issue was already flagged in a previous review.
downloadableDepots.toSortedMap().values.first()throwsNoSuchElementExceptionif the map is empty.
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (3)
98-126: LaunchedEffect runs on both visibility transitions.The effect is keyed on
visible, so it executes when the dialog opens and when it closes. This causes unnecessary work (fetching depots, populating lists) right before dismissal.🔎 Suggested fix
LaunchedEffect(visible) { + if (!visible) return@LaunchedEffect + scrollState.animateScrollTo(0) downloadableDepots.clear() allDownloadableApps.clear()
141-175: Redundant computation:getInstallSizeInfo()is called multiple times per composition.This function is invoked from both
installSizeDisplay()(line 178) andinstallButtonEnabled()(line 188), causing duplicate iterations overdownloadableDepots. Consider memoizing the result.🔎 Suggested optimization using derivedStateOf
+ val installSizeInfo by remember { + derivedStateOf { getInstallSizeInfo() } + } + fun installSizeDisplay() : String { - val installSizeInfo = getInstallSizeInfo() return context.getString( R.string.steam_install_space, installSizeInfo.downloadSize, installSizeInfo.installSize, installSizeInfo.availableSpace ) } fun installButtonEnabled() : Boolean { - val installSizeInfo = getInstallSizeInfo() if (installSizeInfo.availableBytes < installSizeInfo.installBytes) {This requires adding
derivedStateOfto the imports.
371-371: Combine consecutive filters for clarity.The two
.filter { }calls can be merged into one predicate.🔎 Suggested simplification
- onInstall(selectedAppIds.filter { it.key != gameId && !hiddenDlcIds.contains(it.key) }.filter { it.value }.keys.toList()) + onInstall( + selectedAppIds + .filter { (appId, isSelected) -> + appId != gameId && !hiddenDlcIds.contains(appId) && isSelected + } + .keys + .toList() + )
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
385-414: LGTM!The preview is correctly set up using
fakeAppInfo()which ensures debug-only usage. ThePluviaThemewrapper provides proper theming context.
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (3)
90-91: Null-safety: Non-null assertion could crash.Using
!!onSteamService.getAppInfoOf(gameId)will throwNullPointerExceptionif the app info is not found. Handle the null case gracefully by dismissing the dialog or showing an error.
112-114: Potential crash on empty depot map.
downloadableDepots.toSortedMap().values.first()throwsNoSuchElementExceptionif the map is empty. Add a guard or usefirstOrNull().
233-251: Missing accessibility: No content description for hero image.The
CoilImagelacks acontentDescriptionfor screen readers. For decorative images, explicitly passcontentDescription = nullinImageOptions.
🧹 Nitpick comments (7)
app/src/main/java/app/gamenative/data/DownloadInfo.kt (1)
11-15: Consider adding KDoc for the new constructor properties.The new
gameIdanddownloadingAppIdsproperties lack documentation. Adding KDoc would clarify:
- What
gameIdrepresents (e.g., the main game's app ID)- What
downloadingAppIdstracks (e.g., list of app IDs currently being downloaded, including base game and selected DLCs)📝 Suggested documentation
+/** + * Tracks download progress, speed, and ETA for a game installation. + * + * @param gameId The main game's app ID + * @param downloadingAppIds List of app IDs being downloaded (base game + selected DLCs) + * @param jobCount Number of parallel download jobs + */ data class DownloadInfo( val gameId: Int, val downloadingAppIds: CopyOnWriteArrayList<Int>, val jobCount: Int = 1, ) {app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (4)
182-184: Memoization keys may not work as expected.Using
downloadableDepots.keys.toSet()andselectedAppIds.toMap()asrememberkeys creates new collection instances on each recomposition, potentially invalidating the cache unnecessarily. Consider using stable keys or snapshot reads.🔎 Alternative approach
- val installSizeInfo by remember(downloadableDepots.keys.toSet(), selectedAppIds.toMap()) { - derivedStateOf { getInstallSizeInfo() } - } + val installSizeInfo by remember { + derivedStateOf { getInstallSizeInfo() } + }Since
derivedStateOfalready tracks reads fromdownloadableDepotsandselectedAppIds(both are snapshot state), the explicit keys are redundant.
195-207: Complex install button logic could benefit from clarification.The expression on line 203
(selectedAppIds.filter { it.value }.size - fixForSpecialDlcIds.size - 1) > 0is difficult to follow. Consider extracting to a named variable or adding a comment explaining the intent.🔎 Suggested refactor for clarity
if (installedApp != null) { val allAppIds = allDownloadableApps.map { it.first } val fixForSpecialDlcIds = installedDlcIds.filter { allAppIds.contains(it) } - return (selectedAppIds.filter { it.value }.size - fixForSpecialDlcIds.size - 1) > 0 // -1 for main app + val selectedCount = selectedAppIds.count { it.value } + val alreadyInstalledCount = fixForSpecialDlcIds.size + 1 // +1 for main app + val newDlcsToInstall = selectedCount - alreadyInstalledCount + return newDlcsToInstall > 0 }
305-317: Empty string for unknown release dates creates awkward UI.When
displayInfo.releaseDateis 0 or negative, the subtitle shows"Developer • "with a trailing bullet and space. Consider omitting the bullet separator when no year is available.🔎 Suggested fix
Text( - text = "${displayInfo.developer} • ${ + text = buildString { + append(displayInfo.developer) remember(displayInfo.releaseDate) { if (displayInfo.releaseDate > 0) { - SimpleDateFormat("yyyy", Locale.getDefault()).format(Date(displayInfo.releaseDate * 1000)) + " • ${SimpleDateFormat("yyyy", Locale.getDefault()).format(Date(displayInfo.releaseDate * 1000L))}" } else { "" } - } - }", + }.let { append(it) } + }, style = MaterialTheme.typography.bodyMedium,Also note: using
1000Lensures Long arithmetic to avoid potential overflow.
375-382: Consider extracting the filter chain for readability.The nested filters on line 378 are dense. Extracting to a local variable improves readability and debuggability.
🔎 Suggested refactor
Button( enabled = installButtonEnabled(), onClick = { - onInstall(selectedAppIds.filter { it.key != gameId && !hiddenDlcIds.contains(it.key) }.filter { it.value }.keys.toList()) + val dlcsToInstall = selectedAppIds + .filter { (appId, isSelected) -> + appId != gameId && + !hiddenDlcIds.contains(appId) && + isSelected + } + .keys.toList() + onInstall(dlcsToInstall) } ) {app/src/main/java/app/gamenative/service/SteamService.kt (2)
541-544: Simplify OS filtering logic for readability.The double negation in the OS filter is difficult to parse. Consider refactoring for clarity:
🔎 Suggested refactoring
- // 2. Supported OS - if (!(depot.osList.contains(OS.windows) || - (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) - ) - return false + // 2. Supported OS: must support Windows or have no OS restrictions + val hasWindows = depot.osList.contains(OS.windows) + val hasLinux = depot.osList.contains(OS.linux) + val hasMacos = depot.osList.contains(OS.macos) + val isOsNeutral = !hasLinux && !hasMacos + if (!hasWindows && !isOsNeutral) + return false
1321-1333: Handle potential null from getInstalledApp defensively.Line 1322 force-unwraps
getInstalledApp(downloadingAppId)!!, which could throw NPE if the AppInfo doesn't exist despiteMODIFYING_MARKERbeing present (e.g., database corruption or race condition).🔎 Proposed defensive handling
runBlocking { // Update Saved AppInfo if (MarkerUtils.hasMarker(appDirPath, Marker.MODIFYING_MARKER)) { - val appInfo = getInstalledApp(downloadingAppId)!! - val updatedDownloadedDepots = (appInfo.downloadedDepots + entitledDepotIds).distinct() - val updatedDlcDepots = (appInfo.dlcDepots + selectedDlcAppIds).distinct() - - instance?.appInfoDao?.update( - AppInfo( - downloadingAppId, - isDownloaded = true, - downloadedDepots = updatedDownloadedDepots, - dlcDepots = updatedDlcDepots, - ), - ) + val appInfo = getInstalledApp(downloadingAppId) + if (appInfo != null) { + val updatedDownloadedDepots = (appInfo.downloadedDepots + entitledDepotIds).distinct() + val updatedDlcDepots = (appInfo.dlcDepots + selectedDlcAppIds).distinct() + + instance?.appInfoDao?.update( + AppInfo( + downloadingAppId, + isDownloaded = true, + downloadedDepots = updatedDownloadedDepots, + dlcDepots = updatedDlcDepots, + ), + ) + } else { + Timber.w("MODIFYING_MARKER exists but no AppInfo found for $downloadingAppId, falling back to insert") + instance?.appInfoDao?.insert( + AppInfo( + downloadingAppId, + isDownloaded = true, + downloadedDepots = entitledDepotIds, + dlcDepots = selectedDlcAppIds, + ), + ) + } } else {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
app/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/res/values-da/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/res/values-da/strings.xml
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
app/src/main/java/app/gamenative/data/DownloadInfo.kt (1)
12-13: No changes needed. The property is never reassigned—all code only modifies the list contents via thread-safe methods likeadd(),remove(), andclear(). The use ofCopyOnWriteArrayListalready handles thread-safety for content modifications, and no reference replacement occurs in the codebase. While usingvalis a general best practice for immutable data class properties, the specific thread-safety concern raised is not substantiated by actual usage.Likely an incorrect or invalid review comment.
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
392-421: LGTM!The preview composable is well-structured, using appropriate fake data and theme wrapper for UI previews.
app/src/main/java/app/gamenative/service/SteamService.kt (1)
1100-1114: The OS.none check at line 1109 is the correct approach.OS.from()returnsEnumSet.of(OS.none)for null, empty, or invalid OS strings, makingOS.nonethe proper sentinel value for "no valid OS information." SinceosListis anEnumSet<OS>that always contains at leastOS.nonewhen no valid OS is specified, checkingosList.contains(OS.none)correctly identifies depots with no OS information—which indicates DLCs bundled within the main app rather than distributed as separate depots. The current implementation is correct and no alternative empty-list check is needed.
| LaunchedEffect(visible) { | ||
| scrollState.animateScrollTo(0) | ||
|
|
||
| downloadableDepots.clear() | ||
| allDownloadableApps.clear() |
There was a problem hiding this comment.
LaunchedEffect triggers on both visibility changes.
The effect runs when visible changes to false as well, causing unnecessary state clearing when the dialog closes. Guard the logic to only execute when the dialog becomes visible.
🔎 Suggested fix
LaunchedEffect(visible) {
+ if (!visible) return@LaunchedEffect
+
scrollState.animateScrollTo(0)
downloadableDepots.clear()
allDownloadableApps.clear()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| LaunchedEffect(visible) { | |
| scrollState.animateScrollTo(0) | |
| downloadableDepots.clear() | |
| allDownloadableApps.clear() | |
| LaunchedEffect(visible) { | |
| if (!visible) return@LaunchedEffect | |
| scrollState.animateScrollTo(0) | |
| downloadableDepots.clear() | |
| allDownloadableApps.clear() |
🤖 Prompt for AI Agents
In app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
around lines 103 to 107, the LaunchedEffect keyed on visible runs on both show
and hide and clears state when the dialog closes; modify the effect so it only
executes its body when visible is true (e.g., early-return or wrap the existing
logic in if (visible) { ... }) so scroll animation and clearing of
downloadableDepots/allDownloadableApps happen only when the dialog becomes
visible.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (4)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (4)
90-90: Null-safety: Non-null assertion could crash.Using
!!onSteamService.getAppInfoOf(gameId)will throwNullPointerExceptionif the app info is not found. Consider handling the null case gracefully.🔎 Suggested fix
- val appInfo = SteamService.getAppInfoOf(gameId)!! + val appInfo = SteamService.getAppInfoOf(gameId) ?: run { + onDismissRequest() + return + }
107-135: LaunchedEffect triggers on visibility changes in both directions.The effect runs when
visiblechanges tofalseas well, causing unnecessary state clearing when the dialog closes. Guard the logic to only execute when the dialog becomes visible.🔎 Suggested fix
LaunchedEffect(visible) { + if (!visible) return@LaunchedEffect + scrollState.animateScrollTo(0) downloadableDepots.clear()
133-134: Potential crash on empty depot map.
downloadableDepots.toSortedMap().values.first()throwsNoSuchElementExceptionif the map is empty. Add a guard or usefirstOrNull().🔎 Suggested fix
// Add Base Game - allDownloadableApps.add(0, Pair(gameId, downloadableDepots.toSortedMap().values.first())) - selectedAppIds[gameId] = true + val firstDepot = downloadableDepots.toSortedMap().values.firstOrNull() + if (firstDepot != null) { + allDownloadableApps.add(0, Pair(gameId, firstDepot)) + selectedAppIds[gameId] = true + }
237-255: Missing accessibility: No content description for hero image.The
CoilImagelacks acontentDescriptionfor screen readers. For decorative images, explicitly mark asnull; otherwise, provide a meaningful description.🔎 Suggested fix
CoilImage( modifier = Modifier.fillMaxSize(), imageModel = { displayInfo.heroImageUrl }, - imageOptions = ImageOptions(contentScale = ContentScale.Crop), + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + contentDescription = null // Decorative image + ), loading = { LoadingScreen() },
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
204-208: Consider simplifying the install button enabled logic.The logic for determining if new DLCs can be installed is correct but complex. Consider adding explanatory comments or extracting intermediate values with descriptive names to improve readability.
// Current logic: val installed = installedDlcIds.toSet() - mainAppDlcIdsWithoutProperDepotDlcIds.toSet() val realSelectedAppIds = selectedAppIds.filter { it.value }.keys - installed return (realSelectedAppIds.size - 1) > 0 // -1 for main appFor example:
// Calculate newly selected DLCs (not already installed) val alreadyInstalledDlcs = installedDlcIds.toSet() - mainAppDlcIdsWithoutProperDepotDlcIds.toSet() val selectedButNotInstalledAppIds = selectedAppIds.filter { it.value }.keys - alreadyInstalledDlcs val newDlcCount = selectedButNotInstalledAppIds.size - 1 // Exclude base game from count return newDlcCount > 0
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
🧬 Code graph analysis (2)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (3)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
getAppDirPath(660-682)app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
GameManagerDialog(72-394)app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (1)
getGameDisplayInfo(73-179)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (5)
135-147: LGTM: Consistent dialog state management.The GameManagerDialog state management follows the same pattern as the existing
installDialogStatesanduninstallDialogAppIds, providing a clean API for per-game dialog control.
457-462: LGTM: GameManagerDialog replaces install prompt.The change from a simple install prompt to the GameManagerDialog provides users with more control over DLC selection during installation, which aligns with the PR objectives.
630-640: LGTM: ManageGameContent menu option.The new menu option provides access to the GameManagerDialog for installed games, enabling users to manage DLC content post-installation.
1038-1038: LGTM: MODIFYING_MARKER prevents database clearing during verification.Adding the marker before file verification aligns with the PR objectives to prevent clearing
dlc_depotsvalues in the database during verification operations.Based on learnings, the AppInfo table tracks local game installation state and should not be cleared during operations that modify the game files.
1212-1243: LGTM: GameManagerDialog integration handles markers and installation correctly.The dialog integration properly:
- Retrieves game display info via the callback
- Adds
MODIFYING_MARKERfor installed apps before modification (line 1224)- Clears DRM-related markers to ensure proper unpacking
- Captures PostHog analytics
- Initiates download with selected DLC IDs
The marker usage ensures
dlc_depotsvalues are preserved during the update operation, as described in the PR objectives.Based on learnings, the marker pattern prevents incorrect clearing of installation state during file operations.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
476-477: Remove commented-out code.The commented code on lines 476-477 should be removed rather than left in place. The comment explains why the behavior changed, which is helpful, but the actual commented code serves no purpose and reduces readability.
🔎 Proposed fix
depot.dlcAppId == INVALID_APP_ID -> true /* Optional DLC depots are skipped */ - /* This line is commented out because game manager will now handle those optional DLCs */ - //depot.optionalDlcId == depot.dlcAppId -> false + /* Game manager now handles optional DLCs */ /* ① licence cache */
1098-1146: Complex depot selection logic - consider adding clarifying comments.The depot selection logic (lines 1098-1146) handles several edge cases:
- Separating main app depots from DLC app depots
- Filtering already-downloaded depots when modifying
- Handling DLCs without proper depot structures (mainAppDlcIds)
- Consolidating lists when no DLC depots exist
While the logic appears correct, the complexity makes it challenging to maintain. Consider adding inline comments to explain each major step, especially the purpose of
mainAppDlcIdsand whycalculatedDlcAppIdsis cleared whendlcAppDepots.isEmpty().
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (7)
app/src/main/java/app/gamenative/service/SteamService.kt (7)
555-601: LGTM - Good refactoring to centralize depot filtering.The extraction of
filterForDownloadableDepotsandgetMainAppDepotsimproves code organization and reduces duplication. The filtering logic is now centralized and easier to maintain.
607-649: Enhancement looks good - DLC depot aggregation implemented correctly.The updated
getDownloadableDepotsnow properly aggregates depots from both the main app and indirect DLC apps. The customDepotInfoconstruction correctly preserves DLC metadata.
894-899: LGTM - Cleanup of indirect DLC apps on deletion.This change correctly implements the cleanup of indirect DLC app database entries when the base app is deleted, preventing orphaned records as stated in the PR objectives.
909-924: Good design - Backward compatible overloads.The overloaded
downloadAppmethods maintain backward compatibility while adding DLC selection support. The delegation pattern is clean and appropriate.
1230-1282: Multi-app download orchestration implemented correctly.The creation of separate listeners for the main app and each DLC app, along with proper cleanup, demonstrates good resource management. The parallel download approach should work well for multi-app installations.
1336-1361: Download completion logic correctly handles multi-app tracking.The updated
onDownloadCompletedproperly:
- Updates or inserts AppInfo with downloaded depots and DLC associations
- Removes the completed app from the tracking list
- Triggers final cleanup only when all apps are downloaded
The logic correctly distinguishes between modifying an existing installation (MODIFYING_MARKER present) and a fresh install.
1367-1380: Proper cleanup on successful multi-app download completion.The completion handler correctly:
- Adds the DOWNLOAD_COMPLETE_MARKER only after all apps finish
- Removes temporary markers (STEAM_DLL_REPLACED, STEAM_COLDCLIENT_USED, MODIFYING_MARKER)
- Clears persisted progress data
- Emits the library status change event
- Removes the download job from tracking
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
494-516: Consider adding documentation for the DLC edge-case logic.The logic correctly identifies DLC app IDs that lack proper depot-level DLC mappings, but the filtering conditions (count==1, OS.none, no manifests, in hiddenDlcAppIds) are subtle. Adding a brief doc comment explaining why these conditions identify the edge case would improve maintainability.
📝 Suggested documentation
+ /** + * Identifies DLC app IDs referenced in the main app's depots that don't have + * proper depot-level DLC associations. These are typically placeholder/metadata + * DLCs where: + * - Only one depot references the DLC ID (count == 1) + * - The depot has OS.none (no actual game files) + * - The depot has no manifests to download + * - The DLC is marked as hidden + * + * Such DLCs are tracked separately since their content is bundled with the main app. + */ fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> {
1098-1150: Complex but sound DLC depot orchestration logic.The depot filtering and DLC handling logic correctly:
- Separates main app depots from DLC app depots
- Filters out already-downloaded depots when MODIFYING_MARKER is present
- Handles the edge case where DLC content is bundled in the main app (lines 1134-1142)
- Uses
CopyOnWriteArrayListfor thread-safe concurrent accessThe complexity is justified by the feature requirements. Consider extracting the depot categorization logic (lines 1100-1122) into a helper method to improve readability if this function grows further.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/utils/SteamUtils.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/app/gamenative/utils/SteamUtils.kt
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
app/src/main/java/app/gamenative/service/SteamService.kt (4)
42-43: LGTM: Import additions and new public accessors.The new imports (MarkerUtils, Marker, CopyOnWriteArrayList) support the DLC management features. The public accessor functions follow existing patterns with runBlocking for synchronous access to suspend functions, consistent with other methods in this class (e.g., getPkgInfoOf, getAppInfoOf).
Also applies to: 147-147, 430-448
558-649: LGTM: Well-structured depot filtering and aggregation.The centralized
filterForDownloadableDepotsfunction cleanly consolidates the eligibility checks (OS, architecture, DLC ownership, language). ThegetDownloadableDepotsfunction correctly aggregates main app depots with indirect DLC app depots, ensuring each depot carries the correctdlcAppIdfor tracking.
1363-1380: LGTM: Download completion tracking.The completion logic correctly:
- Removes the completed app ID from the tracking list (line 1364 uses the correct predicate)
- Only performs final cleanup (markers, persistence) when all apps in
downloadingAppIdscomplete (line 1367)- Removes all state markers and clears persisted bytes
The use of
removeIf { it == downloadingAppId }is correct (fixed from previous review's inverted predicate issue).
476-477: The GameManager dialog correctly handles optional DLCs. Verification confirms thatgetDownloadableDepots()includes optional DLCs from the uncommented filter, andGameManagerDialogpresents them to users as checkbox selections (lines 116-128, 328-361), with default state respecting whether DLCs are already installed. User selections are properly tracked (line 350-358) and passed to download logic (line 382). The intentional behavior change is complete and working as designed.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (3)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (3)
103-107: LaunchedEffect executes on both show and hide.The effect runs when
visiblechanges tofalse, unnecessarily clearing state and animating scroll when the dialog closes. Guard the logic to execute only when the dialog becomes visible.🔎 Proposed fix
LaunchedEffect(visible) { + if (!visible) return@LaunchedEffect + scrollState.animateScrollTo(0) downloadableDepots.clear()
131-133: Potential crash if depot map is empty.
downloadableDepots.toSortedMap().values.first()throwsNoSuchElementExceptionif the map is empty. UsefirstOrNull()and handle the null case.🔎 Proposed fix
// Add Base Game - allDownloadableApps.add(0, Pair(gameId, downloadableDepots.toSortedMap().values.first())) - selectedAppIds[gameId] = true - enabledAppIds[gameId] = false + val baseGameDepot = downloadableDepots.toSortedMap().values.firstOrNull() + if (baseGameDepot != null) { + allDownloadableApps.add(0, Pair(gameId, baseGameDepot)) + selectedAppIds[gameId] = true + enabledAppIds[gameId] = false + }
236-254: Missing accessibility: No content description for hero image.The
CoilImagelacks acontentDescription, which is important for screen readers. For decorative images, explicitly set tonull; otherwise, provide a meaningful description.🔎 Proposed fix
CoilImage( modifier = Modifier.fillMaxSize(), imageModel = { displayInfo.heroImageUrl }, - imageOptions = ImageOptions(contentScale = ContentScale.Crop), + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + contentDescription = null // Decorative image + ), loading = { LoadingScreen() },
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (3)
153-174: Consider extracting duplicate filter patterns.The same depot filtering pattern appears four times (base game install, base game download, selected install, selected download). Extracting helper functions would reduce duplication.
💡 Example refactor
private fun Map<Int, DepotInfo>.filterBaseGame() = filter { (_, depot) -> depot.dlcAppId == INVALID_APP_ID }.values private fun Map<Int, DepotInfo>.filterSelected(selectedAppIds: Map<Int, Boolean>) = filter { (_, depot) -> selectedAppIds[depot.dlcAppId] == true }.values // Then use: val baseGameInstallBytes = downloadableDepots .filterBaseGame() .sumOf { it.manifests["public"]?.size ?: 0 } val baseGameDownloadBytes = downloadableDepots .filterBaseGame() .sumOf { it.manifests["public"]?.download ?: 0 }
203-206: Complex set operations reduce readability.The button-enabled logic for already-installed games uses multiple set operations that are difficult to parse. Consider adding intermediate variables or comments explaining the logic.
💡 Example refactor
if (installedApp != null) { - val installed = installedDlcIds.toSet() - mainAppDlcIdsWithoutProperDepotDlcIds.toSet() - val realSelectedAppIds = selectedAppIds.filter { it.value }.keys - installed - return (realSelectedAppIds.size - 1) > 0 // -1 for main app + // DLCs that are actually installed (excluding pseudo-DLCs without proper depot IDs) + val actuallyInstalledDlcs = installedDlcIds.toSet() - mainAppDlcIdsWithoutProperDepotDlcIds.toSet() + // DLCs selected by user that aren't already installed + val newDlcsToInstall = selectedAppIds.filter { it.value }.keys - actuallyInstalledDlcs + // Enable button if there are new DLCs to install (excluding main app which is always selected) + return (newDlcsToInstall.size - 1) > 0 }
378-380: Simplify nested filtering for better readability.The nested
filtercalls make it difficult to understand which apps are being passed toonInstall. Consider using an intermediate variable.💡 Example refactor
+ val enabledAndSelected = selectedAppIds + .filterKeys { it in enabledAppIds && enabledAppIds[it] == true } + .filterValues { it } + .keys + .toList() + Button( enabled = installButtonEnabled(), onClick = { - onInstall(selectedAppIds - .filter { selectedId -> selectedId.key in enabledAppIds.filter { enabledId -> enabledId.value } } - .filter { selectedId -> selectedId.value }.keys.toList()) + onInstall(enabledAndSelected) } ) {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
1340-1352: Critical: Force unwrap can cause NPE despite previous review.Line 1341 still uses a force unwrap
getInstalledApp(downloadingAppId)!!which will throw an NPE if the app info doesn't exist. A past review comment flagged this issue (supposedly addressed in commit d2b1e08), but the force unwrap remains.The
MODIFYING_MARKERexistence doesn't guarantee the database entry exists - it could have been deleted by a race condition, database corruption, or manual file manipulation.🔎 Proposed safe handling
// Update database runBlocking { // Update Saved AppInfo if (MarkerUtils.hasMarker(appDirPath, Marker.MODIFYING_MARKER)) { - val appInfo = getInstalledApp(downloadingAppId)!! + val appInfo = getInstalledApp(downloadingAppId) + if (appInfo == null) { + Timber.w("MODIFYING_MARKER exists but no AppInfo found for $downloadingAppId, treating as new install") + instance?.appInfoDao?.insert( + AppInfo( + downloadingAppId, + isDownloaded = true, + downloadedDepots = entitledDepotIds, + dlcDepots = selectedDlcAppIds, + ), + ) + return@runBlocking + } val updatedDownloadedDepots = (appInfo.downloadedDepots + entitledDepotIds).distinct() val updatedDlcDepots = (appInfo.dlcDepots + selectedDlcAppIds).distinct()
909-912: Fix duplicate main app ID when resuming downloads.When
downloadApp(appId)is called to resume a download, it passescurrentDownloadInfo?.downloadingAppIds(which already contains the mainappId) to thedownloadApp(appId, dlcAppIds)overload. This causes the mainappIdto be added twice:
- Line 1125 adds it via
downloadingAppIds.addAll(dlcAppIds)- Line 1128 adds it again via
downloadingAppIds.add(appId)This duplicate breaks download completion tracking at line 1369, since
removeIf()(line 1366) only removes the first occurrence. The completion handler fires only after the duplicate is also removed, delaying completion markers.Filter out the main
appIdfromdownloadingAppIdsbefore passing it asdlcAppIdsat line 911, or skip addingappIdif it's already in the list.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
494-516: Optional: Consider shorter function name.The function logic correctly identifies DLCs without proper depot information (case 2 from the PR description), but the name
getMainAppDlcIdsWithoutProperDepotDlcIdsis quite verbose. Consider a shorter alternative likegetStubDlcIdsorgetEmptyDepotDlcIdsto improve readability.
565-568: Clarify the OS filtering logic.The OS check uses a double negative that's hard to parse:
if (!(depot.osList.contains(OS.windows) || (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) )This can be simplified using De Morgan's law for better readability:
🔎 Proposed refactor
- // 2. Supported OS - if (!(depot.osList.contains(OS.windows) || - (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) - ) - return false + // 2. Supported OS: must be Windows or unspecified (no Linux/macOS) + val isWindowsOrUnspecified = depot.osList.contains(OS.windows) || + (depot.osList.isEmpty() || (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) + if (!isWindowsOrUnspecified) + return false
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (10)
app/src/main/java/app/gamenative/service/SteamService.kt (10)
42-43: LGTM: New imports are necessary and properly used.The new imports for marker utilities and thread-safe collections are utilized throughout the DLC download orchestration logic.
Also applies to: 147-147
430-448: LGTM: New public accessors simplify AppInfo retrieval.The new
getDownloadableDlcAppsOfandgetInstalledAppmethods provide cleaner access to DLC and installation data. TherunBlockingpattern is consistent with existing code patterns in this service.
588-649: LGTM: Depot filtering logic correctly aggregates main and DLC depots.The
getMainAppDepotsandgetDownloadableDepotsfunctions properly leverage the centralizedfilterForDownloadableDepotshelper. The customDepotInfocreation for indirect DLC depots (lines 633-644) correctly tags each depot with its source DLC app ID, supporting the multi-app download feature.
893-900: LGTM: DLC cleanup on app deletion is appropriate.The deletion logic correctly removes AppInfo and related data for indirect DLC apps when the main app is deleted, ensuring proper cleanup of the database. Based on learnings, this cleanup is appropriate for explicit user-initiated deletion (as opposed to logout, where AppInfo should be preserved).
1124-1128: LGTM: Thread-safe list for tracking downloading apps.The use of
CopyOnWriteArrayListfordownloadingAppIdsis appropriate for tracking concurrent DLC downloads. The list is built correctly with selected DLC app IDs followed by the main app ID.
1131-1142: LGTM: Correct handling of DLCs bundled in main app depots.This logic correctly handles the edge case where selected DLC content is bundled within the main app's depots (case 2 from the PR description). The stub DLC IDs are preserved for database tracking (line 1136) while the download is correctly simplified to only the main app (lines 1139-1141).
1230-1272: LGTM: Listener creation correctly separates main app and DLC apps.The listener creation logic properly handles:
- Main app with its depots and associated DLC IDs (lines 1233-1253)
- Individual DLC apps with their own depots (lines 1255-1272)
- Cleanup after download completion (line 1283)
The conditional creation based on
mainAppDepots.isNotEmpty()prevents unnecessary listener creation when all content is bundled.
1366-1366: LGTM: removeIf predicate is correct.The predicate
{ it == downloadingAppId }correctly removes only the completed app from the tracking list. This was fixed from the inverted condition flagged in a previous review.
1366-1382: Verify: Potential race condition in concurrent DLC completion.When multiple DLC downloads complete concurrently, each listener executes:
- Line 1366:
downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId }- Line 1369:
if (downloadInfo.downloadingAppIds.isEmpty())While
CopyOnWriteArrayListmakes individual operations thread-safe, the compound check-then-act pattern here isn't atomic. If two DLC listeners complete simultaneously:
- Both could see
isEmpty() = trueafter their respective removals- Both would attempt cleanup (lines 1370-1382)
- Or both could see
isEmpty() = false, and neither would clean upA past review flagged this same issue (supposedly addressed in commit 7a9d421), but the current code lacks synchronization. Please verify whether:
- The cleanup operations are idempotent (safe to run multiple times)
- There's synchronization elsewhere that prevents this scenario
- Additional locking is needed
As per past review suggestion, consider using synchronized block:
🔎 Proposed fix
// Remove completed appId from downloadInfo.dlcAppIds - downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } - - // All downloading appIds are removed - if (downloadInfo.downloadingAppIds.isEmpty()) { + val allComplete = synchronized(downloadInfo) { + downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } + downloadInfo.downloadingAppIds.isEmpty() + } + + // All downloading appIds are removed + if (allComplete) {
1385-1397: LGTM: Failure handling stops entire download appropriately.When any DLC app download fails, the entire download job is removed (line 1387), which is the correct behavior. The user is notified with a toast message to retry the download.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
909-912: Fix duplicate main app ID in resume downloads.When resuming a download via line 911,
currentDownloadInfo.downloadingAppIds(containing both main app ID and DLC IDs) is passed as thedlcAppIdsparameter. The main implementation then:
- Line 1125 adds all
dlcAppIdsto the new list- Line 1131 adds
appIdagain ifmainAppDepots.isNotEmpty()This creates a duplicate main app ID when resuming. The conditional check at line 1130 prevents duplicates in fresh downloads but not when the resume path passes an already-populated
downloadingAppIds. Either filterappIdfrom the parameter at line 911, or check if it's already present before adding at line 1131.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
494-516: Consider improving readability of complex filtering logic.The function correctly identifies DLC IDs without proper depot DLC IDs, but the nested conditionals and force unwrap on line 505 make it harder to maintain. Consider these optional improvements:
- The
checkMap.size == 1check guarantees the map isn't empty, but usingcheckMap.values.first()would be clearer thancheckMap[checkMap.keys.first()]!!- Extract the multi-condition check (lines 506-508) into a named boolean variable for clarity
🔎 Proposed refactoring for readability
checkingAppDlcIds.forEach { checkingDlcId -> val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } if (checkMap.size == 1) { - val depotInfo = checkMap[checkMap.keys.first()]!! + val depotInfo = checkMap.values.first() - if (depotInfo.osList.contains(OS.none) && - depotInfo.manifests.isEmpty() && - hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { + val isHiddenStubDlc = depotInfo.osList.contains(OS.none) && + depotInfo.manifests.isEmpty() && + hiddenDlcAppIds.contains(checkingDlcId) + if (isHiddenStubDlc) { mainAppDlcIds.add(checkingDlcId) } } }
558-586: Consider clarifying the OS filtering condition.The
filterForDownloadableDepotsfunction correctly extracts common filtering logic. However, the OS check on lines 565-567 is difficult to parse:if (!(depot.osList.contains(OS.windows) || (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) )This evaluates to: "reject if NOT (windows OR (neither linux nor macos))", which is correct for accepting Windows or unknown/none OS depots. Consider this optional refactoring for clarity:
🔎 Proposed refactoring for clarity
- // 2. Supported OS - if (!(depot.osList.contains(OS.windows) || - (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) - ) - return false + // 2. Supported OS: Windows or OS-agnostic (no Linux/macOS specified) + val isWindowsOrUnknownOS = depot.osList.contains(OS.windows) || + (depot.osList.none { it == OS.linux || it == OS.macos }) + if (!isWindowsOrUnknownOS) + return false
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (8)
app/src/main/java/app/gamenative/service/SteamService.kt (8)
42-43: LGTM! Imports aligned with new marker-based flow and concurrent DLC tracking.The new imports support the expanded DLC-aware download flows:
MarkerUtilsandMarkerfor marker-based state management, andCopyOnWriteArrayListfor thread-safe tracking of concurrent DLC downloads.Also applies to: 147-147
430-432: LGTM! New public APIs for DLC management.The
getDownloadableDlcAppsOfandgetInstalledAppmethods follow the established pattern of usingrunBlocking(Dispatchers.IO)for synchronous companion object methods, consistent with similar methods in this class.Also applies to: 438-440
442-448: LGTM! Cleaner abstraction using the new helper.The refactoring of
getInstalledDepotsOfandgetInstalledDlcDepotsOfto use the newgetInstalledApphelper improves code maintainability and consistency.
588-601: LGTM! Clean separation for main app depot selection.The
getMainAppDepotsfunction provides a focused API for retrieving only the main app's downloadable depots, properly delegating filtering to the newfilterForDownloadableDepotshelper.
607-649: LGTM! Correctly handles DLC depots from separate DLC apps.The expanded
getDownloadableDepotsfunction now supports case 3 from the PR description (DLC depots placed inside another DLC app). The intentional passing ofnullforownedDlcon line 628 skips the ownership check for indirect DLCs, which is appropriate since ownership is verified at the app level.
893-899: LGTM! Properly cleans up indirect DLC app data.The updated
deleteAppfunction correctly extends cleanup to include indirect DLC apps, ensuring all related data is removed from the database.
1114-1150: LGTM! Proper handling of resume and empty depot cases.The implementation correctly:
- Filters already-downloaded depots when
MODIFYING_MARKERexists (lines 1114-1119)- Handles case 2 DLC pattern (depots without valid dlcAppId) by consolidating to main app download (lines 1138-1146)
- Validates that depots exist before proceeding (line 1150)
1370-1387: Verify: Potential race condition when multiple DLC downloads complete concurrently.Although
downloadingAppIdsis now aCopyOnWriteArrayList(addressing the thread-safety of iteration), the sequence at lines 1371-1374 is still not atomic:downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } // All downloading appIds are removed if (downloadInfo.downloadingAppIds.isEmpty()) { // Cleanup... }If two DLC downloads complete simultaneously, both threads could:
- Successfully remove their respective IDs
- Both observe
isEmpty() == true- Both proceed to cleanup (duplicate marker removal, duplicate events)
Past review (commit 7a9d421) claimed to address a similar race condition, but the current implementation still has this gap.
🔎 Proposed fix using synchronized block
- // Remove completed appId from downloadInfo.dlcAppIds - downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } - - // All downloading appIds are removed - if (downloadInfo.downloadingAppIds.isEmpty()) { + // Remove completed appId and check atomically + val shouldCleanup = synchronized(downloadInfo) { + downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } + downloadInfo.downloadingAppIds.isEmpty() + } + + if (shouldCleanup) { // Handle completion: add markers MarkerUtils.addMarker(appDirPath, Marker.DOWNLOAD_COMPLETE_MARKER)
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/utils/SteamUtils.kt
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)
762-774: LGTM!The refactored logic properly tracks appended DLC IDs to prevent duplicates, and sorting ensures deterministic output in the configuration file.
776-782: LGTM!The logic correctly emits DLC entries only for installed DLC apps that haven't already been added via depot-based discovery.
| hiddenDlcApps?.forEach { hiddenDlcApp -> | ||
| if (!appendedDlcIds.contains(hiddenDlcApp.id) && | ||
| // only add hidden dlc apps if it is not a DLC of the main app | ||
| appInfo!!.depots.filter { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }.size <= 1) { | ||
| appendLine("${hiddenDlcApp.id}=dlc${hiddenDlcApp.id}") | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential NullPointerException on appInfo!!.
appInfo is nullable (returned from getAppInfoOf(steamAppId) at line 737), but line 788 uses force unwrap appInfo!! without a prior null check. If app info isn't available, this will crash.
Additionally, the condition .size <= 1 allows hidden DLCs that appear in exactly 1 depot of the main app. If the intent is to exclude DLCs associated with the main app, should this be == 0 instead?
🔎 Proposed fix
// only add hidden dlc apps if not found in appendedDlcIds
hiddenDlcApps?.forEach { hiddenDlcApp ->
- if (!appendedDlcIds.contains(hiddenDlcApp.id) &&
+ if (appInfo != null &&
+ !appendedDlcIds.contains(hiddenDlcApp.id) &&
// only add hidden dlc apps if it is not a DLC of the main app
- appInfo!!.depots.filter { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }.size <= 1) {
+ appInfo.depots.none { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }) {
appendLine("${hiddenDlcApp.id}=dlc${hiddenDlcApp.id}")
}
}🤖 Prompt for AI Agents
In app/src/main/java/app/gamenative/utils/SteamUtils.kt around lines 785-791,
avoid force-unwrapping nullable appInfo and tighten the depot check: first, add
a null check for appInfo (e.g., if (appInfo == null) continue) before accessing
its depots to prevent an NPE, and change the depot membership condition from
size <= 1 to size == 0 so hiddenDlcApp is only appended when it is not a DLC of
the main app at all; keep the existing appendedDlcIds check and appendLine
behavior otherwise.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
909-912: Remove appId from dlcAppIds to prevent duplicate entries in downloadingAppIds.At line 911, when retrying a download via
downloadApp(appId), the code passescurrentDownloadInfo?.downloadingAppIdsas thedlcAppIdsparameter. If this list already containsappId(which it will if the main app is being downloaded), then at line 1129,appIdis added again whenmainAppDepots.isNotEmpty(). This creates duplicate entries (e.g.,[456, 789, 123, 123]).Commit 887a031 only added a check for empty main app depots—it did not address this duplicate issue. Filter out
appIdfromdlcAppIdsat line 911 before passing it forward, or check thatappIdis not already indownloadingAppIdsbefore adding at line 1129.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
1098-1148: Review the logic when DLC depots are empty.Lines 1136-1144 contain special handling when
dlcAppDepots.isEmpty():
- Adds
calculatedDlcAppIdstomainAppDlcIds- Clears
calculatedDlcAppIdsanddownloadingAppIds- Adds only main
appIdtodownloadingAppIdsThis appears to handle the case where all DLC depots are embedded in the main app (cases 1 & 2 from PR description). However, ensure this logic correctly handles the scenario where:
- User selects DLCs for installation
- All selected DLC depots are in the main app's depot list
- The DLC IDs should still be tracked for database persistence
The current code adds them to
mainAppDlcIdswhich is passed to the main app listener (line 1242), so this should work correctly. Consider adding a comment explaining this flow for future maintainers.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)
1211-1240: Consider adding error handling in the onInstall callback.The
onInstallcallback launches a coroutine to download the app and selected DLCs, but doesn't include error handling. IfSteamService.downloadApp()throws an exception, the user won't receive feedback about the failure.🔎 Suggested enhancement with error handling
onInstall = { dlcAppIds -> hideGameManagerDialog(gameId) val installedApp = SteamService.getInstalledApp(gameId) if (installedApp != null) { // Remove markers if the app is already installed MarkerUtils.removeMarker(getAppDirPath(gameId), Marker.STEAM_DLL_REPLACED) MarkerUtils.removeMarker(getAppDirPath(gameId), Marker.STEAM_DLL_RESTORED) MarkerUtils.removeMarker(getAppDirPath(gameId), Marker.STEAM_COLDCLIENT_USED) } PostHog.capture( event = "game_install_started", properties = mapOf("game_name" to (appInfo?.name ?: "")) ) CoroutineScope(Dispatchers.IO).launch { + try { SteamService.downloadApp(gameId, dlcAppIds) + } catch (e: Exception) { + Timber.e(e, "Failed to start download for gameId=$gameId") + withContext(Dispatchers.Main) { + Toast.makeText( + context, + context.getString(R.string.download_failed, e.message ?: "Unknown error"), + Toast.LENGTH_SHORT + ).show() + } + } } },Note: This assumes error handling isn't already present within
SteamService.downloadApp(). If errors are handled there, this addition may be unnecessary.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (2)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (3)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
GameManagerDialog(73-392)app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (1)
getGameDisplayInfo(73-179)app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (2)
getGameDisplayInfo(48-690)getGameDisplayInfo(53-57)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (13)
app/src/main/java/app/gamenative/service/SteamService.kt (8)
42-43: LGTM: Import additions support new DLC-aware download flow.The new imports (MarkerUtils, Marker, CopyOnWriteArrayList) are properly used throughout the file for marker-based state management and thread-safe DLC tracking.
Also applies to: 147-147
430-440: LGTM: New public APIs for DLC support.The new
getDownloadableDlcAppsOfandgetInstalledAppmethods properly wrap DAO calls usingrunBlocking(Dispatchers.IO), consistent with the existing pattern in this codebase.
494-516: LGTM: Edge-case handler for DLC depots without proper dlcAppId.This method correctly identifies DLC app IDs from depots that lack proper
dlcAppIdvalues by checking for single-depot DLCs with no OS, no manifests, and presence in hidden DLC list. The logic handles the documented case where "DLC depots without a valid dlcappid [are] treated as included in the main game."
558-601: LGTM: Refactored depot filtering with proper 64-bit preference.The new
filterForDownloadableDepotshelper consolidates filtering logic and correctly prioritizes 64-bit depots when available. ThegetMainAppDepotsmethod properly applies these filters to return only the main app's downloadable depots.
607-649: LGTM: Extended depot collection includes indirect DLC apps.The updated
getDownloadableDepotscorrectly handles the case where DLC depots are placed inside separate DLC apps (case 3 from PR description). The customDepotInfoobjects properly setdlcAppIdto track which DLC each depot belongs to.
893-899: LGTM: Cascade deletion correctly cleans up indirect DLC data.The updated
deleteAppproperly removes database entries (AppInfo, ChangeNumbers, FileChangeLists) for indirect DLC apps when the main game is deleted. This prevents orphaned data.
1368-1384: LGTM: Download completion logic correctly handles multi-app downloads.The listener now properly:
- Removes the completed app from
downloadingAppIds(line 1369 uses correct==comparison)- Only triggers final completion when all apps are downloaded (line 1372)
- Sets markers and emits events only once (lines 1374-1383)
The use of
CopyOnWriteArrayListprovides thread safety for concurrent modifications.
1336-1366: Null safety in onDownloadCompleted is properly handled.The code correctly checks if
getInstalledApp(downloadingAppId)returns null and handles both cases: updating an existing app info record if found, or inserting a new one if not found. No force-unwrap issue exists.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (5)
51-52: LGTM! Clean imports for the new dialog components.The imports are correctly structured and aligned with the new DLC management feature.
135-147: LGTM! Consistent state management pattern.The GameManagerDialog state management follows the same pattern as the existing
installDialogStatesanduninstallDialogAppIds, maintaining consistency across the codebase.
630-640: LGTM! ManageGameContent menu option correctly integrated.The menu option is appropriately restricted to installed games (line 613 check ensures this) and follows the same pattern as other Steam-specific menu options.
857-866: LGTM! Proper state tracking with snapshotFlow.The state tracking implementation correctly uses
snapshotFlowto observe changes in the GameManagerDialog state, consistent with the existinginstallDialogStatetracking pattern.
457-462: Clarify permission handling for GameManagerDialog in the new flow.The GameManagerDialog is now shown directly without a preceding permission check, whereas the previous INSTALL_APP_PENDING flow explicitly checked permissions (lines 967-973). The developer comment suggests permissions "will be handled by the permission launcher in AdditionalDialogs," but this isn't clear from the visible code flow.
While the dialog's operations (reading cached depot info via
getDownloadableDepots()and checking available space viaStatFs) don't strictly require READ/WRITE_EXTERNAL_STORAGE permissions, the inconsistency in permission handling between the old and new flows warrants clarification. If storage permissions are intended to be required, the check should be explicit; if not needed, the developer comment should explain the change in approach.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
app/src/main/java/app/gamenative/service/SteamService.kt (4)
494-516: Consider simplifying and documenting the complex DLC filtering logic.This method has nested filters and complex conditional logic that makes it difficult to understand and maintain. The method name is also very long and doesn't clearly convey the purpose.
Consider:
- Breaking down the logic into smaller, well-named helper methods
- Adding inline comments explaining the business logic for each filter condition
- Renaming to something clearer like
getDlcIdsWithoutManifestsorgetPlaceholderDlcIds🔎 Example refactor for clarity
+/** + * Identifies DLC app IDs that exist in the main game's depot list but have no + * actual manifests (placeholder/hidden DLCs). These are DLCs with: + * - Exactly one depot entry in main app + * - OS type "none" and empty manifests + * - Present in hidden DLC list + */ -fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { +fun getPlaceholderDlcIds(appId: Int): List<Int> { val mainAppDlcIds = mutableListOf<Int>() val hiddenDlcAppIds = getHiddenDlcAppsOf(appId).orEmpty().map { it.id } val appInfo = getAppInfoOf(appId) if (appInfo != null) { - // for each of the dlcAppId found in main depots, filter the count = 1, add that dlcAppId to dlcAppIds + // Find all unique DLC IDs referenced in main app depots val checkingAppDlcIds = appInfo.depots.filter { it.value.dlcAppId != INVALID_APP_ID }.map { it.value.dlcAppId }.distinct() checkingAppDlcIds.forEach { checkingDlcId -> val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } + // Only process DLCs with exactly one depot entry (placeholder pattern) if (checkMap.size == 1) { val depotInfo = checkMap[checkMap.keys.first()]!! - if (depotInfo.osList.contains(OS.none) && - depotInfo.manifests.isEmpty() && - hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { + if (isPlaceholderDepot(depotInfo, checkingDlcId, hiddenDlcAppIds)) { mainAppDlcIds.add(checkingDlcId) } } } } return mainAppDlcIds } + +private fun isPlaceholderDepot(depot: DepotInfo, dlcId: Int, hiddenDlcIds: List<Int>): Boolean { + return depot.osList.contains(OS.none) && + depot.manifests.isEmpty() && + hiddenDlcIds.contains(dlcId) +}
566-567: Simplify confusing double negation in OS filter.The condition
!(depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))uses double negation which is hard to read and verify.🔎 Proposed simplification
// 2. Supported OS - if (!(depot.osList.contains(OS.windows) || - (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) - ) + // Accept if: has Windows OR (no Linux AND no macOS) + val hasWindows = depot.osList.contains(OS.windows) + val hasOnlyLinuxOrMac = depot.osList.contains(OS.linux) || depot.osList.contains(OS.macos) + if (!hasWindows && hasOnlyLinuxOrMac) return false
1133-1144: Consider refactoring side effect onmainAppDlcIds.The logic modifying
mainAppDlcIdsin this block (line 1138) creates a side effect that makes the code harder to reason about. The variable is used later to persist DLC information to the database (line 1242).While the current logic works, consider computing the final DLC list once after all conditions are evaluated, rather than mutating it conditionally.
🔎 Alternative approach
// There are some apps, the dlc depots does not have dlcAppId in the data, need to set it back val mainAppDlcIds = getMainAppDlcIdsWithoutProperDepotDlcIds(appId) + val finalDlcIds = calculatedDlcAppIds.toMutableList() // If there are no DLC depots, download the main app only if (dlcAppDepots.isEmpty()) { - // Because all dlcIDs are coming from main depots, need to add the dlcID to main app in order to save it to db after finish download - mainAppDlcIds.addAll(calculatedDlcAppIds) - // Refresh id List, so only main app is downloaded - calculatedDlcAppIds.clear() + finalDlcIds.clear() downloadingAppIds.clear() downloadingAppIds.add(appId) + } else { + finalDlcIds.addAll(0, mainAppDlcIds) } + // Use finalDlcIds in listener creation instead of mainAppDlcIds + calculatedDlcAppIds
476-477: Remove commented-out code.The commented line and explanation are useful for understanding the change, but should be removed from the final code. The explanation belongs in:
- The commit message (already present)
- Code comments explaining the current behavior
- This PR description (already documented)
🔎 Proposed cleanup
- /* Optional DLC depots are skipped */ - /* This line is commented out because game manager will now handle those optional DLCs */ - //depot.optionalDlcId == depot.dlcAppId -> false - /* ① licence cache */If you want to keep a note in the code, use this format instead:
+ /* Optional DLC filtering is now handled by GameManagerDialog UI */ + /* ① licence cache */
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (5)
app/src/main/java/app/gamenative/service/SteamService.kt (5)
1122-1130: LGTM! Thread-safe collection used appropriately.The use of
CopyOnWriteArrayListfordownloadingAppIdsis appropriate here because:
- The list is small (typically < 10 items)
- It's accessed concurrently by multiple DLC download listeners
- Write operations (add/remove) are infrequent compared to reads (isEmpty check)
This addresses the race condition mentioned in past reviews.
1336-1385: LGTM! Download completion logic properly handles update and insert cases.The refactored completion logic correctly:
- Safely retrieves existing
AppInfo(nullable)- Merges depot lists using distinct() to avoid duplicates
- Updates or inserts as appropriate
- Uses sorted() for consistent ordering
- Thread-safely removes completed app ID from
downloadingAppIds- Cleans up markers and persisted state only after all downloads complete
The past NPE issue has been properly addressed with the nullable check.
1232-1274: LGTM! Per-app listener architecture properly separates concerns.The listener creation logic correctly:
- Creates separate listeners for main app and each DLC
- Passes
mainAppDlcIdsonly to the main app listener (for DB persistence)- Uses empty list for DLC listeners since DLC-of-DLC is not supported (matches PR description)
- Creates proper depot-to-index mappings for progress tracking
- Sets correct install directories for all AppItems
This design aligns with the documented DLC depot patterns.
1102-1117: Depot filtering logic is intentionally defensive and correctly handles the edge cases mentioned.The three filtering concerns you raised are already addressed by the code:
DLCs without proper depots: Line 1102-1106 explicitly checks
depot.manifests.isNotEmpty(), which filters OUT the improper depots mentioned ingetMainAppDlcIdsWithoutProperDepotDlcIds(those with empty manifests on OS.none). This is correct defensive programming.Three && conditions: The conditions in dlcAppDepots filtering (lines 1109-1112) are intentional safeguards: avoid duplication, verify user selection, confirm database availability, and ensure actual content exists. This is appropriate filtering, not overly restrictive.
Downloaded depots:
mainAppDepotscorrectly filters outdownloadedDepotsat line 1116. FordlcAppDepots, the sourcegetDownloadableDlcAppsOf()already queries the database for only downloadable DLC apps, so additional filtering is not needed.
893-900: Add guard to prevent deleting independently-owned or installed DLCs.The deletion at lines 894–899 removes DLC app data for all downloadable DLCs without checking if they are independently owned or installed. This could inadvertently delete data for:
- DLCs purchased separately from the base game
- DLCs that may be installed for other game instances
Reference the
installedDlcIdscheck visible inGameManagerDialog.kt(line 124) and only delete DLC data if it's neither independently owned nor installed elsewhere.
| // All downloading appIds are removed | ||
| if (downloadInfo.downloadingAppIds.isEmpty()) { | ||
| // Handle completion: add markers | ||
| MarkerUtils.addMarker(appDirPath, Marker.DOWNLOAD_COMPLETE_MARKER) | ||
| PluviaApp.events.emit(AndroidEvent.LibraryInstallStatusChanged(downloadInfo.gameId)) | ||
|
|
||
| MarkerUtils.removeMarker(appDirPath, Marker.STEAM_DLL_REPLACED) | ||
| MarkerUtils.removeMarker(appDirPath, Marker.STEAM_COLDCLIENT_USED) | ||
|
|
||
| // Clear persisted bytes file on successful completion | ||
| downloadInfo.clearPersistedBytesDownloaded(appDirPath) | ||
|
|
||
| removeDownloadJob(downloadInfo.gameId) | ||
| } | ||
| } |
There was a problem hiding this comment.
Games are not showing as downloaded consistently. I'm trying to download Nidhogg and a lot of the time it gets stuck at 100%. Then if I say pause/resume/delete, nothing happens and it stays stuck
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (4)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (3)
103-134: LaunchedEffect triggers on both visibility changes.The effect runs when
visiblechanges tofalseas well, causing unnecessary state clearing when the dialog closes. Guard the logic to only execute when the dialog becomes visible.🔎 Proposed fix
LaunchedEffect(visible) { + if (!visible) return@LaunchedEffect + scrollState.animateScrollTo(0) downloadableDepots.clear() allDownloadableApps.clear()
274-292: Missing accessibility: No content description for hero image.The
CoilImagelacks acontentDescriptionfor screen readers. For decorative images, explicitly mark asnull; otherwise, provide a meaningful description.🔎 Suggested fix
CoilImage( modifier = Modifier.fillMaxSize(), imageModel = { displayInfo.heroImageUrl }, - imageOptions = ImageOptions(contentScale = ContentScale.Crop), + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + contentDescription = null // Decorative image + ), loading = { LoadingScreen() },
110-131: Add null-safety check before accessing first depot element.Line 131 calls
downloadableDepots.toSortedMap().values.first()which will throwNoSuchElementExceptionif the collection is empty. SinceSteamService.getDownloadableDepots(gameId)can return an empty map whengetAppInfoOf(appId)returns null (SteamService:604), this crash is possible. Add a guard to check if downloadable depots exist before calling.first(), or usefirstOrNull()with appropriate error handling.app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)
787-793: Potential NullPointerException onappInfo!!.
appInfois nullable (returned fromgetAppInfoOf(steamAppId)at line 739), but line 790 uses force unwrapappInfo!!without a prior null check. The null check at line 796 happens AFTER this code block, so if app info isn't available, this will crash.Additionally, the condition
.size <= 1allows hidden DLCs that appear in exactly 1 depot of the main app. If the intent is to exclude DLCs associated with the main app, this should be== 0or usenone { }instead.🔎 Proposed fix
// only add hidden dlc apps if not found in appendedDlcIds hiddenDlcApps?.forEach { hiddenDlcApp -> - if (!appendedDlcIds.contains(hiddenDlcApp.id) && + if (appInfo != null && + !appendedDlcIds.contains(hiddenDlcApp.id) && // only add hidden dlc apps if it is not a DLC of the main app - appInfo!!.depots.filter { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }.size <= 1) { + appInfo.depots.none { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }) { appendLine("${hiddenDlcApp.id}=dlc${hiddenDlcApp.id}") } }
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)
992-1017: Consider logging exceptions for debugging.The try-catch correctly handles parsing failures by returning
-1, but the caught exception is silently swallowed. Consider adding a warning log before the callback to aid debugging when DirectX version detection fails.🔎 Optional improvement
} catch (e: Exception){ + Timber.w(e, "[DX Fetch] Failed to parse DirectX version for appId $steamAppId") callback(-1) }app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
417-427: Install action correctly filters enabled selections.The button's onClick logic properly filters selected DLCs to only include those that are both selected and enabled (not already installed). The filtering chain, while complex, is functionally correct.
💡 Optional simplification for readability
Button( enabled = installButtonEnabled(), onClick = { - onInstall(selectedAppIds - .filter { selectedId -> selectedId.key in enabledAppIds.filter { enabledId -> enabledId.value } } - .filter { selectedId -> selectedId.value }.keys.toList()) + val enabledAndSelected = selectedAppIds + .filter { (id, isSelected) -> + isSelected && enabledAppIds[id] == true + } + .keys.toList() + onInstall(enabledAndSelected) }app/src/main/java/app/gamenative/service/SteamService.kt (1)
1365-1380: Consider atomic completion check to prevent duplicate cleanup.While
CopyOnWriteArrayListprovides thread-safe modifications, the sequence ofremoveIffollowed byisEmpty()check (lines 1365-1368) can allow multiple concurrent listeners to both see an empty list and execute cleanup logic.The cleanup operations are mostly idempotent (markers, file deletion), but event emission could be duplicated. Consider using a synchronized block or atomic flag for the final completion check.
🔎 Optional improvement for atomic completion
// Remove completed appId from downloadInfo.downloadingAppIds - downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } - - // All downloading appIds are removed - if (downloadInfo.downloadingAppIds.isEmpty()) { + val allComplete = synchronized(downloadInfo) { + downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } + downloadInfo.downloadingAppIds.isEmpty() + } + + // All downloading appIds are removed + if (allComplete) {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/res/values-zh-rCN/strings.xmlapp/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/res/values/strings.xml
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
🪛 detekt (1.23.8)
app/src/main/java/app/gamenative/utils/SteamUtils.kt
[warning] 1015-1015: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (12)
app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt (1)
22-23: LGTM!The addition of the
ManageGameContentenum constant is clean and follows Kotlin best practices. The trailing comma on line 22 is good style as it minimizes diff noise for future additions.app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)
719-725: LGTM! Depots file handling is correct.The delete-and-recreate approach ensures fresh data, and the implementation correctly follows the project learning to only create
depots.txtwhen actual depot data is available (non-null). The sorted output ensures deterministic file content.app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (2)
136-221: Helper functions are well-structured.The size calculation and name resolution functions properly handle nullable types and provide appropriate fallbacks. The
getInstallSizeInfoaggregation correctly accounts for base game and selected DLCs with safe null-handling throughout.
236-248: Install button logic correctly validates prerequisites.The function properly checks available storage against required space and accurately filters out already-installed DLCs before enabling the install action. The
-1adjustment for the main app in the count is correct.app/src/main/java/app/gamenative/service/SteamService.kt (8)
430-448: New DLC query methods are well-structured.The methods provide clean access to DLC and installation data through the DAO layer. The use of
runBlockingis acceptable here per project learnings, given these were converted from synchronous implementations.Based on learnings,
runBlockingis a valid approach for these DAO calls.
490-512: Edge case handling for incomplete DLC depot metadata is sound.The method correctly identifies DLCs that lack proper depot entries by checking for marker characteristics (single depot, OS.none, empty manifests) and cross-referencing with hidden DLC apps. This addresses the documented case where DLC depots don't have valid dlcAppId in the data.
551-597: Depot filtering logic is comprehensive and well-factored.The
filterForDownloadableDepotsfunction properly handles OS compatibility, architecture preference (64-bit over 32-bit), DLC ownership, and language matching. The extracted common filter improves maintainability and ensures consistent filtering across main app and DLC depots.
603-645: Depot aggregation correctly merges main app and indirect DLC depots.The method properly combines main app depots with depots from indirect DLC apps (case 3 from the PR description). The
DepotInforeconstruction correctly reassignsdlcAppIdto track which DLC app owns each depot, enabling proper per-DLC download tracking.
889-895: Cascade deletion of indirect DLCs is correct.When deleting a main app, this properly cleans up related indirect DLC app data (AppInfo, ChangeNumbers, FileChangeLists) to maintain database consistency. The deletion runs within the existing transaction, ensuring atomicity.
1077-1308: Download orchestration properly handles per-DLC tracking.The implementation correctly:
- Separates main app depots from DLC depots with appropriate filtering
- Uses
CopyOnWriteArrayListfor thread-safedownloadingAppIdstracking- Creates isolated
AppDownloadListenerinstances per app/DLC with depot-scoped progress tracking- Handles resume by excluding already-downloaded depots
The per-app listener pattern enables concurrent DLC downloads with independent progress reporting.
1313-1441: AppDownloadListener correctly handles per-app completion.The listener properly:
- Tracks individual app completion with safe nullable handling (no force unwraps)
- Removes completed app IDs from
downloadingAppIdsusing correct predicate- Defers final cleanup until all apps complete (
downloadingAppIds.isEmpty())- Updates or inserts
AppInfoappropriately based on installation stateThe fixes from previous reviews (null safety at line 1337, correct
removeIfpredicate at line 1365) are correctly applied.
905-920: No action required—the download restart flow correctly prevents duplicate main app IDs.Line 1090's early return check (
if (downloadJobs.contains(appId)) return getAppDownloadInfo(appId)) prevents creating a newdownloadingAppIdslist when a download is already active, eliminating the duplicate concern. When resuming a download, the existingDownloadInfoobject is returned unchanged. When starting a fresh download, the newly created list at line 1118 has no duplicates because it starts clean and adds the mainappIdonly once at line 1125.Likely an incorrect or invalid review comment.
| <string name="steam_2fa_device">请输入您验证器应用中的双重验证码</string> | ||
| <string name="steam_2fa_email">请输入发送至邮箱 %s 的验证码</string> | ||
| <string name="steam_install_space_prompt">要安装的应用有以下空间要求:\n\n\t下载大小:%1$s\n\t磁盘占用空间:%2$s\n\t可用空间:%3$s</string> | ||
| <string name="steam_install_space">下载大小: %1$s\n磁碟占用空间: %2$s\n可用空间: %3$s</string> |
There was a problem hiding this comment.
Fix terminology inconsistency: Use 磁盘 instead of 磁碟.
Line 16 uses 磁碟 (Traditional Chinese/Taiwan usage), but this is a Simplified Chinese (zh-rCN) localization file. Line 15 correctly uses 磁盘 for "disk". For consistency and proper Simplified Chinese localization, change 磁碟 to 磁盘.
🔎 Proposed fix
- <string name="steam_install_space">下载大小: %1$s\n磁碟占用空间: %2$s\n可用空间: %3$s</string>
+ <string name="steam_install_space">下载大小: %1$s\n磁盘占用空间: %2$s\n可用空间: %3$s</string>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <string name="steam_install_space">下载大小: %1$s\n磁碟占用空间: %2$s\n可用空间: %3$s</string> | |
| <string name="steam_install_space">下载大小: %1$s\n磁盘占用空间: %2$s\n可用空间: %3$s</string> |
🤖 Prompt for AI Agents
In app/src/main/res/values-zh-rCN/strings.xml around line 16, the string uses
the Traditional Chinese term "磁碟" but this file is Simplified Chinese (zh-rCN);
replace "磁碟" with the Simplified Chinese term "磁盘" so the entry becomes
consistent with other translations and the locale.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
app/src/main/java/app/gamenative/service/SteamService.kt (4)
905-908: Minor inefficiency: Unnecessary fetch before early return.When resuming an existing download, Line 906 fetches
currentDownloadInfowhich is passed to the second overload, but line 1090 returns early before using thedlcAppIdsparameter. Consider checking for existing job directly in this overload to avoid the unnecessary fetch.🔎 Suggested refactor
fun downloadApp(appId: Int): DownloadInfo? { + val existingJob = downloadJobs[appId] + if (existingJob != null) return existingJob + - val currentDownloadInfo = downloadJobs[appId] - return downloadApp(appId, currentDownloadInfo?.downloadingAppIds ?: emptyList()) + return downloadApp(appId, emptyList()) }
1090-1094: Minor inefficiency: Expensive call before early return check.Line 1094 calls
getDownloadableDlcAppsOf(appId)before the job-exists check at line 1090. If a download is already in progress, this database query is wasted. Consider moving line 1094 after line 1092.
1097-1103: Minor optimization opportunity: Combine filters.Lines 1098-1103 filter
mainDepotstwice and concatenate the results. This can be simplified into a single filter operation for better performance.🔎 Proposed refactor
- val mainDepots = getMainAppDepots(appId) - var mainAppDepots = mainDepots.filter { (_, depot) -> - depot.dlcAppId == INVALID_APP_ID - } + mainDepots.filter { (_, depot) -> - dlcAppIds.contains(depot.dlcAppId) && depot.manifests.isNotEmpty() - } + val mainDepots = getMainAppDepots(appId) + var mainAppDepots = mainDepots.filter { (_, depot) -> + depot.dlcAppId == INVALID_APP_ID || + (dlcAppIds.contains(depot.dlcAppId) && depot.manifests.isNotEmpty()) + }
1142-1144: Consider user notification when no depots available.When
selectedDepots.isEmpty(), the download silently returnsnullwithout notifying the user why the download didn't start. Consider adding a user-facing notification (toast/dialog) before returning so users understand what happened.🔎 Proposed addition
Timber.i("selectedDepots is empty? " + selectedDepots.isEmpty()) if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No downloadable content found for app $appId") return null }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
app/src/main/java/app/gamenative/service/SteamService.kt (5)
42-43: LGTM: Import additions support new functionality.The added imports for
MarkerUtils,Marker, andCopyOnWriteArrayListare appropriate for the DLC download tracking and completion marker features introduced in this PR.Also applies to: 147-147
430-432: LGTM: New public API functions follow established patterns.The new functions
getDownloadableDlcAppsOf,getInstalledApp, andgetInstalledDlcDepotsOfproperly abstract database access and follow the existing code patterns in this class.Also applies to: 438-440, 446-448
551-597: LGTM: Well-structured depot filtering logic.The extraction of common filtering logic into
filterForDownloadableDepotsis a good refactor that improves maintainability. The filtering criteria are comprehensive and correctly handle architecture preferences, language matching, and DLC ownership.
603-645: LGTM: Enhanced depot discovery supports indirect DLC patterns.The modification correctly handles the DLC depot pattern where depots are inside another DLC app (case 3 from PR description). Creating new
DepotInfoobjects with the properdlcAppIdensures correct tracking throughout the download process.
889-895: LGTM: Proper cascade deletion for indirect DLC apps.The addition of cascade deletion for indirect DLC apps ensures that all related app data (AppInfo, change numbers, file change lists) is properly cleaned up when the main app is deleted. This prevents orphaned data in the database.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (3)
app/src/main/java/app/gamenative/service/SteamService.kt (3)
1328-1381: Two concerns: blocking call and potential race condition.
Line 1338 uses
runBlockingwhich blocks the calling coroutine thread. A past review suggested converting to a suspend function withwithContext(Dispatchers.IO). Since this is called from within the download job coroutine, blocking could impact performance or cause issues depending on the dispatcher.Lines 1367-1370 have a potential race condition. If multiple DLC completions occur concurrently:
- Line 1367:
removeIfand line 1370:isEmpty()are not atomicCopyOnWriteArrayListdoesn't make these operations atomic together- Multiple threads could all see
!isEmpty()and skip adding completion markers, or multiple threads could all seeisEmpty()and add markers multiple timesRecommended fixes:
🔎 Proposed improvements
- Convert to suspend and use proper coroutine context:
-private fun completeAppDownload( +private suspend fun completeAppDownload( downloadInfo: DownloadInfo, downloadingAppId: Int, entitledDepotIds: List<Int>, selectedDlcAppIds: List<Int>, appDirPath: String, ) { Timber.i("Item $downloadingAppId download completed, saving database") // Update database - runBlocking { + withContext(Dispatchers.IO) { // ... rest of database operations }
- Synchronize the completion check:
- // Remove completed appId from downloadInfo.dlcAppIds - downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } - - // All downloading appIds are removed - if (downloadInfo.downloadingAppIds.isEmpty()) { + // Remove completed appId and check for overall completion atomically + val allComplete = synchronized(downloadInfo.downloadingAppIds) { + downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } + downloadInfo.downloadingAppIds.isEmpty() + } + + // All downloading appIds are removed + if (allComplete) {Note: These issues were flagged in previous reviews but remain unaddressed.
1288-1299: Sequential completions can leave download in inconsistent state.If an early
completeAppDownloadcall succeeds (updating DB, removing fromdownloadingAppIds) but a later call fails, the download will be partially completed:
- Some apps marked as downloaded in the database
downloadingAppIdspartially emptied- Completion markers never added (because list not fully empty)
- Job removed by catch block at line 1311
This could explain the reported issue where "games get stuck at 100%."
Recommended approach: Wrap all completion operations in a transaction-like structure where either all succeed or all are rolled back. Consider collecting all completion tasks, executing them, and only after all succeed should you update shared state and add markers.
Note: This issue was flagged in a previous review but remains unaddressed.
905-908: Potential duplicate main app ID in downloadingAppIds.At line 907,
currentDownloadInfo?.downloadingAppIdsalready includes the mainappId. This list is passed asdlcAppIdsto the overload at line 910, then at lines 1119-1120 it's copied todownloadingAppIds, and finally at line 1125 the mainappIdis added again, resulting in a duplicate.A past review flagged this (supposedly addressed in commit 887a031), but the issue remains. Consider either:
- Filter out
appIdfromdownloadingAppIdsbefore passing it:return downloadApp(appId, currentDownloadInfo?.downloadingAppIds?.filter { it != appId } ?: emptyList())- Store selected DLC IDs separately in
DownloadInfoto avoid ambiguity.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
490-512: Consider returning immutableList<Int>instead ofMutableList<Int>.The function constructs and returns a
MutableList<Int>, but callers don't appear to require mutability (line 1129 only reads from it). ReturningList<Int>would better express intent and prevent unintended modifications.🔎 Proposed change
-fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { +fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): List<Int> {
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (10)
app/src/main/java/app/gamenative/service/SteamService.kt (10)
42-43: LGTM - Imports are used correctly.The new imports for
MarkerUtils,Marker, andCopyOnWriteArrayListare all properly utilized throughout the DLC-aware download implementation.Also applies to: 147-147
430-432: LGTM - Consistent DAO wrapper pattern.The function follows the established pattern for synchronous DAO wrappers using
runBlocking(Dispatchers.IO), consistent with similar methods likegetAppInfoOf. Based on learnings, this approach is acceptable for maintaining API compatibility.
438-440: LGTM - Consistent DAO wrapper pattern.The function correctly wraps the suspend DAO method with
runBlocking(Dispatchers.IO), following the established pattern for synchronous API access.
442-448: LGTM - Safe null handling and consistent refactoring.Both methods correctly use safe navigation (
?.) to access fields on the nullableAppInforeturned bygetInstalledApp, maintaining consistency with the refactored approach.
551-582: Verify OS filtering logic at lines 561-563.The condition on line 562 appears unusual:
if (!(depot.osList.contains(OS.linux) || (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))))This double-negation simplifies to: "exclude if has linux OR (no linux AND no macos)", which would allow macOS-only depots. For Windows gaming on Android emulation, the intended logic is likely:
if (!(depot.osList.contains(OS.windows) || depot.osList.isEmpty()))Please confirm whether macOS depots should be included.
584-597: LGTM - Main app depot filtering is correct.The method correctly identifies depots belonging to the main app using the centralized
filterForDownloadableDepots, with proper 64-bit detection and DLC ownership checking.
603-645: LGTM - DLC depot aggregation logic is correct.The method properly extends the main app depots with depots from indirect DLC apps. The use of
nullforownedDlcwhen filtering indirect DLC depots (line 624) correctly skips the ownership check, as these DLCs are already validated bygetDownloadableDlcAppsOf.
889-896: LGTM - Cascade deletion for indirect DLCs is correct.The addition properly cleans up database entries for indirect DLC apps when the main app is deleted, ensuring data consistency. The operations are correctly wrapped in a transaction.
1228-1269: LGTM - Per-app/DLC listener creation is correct.The code properly creates separate listeners and
AppIteminstances for the main app and each DLC, with correct depot-to-index mappings for progress tracking. All items correctly use the main app's install directory.
1407-1407: LGTM - Correct use ofdownloadInfo.gameId.The change from
item.appIdtodownloadInfo.gameIdcorrectly identifies the main app's download job to remove on failure, since the job is keyed by the main app ID.
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
1286-1303: Major: Sequential completion calls can leave inconsistent state.Lines 1289-1300 call
completeAppDownloadsequentially for the main app and each DLC. A past review flagged that if an early call succeeds but a later call fails, the download will be in an inconsistent state:
- Some apps marked completed in DB with their IDs removed from
downloadingAppIds- Later apps not completed
- The
downloadingAppIds.isEmpty()check (line 1371) never succeeds- Completion markers never added, leaving the download "stuck at 100%"
The past review suggested wrapping all completions in a transaction-like structure, but the code remains unchanged.
This likely contributes to the reported stuck-at-100% issue.
🔎 Recommended approach
- // Complete app download - if (mainAppDepots.isNotEmpty()) { - val mainAppDepotIds = mainAppDepots.keys.sorted() - completeAppDownload(di, appId, mainAppDepotIds, mainAppDlcIds, appDirPath) - } - - // Complete dlc app download - calculatedDlcAppIds.forEach { dlcAppId -> - val dlcDepots = selectedDepots.filter { it.value.dlcAppId == dlcAppId } - val dlcDepotIds = dlcDepots.keys.sorted() - completeAppDownload(di, dlcAppId, dlcDepotIds, emptyList(), appDirPath) - } - - // Remove the job here - removeDownloadJob(appId) + // Complete all apps atomically to avoid partial completion + try { + // Complete app download + if (mainAppDepots.isNotEmpty()) { + val mainAppDepotIds = mainAppDepots.keys.sorted() + completeAppDownload(di, appId, mainAppDepotIds, mainAppDlcIds, appDirPath) + } + + // Complete dlc app download + calculatedDlcAppIds.forEach { dlcAppId -> + val dlcDepots = selectedDepots.filter { it.value.dlcAppId == dlcAppId } + val dlcDepotIds = dlcDepots.keys.sorted() + completeAppDownload(di, dlcAppId, dlcDepotIds, emptyList(), appDirPath) + } + + // Only remove the job after all completions succeed + removeDownloadJob(appId) + } catch (e: Exception) { + Timber.e(e, "Completion failed, download may be in inconsistent state") + // Don't remove the job - allow retry + throw e + }Additionally, consider making
completeAppDownloadupdate the database within a single transaction to ensure atomicity.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
1142-1144: Minor: Consider user notification when no depots available.A past review suggested adding a user notification when
selectedDepots.isEmpty()to inform users why the download didn't start. This would improve the user experience.🔎 Proposed improvement
Timber.i("selectedDepots is empty? " + selectedDepots.isEmpty()) if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No depots available to download for this game") return null }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (17)
app/src/main/java/app/gamenative/service/SteamService.kt (17)
42-43: LGTM!New imports are correctly added to support marker-based download completion tracking and thread-safe DLC app ID list management.
Also applies to: 147-147
430-432: LGTM!The function follows the established pattern for synchronous DAO queries and aligns with the learnings that
runBlockingis acceptable for maintaining synchronous APIs.
438-440: LGTM!Clean implementation following the established pattern. Provides necessary access to installed app metadata for DLC tracking.
442-448: LGTM!Both functions correctly migrated to use the new
AppInfo-based approach for tracking installed depots and DLC depots. Null-safety is properly handled.
490-512: LGTM!This function correctly identifies DLC pattern (2) from the PR description: DLC depots without valid dlcappid that should be treated as included in the main game. The logic appropriately filters for single-depot DLCs with no OS, no manifests, and presence in the hidden DLC list.
551-582: LGTM!Well-structured common filter that consolidates depot eligibility logic. The filtering criteria are clearly documented and handle manifest availability, OS compatibility, architecture preference (64-bit over 32-bit), DLC ownership, and language matching appropriately.
584-597: LGTM!Clean implementation that correctly filters the main app's depots using the common filter logic. Properly integrates owned DLC checking and architecture preferences.
603-645: LGTM!The function now correctly handles both main app depots and DLC app depots (pattern 3 from PR description). The custom DepotInfo objects properly set
dlcAppIdto the DLC app ID, enabling per-DLC tracking during downloads.
889-896: LGTM!Properly cleans up indirect DLC app data when deleting a game. This prevents orphaned database entries for DLC apps.
910-920: LGTM!Clean public API for initiating downloads with specific DLC selections. Correctly retrieves all downloadable depots before delegating to the main implementation.
1094-1140: LGTM on DLC depot filtering logic!The logic correctly separates main app depots from DLC app depots, filters already-downloaded depots, and handles the special case where DLC depots are in separate DLC apps (pattern 3). The handling of
mainAppDlcIdsfor pattern 2 DLCs is also correct.
1225-1226: LGTM on autoStartDownload = false!Setting
autoStartDownload = falsecorrectly prevents DepotDownloader from automatically starting when items are added, allowing the game manager to control when downloads begin. This aligns with the PR objectives and commit message.
1229-1270: LGTM on per-app listener and AppItem creation!The code correctly creates separate listeners and AppItems for the main app and each DLC app. The
depotIdToIndexmapping enables proper progress tracking for each app's depots. This is a clean implementation of the multi-app download orchestration.
1304-1320: LGTM on error handling!The catch block correctly persists progress, marks depots as failed, and removes the download job. The
invokeOnCompletionhandler also properly handles cancellation. This ensures cleanup happens regardless of how the job ends.
1368-1368: LGTM on removeIf predicate!The predicate
{ it == downloadingAppId }correctly removes only the completed app ID. Past review flagged an inverted condition (!=) that was supposedly fixed in commit d2b1e08, and the current code confirms the fix is in place.
1406-1418: LGTM on failure handling!The
onDownloadFailedhandler correctly removes the download job and shows a user-facing error message. UsingdownloadInfo.gameIdensures the correct job is removed.
1371-1381: No race condition concern:completeAppDownloadis called sequentially from a single coroutine.
completeAppDownloadis only invoked at lines 1292 and 1299, both sequentially within the same try-catch block afterdepotDownloader.getCompletion().await(). The main app download completion removes the appId, then the DLC downloads are processed in a forEach loop, each removing its respective dlcAppId. Since both calls execute sequentially in a single coroutine within the same download job, concurrent calls toremoveIfon the samedownloadingAppIdsinstance cannot occur, eliminating the race condition scenario described.
7d1ffc4 to
73c6391
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
Fix all issues with AI Agents 🤖
In @app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt:
- Around line 89-93: The call to SteamService.getInstalledApp(gameId) in
GameManagerDialog is performing a blocking DB query on the composition thread
and must be memoized to avoid repeated queries during recomposition; wrap the
call with remember(gameId) so the result is cached (e.g., replace the direct
call that sets installedApp with a remember(gameId) block) and keep
installedDlcIds deriving from that remembered value
(installedApp?.dlcDepots.orEmpty()) so recompositions do not re-run the blocking
fetch.
In @gradle/libs.versions.toml:
- Line 14: Update the javasteam entry to reflect the library migration and
version alignment: change or remove the outdated comment referencing
in.dragonbra and replace it with the new group/source (io.github.utkarshdalal)
or remove the URL entirely; confirm whether the version bump/downgrade to
"1.8.0-SNAPSHOT" in gradle/libs.versions.toml was intentional for the forked
javasteam artifact (previously "1.8.1-SNAPSHOT") and if not, restore the version
back to "1.8.1-SNAPSHOT"; finally, ensure consistency with app/build.gradle.kts
where the localBuild conditional currently references "1.8.1-SNAPSHOT"
jars—either update app/build.gradle.kts to use "1.8.0-SNAPSHOT" or change
libs.versions.toml to "1.8.1-SNAPSHOT" so both files reference the same snapshot
version.
- Around line 72-75: Verify that the new artifacts under group
io.github.utkarshdalal actually use the same Java package namespace; search the
codebase for imports like in.dragonbra.javasteam.* (e.g., in SteamService.kt and
LicenseSerializer.kt) and either update those imports to the new package names
provided by the fork or change the dependency back if the fork preserved the
original packages; also confirm the artifact coordinates exist on Maven Central
and are downloadable, and update the hardcoded
Class.forName("in.dragonbra.javasteam.util.log.LogManager") in SteamService.kt
to the correct fully-qualified name (or add a fallback lookup that tries both
the old and new FQCNs) to avoid ClassNotFoundExceptions; once confirmed, remove
the commented-out old dependency lines in libs.versions.toml.
♻️ Duplicate comments (6)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (2)
274-292: Missing accessibility: Add content description for hero image.The
CoilImagelacks acontentDescriptionfor screen readers. For decorative images, explicitly set it tonull.🔎 Suggested fix
CoilImage( modifier = Modifier.fillMaxSize(), imageModel = { displayInfo.heroImageUrl }, - imageOptions = ImageOptions(contentScale = ContentScale.Crop), + imageOptions = ImageOptions( + contentScale = ContentScale.Crop, + contentDescription = null // Decorative image + ), loading = { LoadingScreen() },
103-134: LaunchedEffect runs on both visibility changes and may crash on empty depots.Two issues:
- The effect runs when
visiblebecomesfalseas well, clearing state unnecessarily. Add an early return guard.- Line 131 uses
.first()which throws ifdownloadableDepotsis empty.🔎 Suggested fix
LaunchedEffect(visible) { + if (!visible) return@LaunchedEffect + scrollState.animateScrollTo(0) downloadableDepots.clear() allDownloadableApps.clear() // ... existing code ... // Add Base Game - allDownloadableApps.add(0, Pair(gameId, downloadableDepots.toSortedMap().values.first())) - selectedAppIds[gameId] = true - enabledAppIds[gameId] = false + val firstDepot = downloadableDepots.toSortedMap().values.firstOrNull() + if (firstDepot != null) { + allDownloadableApps.add(0, Pair(gameId, firstDepot)) + selectedAppIds[gameId] = true + enabledAppIds[gameId] = false + } }app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)
808-814: Potential NullPointerException onappInfo!!at line 811.This is a duplicate of a past review comment. The force unwrap
appInfo!!at line 811 can cause an NPE if app info is unavailable. Additionally, the condition.size <= 1on line 811 allows hidden DLCs that appear in exactly 1 depot, which may not align with the intent expressed in the comment "only add hidden dlc apps if it is not a DLC of the main app."🔎 Proposed fix
// only add hidden dlc apps if not found in appendedDlcIds hiddenDlcApps?.forEach { hiddenDlcApp -> - if (!appendedDlcIds.contains(hiddenDlcApp.id) && + if (appInfo != null && + !appendedDlcIds.contains(hiddenDlcApp.id) && // only add hidden dlc apps if it is not a DLC of the main app - appInfo!!.depots.filter { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }.size <= 1) { + appInfo.depots.none { (_, depot) -> depot.dlcAppId == hiddenDlcApp.id }) { appendLine("${hiddenDlcApp.id}=dlc${hiddenDlcApp.id}") } }app/src/main/java/app/gamenative/service/SteamService.kt (3)
1118-1126: Critical: Duplicate main app ID indownloadingAppIds.This is a duplicate of past review comments. Lines 1118-1126 construct
downloadingAppIdsby:
- Adding all
dlcAppIds(line 1119)- Adding
appIdif main app has depots (line 1125)When resuming (line 907),
dlcAppIdscomes fromcurrentDownloadInfo?.downloadingAppIdswhich already contains the main app ID, causing duplication. This breaks completion tracking at line 1371 where the code checksdownloadingAppIds.isEmpty().🔎 Proposed fix
val downloadingAppIds = CopyOnWriteArrayList<Int>() - downloadingAppIds.addAll(dlcAppIds) + // Exclude the main appId from dlcAppIds to prevent duplicates when resuming + downloadingAppIds.addAll(dlcAppIds.filter { it != appId }) val calculatedDlcAppIds = dlcAppIds.toMutableList() // Add main app ID if there are main app depots if (mainAppDepots.isNotEmpty()) { downloadingAppIds.add(appId) }
1289-1303: Potential inconsistent state from sequential completions.This is a duplicate of past review comments. Lines 1289-1299 call
completeAppDownloadsequentially for the main app and each DLC. If an early call succeeds but a later one fails:
- Some apps are marked completed in the database
downloadingAppIdsis partially emptied- Completion markers are never added (because the list is not empty)
- The job is removed by the catch block at line 1311
This could explain reported issues where "games get stuck at 100%."
Consider wrapping all completion calls in a transaction-like structure: collect all completion tasks first, execute them atomically, and only update shared state after all succeed. On any failure, log the error and perform rollback before rethrowing.
905-920: Duplicate app ID in downloadingAppIds when resuming downloads.When resuming a download via
downloadApp(appId), the overload passescurrentDownloadInfo?.downloadingAppIdsas thedlcAppIdsparameter (line 907). At line 1119,downloadingAppIds.addAll(dlcAppIds)adds all passed app IDs, then at line 1125, the mainappIdis added again ifmainAppDepots.isNotEmpty(). If the existingdownloadingAppIdsalready contains the main app ID, it will be duplicated in the tracking list. Add deduplication logic (e.g.,toSet()) or filter outappIdfromdlcAppIdsbefore passing it to prevent this duplication.
🧹 Nitpick comments (4)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (2)
65-71: Consider extracting shared data class.This
InstallSizeInfodata class is duplicated fromSteamAppScreen.kt(lines 58-64). Consider extracting it to a shared location (e.g., adatapackage) to avoid duplication.
417-427: Consider simplifying the filter logic for clarity.The nested filter on lines 420-422 is correct but dense. Consider extracting to a named variable for readability.
🔎 Suggested simplification
Button( enabled = installButtonEnabled(), onClick = { - onInstall(selectedAppIds - .filter { selectedId -> selectedId.key in enabledAppIds.filter { enabledId -> enabledId.value } } - .filter { selectedId -> selectedId.value }.keys.toList()) + val enabledKeys = enabledAppIds.filterValues { it }.keys + val newlySelectedDlcs = selectedAppIds + .filterKeys { it in enabledKeys } + .filterValues { it } + .keys.toList() + onInstall(newlySelectedDlcs) } ) {app/src/main/java/app/gamenative/service/SteamService.kt (2)
1142-1144: Consider user notification when no depots are available.The check correctly prevents downloads when
selectedDepots.isEmpty(). However, as noted in a past review, consider adding a user-facing notification (toast, dialog) before returning null so users understand why the download didn't start.Optional improvement
if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No downloadable content found for this app") return null }
1329-1382: Consider converting to suspend function instead of runBlocking.Past review comments noted that using
runBlockingat line 1339 within a coroutine context is suboptimal. This function is called from within download jobs (lines 1292, 1299) that already run in a coroutine context.However, the current implementation correctly:
- Handles nullable
appInfowithout force unwraps (lines 1340-1364)- Uses
removeIf { it == downloadingAppId }(line 1368) - past incorrect condition has been fixed- Properly checks for completion via
isEmpty()(line 1371)Recommended refactor
Convert
completeAppDownloadto asuspendfunction and replacerunBlockingwith proper suspend semantics:- private fun completeAppDownload( + private suspend fun completeAppDownload( downloadInfo: DownloadInfo, downloadingAppId: Int, entitledDepotIds: List<Int>, selectedDlcAppIds: List<Int>, appDirPath: String, ) { Timber.i("Item $downloadingAppId download completed, saving database") // Update database - runBlocking { + withContext(Dispatchers.IO) { val appInfo = getInstalledApp(downloadingAppId) // ... rest of the database operations }Then update callers at lines 1292 and 1299 to invoke as a suspend function (they're already in coroutine contexts).
Based on learnings,
runBlockingis acceptable for public APIs maintaining synchronous contracts, but this is an internal function within a coroutine context where proper suspend semantics are preferred.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
app/build.gradle.ktsapp/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/db/dao/AppInfoDao.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.ktapp/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/res/values-da/strings.xmlapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values-pt-rBR/strings.xmlapp/src/main/res/values-zh-rCN/strings.xmlapp/src/main/res/values-zh-rTW/strings.xmlapp/src/main/res/values/strings.xmlgradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (7)
- app/src/main/res/values/strings.xml
- app/src/main/res/values-da/strings.xml
- app/src/main/res/values-pt-rBR/strings.xml
- app/build.gradle.kts
- app/src/main/res/values-zh-rCN/strings.xml
- app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt
- app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/db/dao/AppInfoDao.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (2)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (25)
app/src/main/res/values-zh-rTW/strings.xml (1)
16-16: LGTM! Localization addition is accurate and consistent.The Traditional Chinese translation is appropriate, format placeholders are correctly positioned, and the string provides a concise alternative to
steam_install_space_promptfor contexts where the introductory text isn't needed.app/src/main/res/values-fr/strings.xml (1)
16-16: LGTM!The French translation is accurate and natural. The string formatting with placeholders (
%1$s,%2$s,%3$s) and newline separators is correct. This provides a clean, reusable way to display installation space information separate from the prompt context.app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt (1)
23-24: LGTM!The new
ManageGameContentenum constant follows the existing naming and declaration pattern. The trailing comma addition is good Kotlin style for multi-line enums.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (6)
51-52: LGTM!Imports are correctly added for the new dialog component and its state class.
135-147: LGTM!The per-game dialog state management follows the established pattern used for
installDialogStatesanduninstallDialogAppIds, maintaining consistency throughout the companion object.
457-462: LGTM!The install flow for uninstalled games now routes through the
GameManagerDialog, enabling users to select DLCs before installation. This aligns with the PR objectives.
630-640: LGTM!The new menu option correctly opens the
GameManagerDialogfor installed games, allowing users to manage DLC content post-installation.
857-866: LGTM!The dialog state observation follows the same
snapshotFlowpattern used forinstallDialogState, maintaining consistency.
1210-1240: LGTM!The dialog integration correctly handles installation by:
- Hiding the dialog first
- Removing DRM markers if the app is already installed
- Capturing analytics
- Launching the download on an IO dispatcher
The use of
CoroutineScope(Dispatchers.IO).launchis consistent with other download operations in this file.app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (3)
136-248: LGTM!The helper functions properly handle null cases and edge conditions. The
installButtonEnabledlogic correctly accounts for already-installed DLCs.
362-400: LGTM!The DLC list correctly displays each item with its download/install size and manages the checkbox state appropriately. Already-installed items are disabled from selection.
436-465: LGTM!The preview function correctly uses fake data and wraps the dialog in the app theme for accurate preview rendering.
app/src/main/java/app/gamenative/data/DownloadInfo.kt (1)
12-13: LGTM! Thread-safe collection for per-DLC tracking.The addition of
gameIdanddownloadingAppIdsenables per-game and per-DLC download tracking. UsingCopyOnWriteArrayListis appropriate here sincedownloadingAppIdsis mutated during download completion (line 1368 in SteamService.kt) and potentially accessed from multiple threads.app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
24-24: LGTM! Method rename improves clarity.The rename from
getInstalledDepotstogetInstalledAppbetter reflects that the method returns the fullAppInfoobject, not just depot IDs. This aligns with the usage pattern in SteamService.kt where callers extract various fields from the returnedAppInfo.app/src/main/java/app/gamenative/utils/SteamUtils.kt (3)
736-742: LGTM! Clean depots.txt recreation ensures consistency.Deleting the existing
depots.txtand recreating it from the sorted list of installed depots ensures the file stays in sync with the actual installation state. This approach is safer than trying to merge/update an existing file.Based on learnings, the file is only created when
getInstalledDepotsOfreturns actual data (non-null).
785-806: LGTM! Enhanced DLC emission with proper tracking.The new DLC emission logic correctly:
- Uses sorted DLC depot IDs for deterministic output (line 794)
- Tracks appended IDs to prevent duplicates (lines 796, 803)
- Validates installation status before emitting DLC apps (line 801)
This ensures the
app.iniconfiguration accurately reflects installed content.
1036-1038: LGTM! Defensive error handling for external API call.Wrapping the
fetchDirect3DMajorcallback logic in a try-catch ensures that exceptions from the PCGamingWiki API don't crash the application. Returning -1 on error is a reasonable fallback.app/src/main/java/app/gamenative/service/SteamService.kt (8)
430-448: LGTM! New public APIs for DLC management.The new helper functions provide clear separation of concerns:
getDownloadableDlcAppsOffetches DLC apps from databasegetInstalledAppreturns full AppInfo (replacing oldgetInstalledDepots)getInstalledDepotsOfandgetInstalledDlcDepotsOfderive data fromgetInstalledAppBased on learnings, using
runBlockingto maintain synchronous APIs is acceptable when callers expect synchronous behavior.
490-512: LGTM! Edge case handling for DLCs without proper depot DLC IDs.This function identifies DLC apps that appear in main game depots but lack proper
dlcAppIdassociations (case 2 from the PR description: "DLC depots inside main game depots without a valid dlcappid"). The validation logic at lines 502-504 correctly filters for depots with no OS/manifests that are in the hidden DLC list.
551-597: LGTM! Consolidated depot filtering logic.The new
filterForDownloadableDepotsfunction centralizes filtering logic with appropriate checks:
- Manifest availability (lines 555-559)
- OS compatibility (lines 561-564)
- Architecture preference for 64-bit when available (lines 568-573)
- DLC ownership validation (lines 575-576)
- Language matching (lines 578-579)
getMainAppDepotscorrectly applies this filter to main app depots only, establishing a clear separation from the fullgetDownloadableDepotsthat also includes indirect DLC depots.
603-645: LGTM! Extended depot resolution for indirect DLCs.The enhanced
getDownloadableDepotsnow handles case 3 from the PR description (DLC depots placed inside another DLC app). The logic correctly:
- Fetches indirect DLC apps via
getDownloadableDlcAppsOf(line 619)- Filters their depots using the same criteria (lines 623-625)
- Reconstructs
DepotInfoobjects withdlcAppIdset to the DLC app ID (line 631)This ensures all DLC depots are discoverable regardless of their placement in Steam's data structure.
889-895: LGTM! Cascade cleanup prevents orphaned DLC data.When deleting a main app, the code now also cleans up database entries for indirect DLC apps. This prevents orphaned data and ensures database consistency. The cleanup includes AppInfo, change numbers, and file change lists for each DLC.
1150-1227: LGTM! Enhanced download setup with per-DLC tracking.The download setup correctly:
- Creates
DownloadInfowithgameIdanddownloadingAppIdsfor multi-app tracking (line 1150)- Sets depot weights based on manifest sizes for accurate progress (lines 1153-1159)
- Loads persisted progress for resume capability (lines 1166-1169)
- Configures
autoStartDownload=false(line 1226), aligning with the PR description's intent to let the game manager control when downloads begin
1229-1270: LGTM! Per-app listeners enable DLC-aware progress tracking.The new pattern creates separate
AppDownloadListenerinstances for the main app and each DLC app, with each listener tracking its own depot-to-index mapping. This enables:
- Independent progress tracking per app (lines 1238, 1259)
- Proper event handling for each component
- Clean separation between main app and DLC downloads
1387-1464: LGTM! Simplified listener constructor.The
AppDownloadListenerconstructor has been streamlined to only acceptdownloadInfoanddepotIdToIndex, removing redundant parameters. The listener correctly:
- Tracks cumulative uncompressed bytes per depot (lines 1393, 1434-1436)
- Calculates deltas for progress updates (lines 1434-1436, 1452-1454)
- Uses
downloadInfo.gameIdfor job removal on failure (line 1408)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
Show resolved
Hide resolved
gradle/libs.versions.toml
Outdated
| feature-delivery = "2.1.0" # https://mvnrepository.com/artifact/com.google.android.play/feature-delivery | ||
| hiltNavigationCompose = "1.2.0" # https://mvnrepository.com/artifact/androidx.hilt/hilt-navigation-compose | ||
| javasteam = "1.8.1-SNAPSHOT" # https://mvnrepository.com/artifact/in.dragonbra/javasteam | ||
| javasteam = "1.8.0-SNAPSHOT" # https://mvnrepository.com/artifact/in.dragonbra/javasteam |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the current state of gradle/libs.versions.toml
head -n 80 gradle/libs.versions.tomlRepository: utkarshdalal/GameNative
Length of output: 7694
🏁 Script executed:
# Check for all javasteam version references in the codebase
rg "javasteam.*1\.8\." --type toml --type gradle --type kts -iRepository: utkarshdalal/GameNative
Length of output: 94
🏁 Script executed:
# Search for app/build.gradle.kts and check its contents
fd -e "build.gradle.kts" | head -20Repository: utkarshdalal/GameNative
Length of output: 49
🏁 Script executed:
# Search for javasteam references in gradle and kts files
rg "javasteam" --type gradle --type toml -A 2 -B 2Repository: utkarshdalal/GameNative
Length of output: 1956
🏁 Script executed:
# Find and read app/build.gradle.kts
fd -type f -name "build.gradle.kts" | xargs grep -l "javasteam\|localBuild" | head -5Repository: utkarshdalal/GameNative
Length of output: 237
🏁 Script executed:
# Search for any version references to 1.8.1
rg "1\.8\.1" --type gradle --type toml --type ktsRepository: utkarshdalal/GameNative
Length of output: 94
🏁 Script executed:
# Find build.gradle.kts files properly
find . -name "build.gradle.kts" -type fRepository: utkarshdalal/GameNative
Length of output: 136
🏁 Script executed:
# Search for 1.8.1 references without incorrect --type
rg "1\.8\.1"Repository: utkarshdalal/GameNative
Length of output: 328
🏁 Script executed:
# Search for localBuild references
rg "localBuild" -A 3 -B 3Repository: utkarshdalal/GameNative
Length of output: 680
🏁 Script executed:
# Check if the new Maven coordinates are accessible
curl -s -I "https://repo1.maven.org/maven2/io/github/utkarshdalal/javasteam/" 2>&1 | head -5Repository: utkarshdalal/GameNative
Length of output: 190
🏁 Script executed:
# Check for any git history or comments that might explain the version change
rg "utkarshdalal\|javasteam" --type toml -B 3 -A 1 | head -40Repository: utkarshdalal/GameNative
Length of output: 49
Update the javasteam version declaration and comment to reflect the library migration.
Three issues need to be addressed:
-
Outdated comment URL: The comment on line 14 still references the old group
in.dragonbra(https://mvnrepository.com/artifact/in.dragonbra/javasteam), but the library now usesio.github.utkarshdalal. Update the comment to reflect the new source or remove it if no longer applicable. -
Version downgrade to 1.8.0-SNAPSHOT: The version was downgraded from
1.8.1-SNAPSHOT(currently referenced inapp/build.gradle.ktslocalBuild) to1.8.0-SNAPSHOT. Confirm this downgrade is intentional for the forked library. -
Version discrepancy with app/build.gradle.kts: The
localBuildconditional inapp/build.gradle.ktsreferences1.8.1-SNAPSHOTjars, whilegradle/libs.versions.tomlnow declares1.8.0-SNAPSHOT. WhenlocalBuild = false(default), the new version will be used instead. Ensure this transition is intentional and updateapp/build.gradle.ktsif the local build version should also change.
🤖 Prompt for AI Agents
In @gradle/libs.versions.toml around line 14, Update the javasteam entry to
reflect the library migration and version alignment: change or remove the
outdated comment referencing in.dragonbra and replace it with the new
group/source (io.github.utkarshdalal) or remove the URL entirely; confirm
whether the version bump/downgrade to "1.8.0-SNAPSHOT" in
gradle/libs.versions.toml was intentional for the forked javasteam artifact
(previously "1.8.1-SNAPSHOT") and if not, restore the version back to
"1.8.1-SNAPSHOT"; finally, ensure consistency with app/build.gradle.kts where
the localBuild conditional currently references "1.8.1-SNAPSHOT" jars—either
update app/build.gradle.kts to use "1.8.0-SNAPSHOT" or change libs.versions.toml
to "1.8.1-SNAPSHOT" so both files reference the same snapshot version.
gradle/libs.versions.toml
Outdated
| #javasteam = { group = "in.dragonbra", name = "javasteam", version.ref = "javasteam" } | ||
| javasteam = { group = "io.github.utkarshdalal", name = "javasteam", version.ref = "javasteam" } | ||
| #javasteam-depotdownloader = { group = "in.dragonbra", name = "javasteam-depotdownloader", version.ref = "javasteam" } | ||
| javasteam-depotdownloader = { group = "io.github.utkarshdalal", name = "javasteam-depotdownloader", version.ref = "javasteam" } |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
io.github.utkarshdalal javasteam maven repository
💡 Result:
Do you want the Maven coordinates (groupId/artifactId/version) for io.github.utkarshdalal:javasteam, or instructions to add the GitHub/Maven repository to your project?
🏁 Script executed:
# Search for javasteam usage in the codebase
rg "javasteam" --type gradle --type kotlin --type java -A 2 -B 2Repository: utkarshdalal/GameNative
Length of output: 22367
🏁 Script executed:
# Check if there are any other references to the old group
rg "in.dragonbra" --type gradle --type kotlin --type javaRepository: utkarshdalal/GameNative
Length of output: 487
🏁 Script executed:
# Look at the complete gradle/libs.versions.toml file to understand context
cat -n gradle/libs.versions.toml | head -100Repository: utkarshdalal/GameNative
Length of output: 9963
Update imports and verify package namespace compatibility.
The gradle dependency group has been changed from in.dragonbra to io.github.utkarshdalal for both javasteam and javasteam-depotdownloader, but the codebase still imports from the in.dragonbra.javasteam.* package namespace (throughout SteamService.kt, LicenseSerializer.kt, and other files). This works only if the forked library preserved the original package names.
Verify that:
- The
io.github.utkarshdalalfork maintains thein.dragonbra.javasteam.*package namespace or update all imports if it changed - The artifact is accessible on Maven Central (web search produced no confirmation)
- The hardcoded
Class.forName("in.dragonbra.javasteam.util.log.LogManager")reference in SteamService.kt is compatible with the forked version
Remove commented-out old dependencies once verified.
🤖 Prompt for AI Agents
In @gradle/libs.versions.toml around lines 72-75, Verify that the new artifacts
under group io.github.utkarshdalal actually use the same Java package namespace;
search the codebase for imports like in.dragonbra.javasteam.* (e.g., in
SteamService.kt and LicenseSerializer.kt) and either update those imports to the
new package names provided by the fork or change the dependency back if the fork
preserved the original packages; also confirm the artifact coordinates exist on
Maven Central and are downloadable, and update the hardcoded
Class.forName("in.dragonbra.javasteam.util.log.LogManager") in SteamService.kt
to the correct fully-qualified name (or add a fallback lookup that tries both
the old and new FQCNs) to avoid ClassNotFoundExceptions; once confirmed, remove
the commented-out old dependency lines in libs.versions.toml.
73c6391 to
2e49bba
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
1285-1296: Sequential completion calls risk partial state on failure.The code calls
completeAppDownloadsequentially for the main app and each DLC. If an early call succeeds but a later call fails:
- Some apps are marked as completed in the database
- Their IDs are removed from
downloadingAppIds- Completion markers are never added (line 1369 requires empty list)
- The job is removed by the catch block at line 1308
This could explain reports of games getting "stuck at 100%."
Consider collecting all completion operations and executing them atomically, or deferring marker addition until all database updates succeed. You might wrap the completion logic in a try-catch and rollback on failure, or use a database transaction if your DAO supports it.
1118-1126: Critical: Duplicate main app ID when resuming downloads.When resuming a download via line 907,
dlcAppIdscontainscurrentDownloadInfo?.downloadingAppIds, which already includes the mainappId. The code then:
- Adds all
dlcAppIdstodownloadingAppIdsat line 1119 (including the main app ID)- Adds
appIdagain at line 1125 if main depots existThis results in the main app ID appearing twice in
downloadingAppIds, breaking completion tracking at line 1367 where the list must be empty for completion to be recognized.This likely explains the reported issue where "games get stuck at 100%."
🔎 Recommended fix
val downloadingAppIds = CopyOnWriteArrayList<Int>() - downloadingAppIds.addAll(dlcAppIds) + // Exclude the main appId from dlcAppIds to prevent duplicates when resuming + downloadingAppIds.addAll(dlcAppIds.filter { it != appId }) val calculatedDlcAppIds = dlcAppIds.toMutableList() // Add main app ID if there are main app depots if (mainAppDepots.isNotEmpty()) { downloadingAppIds.add(appId) }
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/service/SteamService.kt (3)
490-512: Function logic is correct but could benefit from comments.The function
getMainAppDlcIdsWithoutProperDepotDlcIdsimplements a specific heuristic to identify DLC IDs that lack proper depot metadata. While the logic appears correct, the nested filtering and specific conditions (checkMap.size == 1, checking forOS.none, empty manifests, and hidden DLC list) would benefit from inline comments explaining the business rules.🔎 Suggested improvement: Add inline documentation
fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { val mainAppDlcIds = mutableListOf<Int>() val hiddenDlcAppIds = getHiddenDlcAppsOf(appId).orEmpty().map { it.id } val appInfo = getAppInfoOf(appId) if (appInfo != null) { - // for each of the dlcAppId found in main depots, filter the count = 1, add that dlcAppId to dlcAppIds + // Identify DLC IDs that appear in exactly one depot with no proper metadata. + // These represent DLCs without valid depot structure (case 2 from PR description). val checkingAppDlcIds = appInfo.depots.filter { it.value.dlcAppId != INVALID_APP_ID }.map { it.value.dlcAppId }.distinct() checkingAppDlcIds.forEach { checkingDlcId -> val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } if (checkMap.size == 1) { val depotInfo = checkMap[checkMap.keys.first()]!! + // DLC depot must have no OS, no manifests, and be in hidden DLC list if (depotInfo.osList.contains(OS.none) && depotInfo.manifests.isEmpty() && hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { mainAppDlcIds.add(checkingDlcId) } } } } return mainAppDlcIds }
1142-1144: Consider user notification for empty depot list.While the check correctly prevents attempting downloads when no depots are available, users may be confused why the download didn't start. Consider adding a user-facing notification before returning null.
🔎 Suggested improvement
Timber.i("selectedDepots is empty? " + selectedDepots.isEmpty()) if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No depots available for app $appId") return null }
1325-1378: Consider making completeAppDownload a suspend function.The function uses
runBlockingat line 1335 to perform database operations. While learnings indicaterunBlockingis acceptable for public APIs maintaining synchronous contracts, this is a private function called from within a coroutine context (the download job).Using
runBlockinghere:
- Blocks the coroutine thread unnecessarily
- Could cause deadlocks if the dispatcher thread pool is exhausted
- Is inconsistent with Kotlin coroutine best practices
Based on learnings, note that
runBlockingis acceptable for public APIs but not for internal functions within coroutine contexts.🔎 Recommended refactor
- private fun completeAppDownload( + private suspend fun completeAppDownload( downloadInfo: DownloadInfo, downloadingAppId: Int, entitledDepotIds: List<Int>, selectedDlcAppIds: List<Int>, appDirPath: String, ) { Timber.i("Item $downloadingAppId download completed, saving database") // Update database - runBlocking { + withContext(Dispatchers.IO) { val appInfo = getInstalledApp(downloadingAppId) // Update Saved AppInfo if (appInfo != null) { val updatedDownloadedDepots = (appInfo.downloadedDepots + entitledDepotIds).distinct() val updatedDlcDepots = (appInfo.dlcDepots + selectedDlcAppIds).distinct() instance?.appInfoDao?.update( AppInfo( downloadingAppId, isDownloaded = true, downloadedDepots = updatedDownloadedDepots.sorted(), dlcDepots = updatedDlcDepots.sorted(), ), ) } else { instance?.appInfoDao?.insert( AppInfo( downloadingAppId, isDownloaded = true, downloadedDepots = entitledDepotIds.sorted(), dlcDepots = selectedDlcAppIds.sorted(), ), ) } }Then update the callers at lines 1288 and 1295 (which are already in a coroutine context) to call this as a suspend function.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/service/SteamService.ktgradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (1)
- gradle/libs.versions.toml
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
app/src/main/java/app/gamenative/service/SteamService.kt (5)
42-43: LGTM: Appropriate imports for DLC tracking.The new imports support the DLC-aware download orchestration:
MarkerUtilsandMarkerfor completion signaling, andCopyOnWriteArrayListfor thread-safe tracking of downloading app IDs.Also applies to: 147-147
551-582: LGTM: Well-structured depot filtering logic.The
filterForDownloadableDepotsfunction implements comprehensive filtering with proper checks for manifests, OS compatibility, architecture preferences (64-bit priority), DLC ownership, and language matching. The logic correctly handles the case where 32-bit depots are only allowed when no 64-bit depot exists.
889-895: LGTM: Proper cascade deletion for DLC apps.The addition correctly cascades deletion to indirect DLC apps, ensuring related AppInfo, change numbers, and file change lists are cleaned up within the same transaction.
1228-1269: LGTM: Well-structured per-DLC download orchestration.The code correctly creates separate
AppItemandAppDownloadListenerinstances for the main app and each DLC, with proper depot ID to index mapping. The use of sorted depot IDs ensures consistent progress tracking.
1383-1460: LGTM: Simplified and cleaner listener implementation.The refactored
AppDownloadListenerhas a cleaner constructor signature and properly delegates completion handling to the separatecompleteAppDownloadfunction. Progress tracking logic correctly handles cumulative bytes and deltas.
There was a problem hiding this comment.
Actionable comments posted: 1
Fix all issues with AI Agents 🤖
In @gradle/libs.versions.toml:
- Around line 72-73: The Maven group change for the javasteam artifacts may not
match the package namespace used across the codebase (imports like
in.dragonbra.javasteam.* and the reflective
Class.forName("in.dragonbra.javasteam.util.log.LogManager") in SteamService.kt),
so confirm with the joshuatam/JavaSteam maintainers that their published
artifact preserves the original in.dragonbra.javasteam package; if it does,
update gradle/libs.versions.toml to use group = "in.dragonbra" for javasteam and
javasteam-depotdownloader, otherwise update all imports and the Class.forName
string to the new package namespace provided by the fork, and finally reconcile
the version mismatch by making the version in gradle/libs.versions.toml and
app/build.gradle.kts identical (choose either 1.8.1-SNAPSHOT or 1.8.0-2-SNAPSHOT
and apply consistently).
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
gradle/libs.versions.toml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
| javasteam = { group = "io.github.joshuatam", name = "javasteam", version.ref = "javasteam" } | ||
| javasteam-depotdownloader = { group = "io.github.joshuatam", name = "javasteam-depotdownloader", version.ref = "javasteam" } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the context in gradle/libs.versions.toml
head -80 gradle/libs.versions.toml | tail -15Repository: utkarshdalal/GameNative
Length of output: 1690
🏁 Script executed:
# Search for in.dragonbra.javasteam imports in the codebase
rg "import.*in\.dragonbra\.javasteam" --type kotlin --type java -nRepository: utkarshdalal/GameNative
Length of output: 49
🏁 Script executed:
# Search for ClassForName references related to javasteam
rg "Class\.forName.*javasteam" --type kotlin --type java -nRepository: utkarshdalal/GameNative
Length of output: 212
🏁 Script executed:
# Check settings.gradle.kts for repository configuration
cat settings.gradle.kts | head -50Repository: utkarshdalal/GameNative
Length of output: 722
🏁 Script executed:
# Check build.gradle.kts files for javasteam version references
rg "javasteam" --type gradle --type kts -B2 -A2Repository: utkarshdalal/GameNative
Length of output: 94
🏁 Script executed:
# Search for ALL references to in.dragonbra (not just imports)
rg "in\.dragonbra" --type kotlin --type java -nRepository: utkarshdalal/GameNative
Length of output: 212
🏁 Script executed:
# Check if there are other references to javasteam
rg "javasteam" --type kotlin --type java -C2Repository: utkarshdalal/GameNative
Length of output: 22367
🏁 Script executed:
# Look at the SteamService.kt file around the ClassForName reference
sed -n '2115,2130p' app/src/main/java/app/gamenative/service/SteamService.ktRepository: utkarshdalal/GameNative
Length of output: 630
🏁 Script executed:
# Check app/build.gradle.kts for dependencies
cat app/build.gradle.kts | grep -A2 -B2 "javasteam"Repository: utkarshdalal/GameNative
Length of output: 705
🏁 Script executed:
# Try to verify Maven artifact exists in Sonatype snapshots
curl -s "https://central.sonatype.com/api/v1/search?q=io.github.joshuatam%20javasteam&limit=10" | head -100Repository: utkarshdalal/GameNative
Length of output: 10215
Verify package namespace compatibility before merging—forked library must preserve in.dragonbra.javasteam.* packages.
The Maven group has been changed from in.dragonbra to io.github.joshuatam for both javasteam libraries. However, the codebase extensively depends on in.dragonbra.javasteam.* package imports:
- Direct imports across 20+ files: SteamService.kt (40+ imports), LicenseConverter.kt, FriendConverter.kt, SteamUtils.kt, LicenseSerializer.kt, and others
- Hardcoded ClassForName reference:
Class.forName("in.dragonbra.javasteam.util.log.LogManager")at SteamService.kt:2121
If the forked library changed the package namespace, this change will cause:
- Compilation failures on all imported classes
- Runtime ClassNotFoundException when SteamService initializes the logger
Confirm with the joshuatam/JavaSteam repository maintainers that the published artifact preserves the original in.dragonbra.javasteam.* package structure.
Additionally, there's a version mismatch: app/build.gradle.kts references version 1.8.1-SNAPSHOT locally, but gradle/libs.versions.toml specifies 1.8.0-2-SNAPSHOT.
🤖 Prompt for AI Agents
In @gradle/libs.versions.toml around lines 72-73, The Maven group change for the
javasteam artifacts may not match the package namespace used across the codebase
(imports like in.dragonbra.javasteam.* and the reflective
Class.forName("in.dragonbra.javasteam.util.log.LogManager") in SteamService.kt),
so confirm with the joshuatam/JavaSteam maintainers that their published
artifact preserves the original in.dragonbra.javasteam package; if it does,
update gradle/libs.versions.toml to use group = "in.dragonbra" for javasteam and
javasteam-depotdownloader, otherwise update all imports and the Class.forName
string to the new package namespace provided by the fork, and finally reconcile
the version mismatch by making the version in gradle/libs.versions.toml and
app/build.gradle.kts identical (choose either 1.8.1-SNAPSHOT or 1.8.0-2-SNAPSHOT
and apply consistently).
There was a problem hiding this comment.
@utkarshdalal, thanks for confirming! Good to know the forked library maintains backward compatibility with the original package namespace.
🧠 Learnings used
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI Agents
In @app/src/main/java/app/gamenative/service/SteamService.kt:
- Around line 905-908: downloadApp(appId) passes
currentDownloadInfo?.downloadingAppIds which may include the main appId, causing
duplicates when the overloaded downloadApp(appId, dlcAppIds) unconditionally
adds dlcAppIds and later adds appId again; fix by excluding the main appId from
the dlc list before calling the overload (e.g., compute dlcAppIds =
currentDownloadInfo?.downloadingAppIds?.filter { it != appId } ?: emptyList())
or dedupe inside downloadApp(appId, dlcAppIds) by converting to a Set before
merging; update the code paths using downloadJobs and the downloadApp overload
to ensure the main appId is never duplicated in the final depot list (see
functions downloadApp(Int) and downloadApp(Int, List<Int>) and the addition
logic around lines where dlcAppIds are merged).
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
1142-1144: Consider notifying user when no depots are available.The check at lines 1142-1144 correctly prevents attempting to download when no depots are available after filtering. However, returning
nullsilently may leave users wondering why the download didn't start.A past review suggested adding user notification, which would improve UX.
🔎 Proposed improvement
Timber.i("selectedDepots is empty? " + selectedDepots.isEmpty()) if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No depots available to download for app $appId") return null }
1329-1382: Consider converting to suspend function to avoid runBlocking.Line 1339 uses
runBlockingto execute database operations within a coroutine context. While a past learning indicatesrunBlockingis acceptable for maintaining synchronous public APIs, this is a private function called from within a coroutine (the download job). UsingrunBlockinghere:
- Blocks the calling coroutine thread
- Could cause deadlocks depending on the dispatcher
- Is inconsistent with Kotlin coroutine best practices for internal functions
🔎 Recommended refactor
- private fun completeAppDownload( + private suspend fun completeAppDownload( downloadInfo: DownloadInfo, downloadingAppId: Int, entitledDepotIds: List<Int>, selectedDlcAppIds: List<Int>, appDirPath: String, ) { Timber.i("Item $downloadingAppId download completed, saving database") // Update database - runBlocking { + withContext(Dispatchers.IO) { val appInfo = getInstalledApp(downloadingAppId) // Update Saved AppInfo if (appInfo != null) { // ... existing update logic } else { // ... existing insert logic } } // ... rest of function }Then update callers at lines 1292 and 1299 (they're already in a coroutine context).
Based on learnings, note that
runBlockingis acceptable for public APIs maintaining synchronous contracts, but not for internal functions within coroutine contexts.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/service/SteamService.ktgradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (1)
- gradle/libs.versions.toml
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
app/src/main/java/app/gamenative/service/SteamService.kt (3)
889-895: LGTM: Cascading deletion for indirect DLCs.The extended deletion logic correctly identifies and removes database entries for indirect DLC apps when the main app is deleted. This ensures cleanup is comprehensive and prevents orphaned data.
490-512: Add unit tests forgetMainAppDlcIdsWithoutProperDepotDlcIdsand document the DLC depot detection logic.This function contains complex multi-condition filtering (count == 1, OS.none check, empty manifests, hidden DLC validation) that detects DLC depots without proper dlcAppId metadata. While the logic appears correct and is actively used in download orchestration (line 1129) and UI code (GameManagerDialog), it lacks unit tests and inline documentation explaining why each condition is required.
Consider:
- Adding unit tests with Steam data examples showing when this detection is necessary
- Adding inline comments explaining the significance of each condition (e.g., why OS.none indicates metadata-only depots)
- Documenting the specific Steam data quirk this handles (currently only noted in a comment at line 1128)
1367-1371: No action needed—the code is thread-safe.Lines 1292 and 1299 call
completeAppDownloadsequentially within the same coroutine job. Each download job maintains its own independentDownloadInfoobject, so there is no concurrent modification ofdownloadingAppIds. TheremoveIfoperation at line 1368 executes sequentially on a single thread per job, andCopyOnWriteArrayListprovides thread-safe iteration semantics. TheAppDownloadListenerdoes not modifydownloadingAppIds, only reads fromdownloadInfo. No race condition exists.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI Agents
In @app/src/main/java/app/gamenative/service/SteamService.kt:
- Around line 905-926: The resume path can pass a list that already contains the
main app ID, which causes duplication when the overloaded downloadApp adds
dlcAppIds and then conditionally adds appId again; update the overloaded
downloadApp (the variant that takes dlcAppIds and manipulates downloadingAppIds
and mainAppDepots) to deduplicate incoming IDs before mutation—either filter
dlcAppIds to remove appId (e.g., dlcAppIds.filterNot { it == appId }) before
calling downloadingAppIds.addAll(dlcAppIds) or check
downloadingAppIds.contains(appId) before adding appId when
mainAppDepots.isNotEmpty(), so the main app ID is never added twice.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
446-448: Misleading function name: returns app IDs, not depot IDs.
getInstalledDlcDepotsOfreturns values fromAppInfo.dlcDepots, which actually stores DLC app IDs (see line 1351 whereselectedDlcAppIdsis stored). Consider renaming togetInstalledDlcAppIdsOffor clarity.🔎 Proposed rename
- fun getInstalledDlcDepotsOf(appId: Int): List<Int>? { - return getInstalledApp(appId)?.dlcDepots + fun getInstalledDlcAppIdsOf(appId: Int): List<Int>? { + return getInstalledApp(appId)?.dlcDepots }Also consider renaming the
dlcDepotsfield inAppInfotodlcAppIdsif feasible.
1160-1162: Consider adding user notification for empty depot case.When
selectedDepots.isEmpty(), the function silently returns null. As flagged previously, users won't understand why the download didn't start.🔎 Proposed improvement
Timber.i("selectedDepots is empty? " + selectedDepots.isEmpty()) if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No depots available to download for app $appId") return null }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/build.gradle.ktsapp/src/main/java/app/gamenative/service/SteamService.ktgradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (2)
- app/build.gradle.kts
- gradle/libs.versions.toml
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (10)
app/src/main/java/app/gamenative/service/SteamService.kt (10)
42-43: LGTM: Thread-safe imports added.The
CopyOnWriteArrayListimport addresses the previously flagged race condition ondownloadingAppIds, and the marker utilities support proper download state tracking.Also applies to: 147-147
490-512: LGTM: Edge case DLC detection logic.The function correctly handles the case where DLC depots lack proper
dlcAppIdvalues. The force unwrap at line 501 is safe due to thecheckMap.size == 1guard.
551-597: LGTM: Well-structured depot filtering logic.The
filterForDownloadableDepotsfunction centralizes depot eligibility checks including:
- Manifest availability
- OS/architecture filtering (preferring 64-bit)
- DLC ownership validation
- Language matching
This promotes code reuse and consistency.
603-645: LGTM: DLC depot aggregation.The function correctly aggregates depots from both the main app and indirect DLC apps, creating new
DepotInfoobjects with the properdlcAppIdassignment for tracking purposes.
889-896: LGTM: Cascade deletion for DLC data.Properly cleans up DLC-related database entries when the main app is deleted. The transaction wrapping ensures atomicity.
928-938: LGTM: Clean delegation to main download function.
1393-1470: LGTM: Simplified AppDownloadListener.The listener now takes only the essential
downloadInfoanddepotIdToIndexparameters. The progress tracking via cumulative byte deltas and the depot-to-index mapping are correctly implemented.
1234-1285: LGTM: Improved download orchestration.The explicit
autoStartDownload = falsewith manualfinishAdding()andstartDownloading()calls provides better control over the download lifecycle. The single listener with depot-to-index mapping is clean and efficient.
1136-1144: CopyOnWriteArrayList addresses race condition, but duplicate issue remains.Good use of
CopyOnWriteArrayListfor thread-safe modifications. However, as noted in the review of lines 905-926, the resume path can still introduce duplicate main app IDs. The fix should be applied at the resume callsite (line 908).
1114-1134: LGTM: Depot filtering correctly handles resume and DLC patterns.The filtering logic:
- Excludes already-downloaded depots for resume/verify (line 1130)
- Requires DLCs to be both user-selected AND valid indirect DLCs (line 1125)
- Separates main app depots from DLC app depots correctly
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI Agents
In @app/src/main/java/app/gamenative/data/DownloadInfo.kt:
- Line 13: The DownloadInfo data holder exposes a public mutable var
downloadingAppIds: CopyOnWriteArrayList<Int>, which allows external reassignment
and mutation; change this to a read-only exposure by making the property
immutable (val) and type it as List<Int> (or provide a private backing
CopyOnWriteArrayList<Int> with a public val downloadingAppIds: List<Int>) and
add explicit methods on DownloadInfo (e.g., addDownloadingAppId,
removeDownloadingAppId, replaceDownloadingAppIds) to control mutation so callers
cannot reassign or directly modify the internal list.
In @app/src/main/java/app/gamenative/db/PluviaDatabase.kt:
- Around line 40-42: Add a proper Room migration from 8 to 9 instead of relying
on destructive fallback: implement ROOM_MIGRATION_V8_to_V9 as a Migration(8,9)
that creates the new DownloadingAppInfo table (matching the DownloadingAppInfo
entity columns and types, e.g., appId PRIMARY KEY and dlcAppIds stored as TEXT
per your TypeConverter) and then register ROOM_MIGRATION_V8_to_V9 in the
database builder inside DatabaseModule (where other migrations are added) so
AppInfo and existing data are preserved during upgrade.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt (2)
13-13: Remove redundant type argument.The type argument in
emptyList<Int>()is unnecessary as Kotlin can infer it from the property type.🔎 Proposed fix
- val dlcAppIds: List<Int> = emptyList<Int>() + val dlcAppIds: List<Int> = emptyList()
14-14: Remove unnecessary empty braces.The
}{}syntax is unnecessary and should be removed.🔎 Proposed fix
- val dlcAppIds: List<Int> = emptyList<Int>() -){} + val dlcAppIds: List<Int> = emptyList() +)
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
app/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/data/DownloadingAppInfo.ktapp/src/main/java/app/gamenative/db/PluviaDatabase.ktapp/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.ktapp/src/main/java/app/gamenative/di/DatabaseModule.ktapp/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/di/DatabaseModule.ktapp/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.ktapp/src/main/java/app/gamenative/db/PluviaDatabase.ktapp/src/main/java/app/gamenative/data/DownloadingAppInfo.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (12)
app/src/main/java/app/gamenative/di/DatabaseModule.kt (1)
62-64: LGTM!The DI provider follows the established pattern and correctly exposes the new DAO.
app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt (1)
9-22: LGTM!The DAO implementation is correct with appropriate conflict strategy, nullable return type, and suspend functions for coroutine support.
app/src/main/java/app/gamenative/data/DownloadInfo.kt (2)
12-12: LGTM!The
gameIdproperty appropriately tracks which game is being downloaded.
36-52: LGTM!The refactored cancellation flow properly centralizes logic with parameterized messages, and the convenience methods (
cancel(),failedToDownload()) provide clear intent while the corecancel(message: String)handles persistence, cleanup, and job cancellation correctly.app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt (2)
12-13: TypeConverter forList<Int>is already properly implemented.A TypeConverter for
List<Int>exists inLicenseConverterand is registered inPluviaDatabase. The implementation uses JSON serialization viatoIntList()/fromIntList()methods, which correctly handles the conversion betweenList<Int>and SQLite-compatible strings.
7-14: The current implementation is correct.DownloadingAppInfois properly cleared during logout viadownloadingAppInfoDao.deleteAll()inclearDatabase(), whileAppInfo(which tracks installed games) is correctly preserved. Transient download state does not persist across logout sessions.app/src/main/java/app/gamenative/service/SteamService.kt (6)
42-43: LGTM: Imports and DAO injection support DLC download tracking.The new imports (MarkerUtils, Marker, DownloadingAppInfo, DownloadingAppInfoDao, CopyOnWriteArrayList) and DAO injection properly support the DLC download management functionality.
CopyOnWriteArrayListis an appropriate choice for thread-safe concurrent access todownloadingAppIds.Also applies to: 143-144, 149-149, 179-180
435-457: LGTM: Public API additions are well-structured.The new public API functions (
getDownloadingAppInfoOf,getDownloadableDlcAppsOf,getInstalledApp) properly userunBlockingto maintain synchronous contracts, which is acceptable for public APIs per learnings. The refactoring ofgetInstalledDepotsOfandgetInstalledDlcDepotsOfto usegetInstalledAppreduces duplication and improves maintainability.Based on learnings,
runBlockingis appropriate for public APIs maintaining synchronous contracts.
499-606: LGTM: Helper functions properly handle DLC filtering edge cases.The new helper functions are well-designed:
getMainAppDlcIdsWithoutProperDepotDlcIdscorrectly identifies DLCs that lack proper depot DLC IDs by checking for single-depot entries with no OS and no manifestsfilterForDownloadableDepotsconsolidates filtering logic (manifest, OS, architecture, DLC ownership, language) into a reusable function, reducing duplicationgetMainAppDepotsappropriately applies filtering to main app depots only
612-654: LGTM: Extended depot collection and cascading deletion support indirect DLCs.The updates properly handle indirect DLC apps:
getDownloadableDepotsnow includes depots from indirect DLC apps, correctly settingdlcAppIdfor each depotdeleteAppcascades deletion to indirect DLC apps, ensuring database consistency when removing a gameAlso applies to: 898-905
2029-2029: Verify downloading app info cleanup aligns with resume semantics.Line 2029 clears
downloadingAppInfoDaoduring database cleanup (called on logout per line 2020-2033). This is appropriate as downloading state is user-specific. However, verify this doesn't interfere with the AppInfo table, which per learnings should NOT be cleared during logout because games remain physically installed.The current implementation appears correct—
downloadingAppInfoDaois user-specific download state, whileappInfoDaotracks installation state. Based on learnings,appInfoDaois intentionally NOT cleared here (not present in the transaction), which is correct.Based on learnings, AppInfo should not be cleared during logout, and this code correctly omits it.
1110-1118: Convert to suspend function or use appropriate dispatcher.Using
runBlockingat lines 1111-1118 to insert downloading app info can cause performance issues or deadlocks, as this is called from various contexts including coroutine contexts. ThedownloadAppfunction is already executed within coroutine contexts (called from UI or other async code).🔎 Recommended approach
Option 1: Convert the entire
downloadAppfunction to suspend and use proper coroutine context:fun downloadApp( appId: Int, downloadableDepots: Map<Int, DepotInfo>, userSelectedDlcAppIds: List<Int>, branch: String, -): DownloadInfo? { +): suspend fun DownloadInfo? { // Save downloading app info - runBlocking { - instance?.downloadingAppInfoDao?.insert( - DownloadingAppInfo( - appId, - dlcAppIds = userSelectedDlcAppIds - ), - ) - } + instance?.downloadingAppInfoDao?.insert( + DownloadingAppInfo( + appId, + dlcAppIds = userSelectedDlcAppIds + ), + )Option 2: If maintaining synchronous API, use
withContext(Dispatchers.IO)instead ofrunBlocking(though this still blocks the caller).Then update callers to handle the suspend function properly.
Based on learnings,
runBlockingis acceptable for public APIs maintaining synchronous contracts, but this appears to be an internal implementation detail within a coroutine context.⛔ Skipped due to learnings
Learnt from: utkarshdalal Repo: utkarshdalal/GameNative PR: 159 File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6 Timestamp: 2025-09-19T17:07:27.941Z Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.Learnt from: utkarshdalal Repo: utkarshdalal/GameNative PR: 159 File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136 Timestamp: 2025-09-19T17:13:01.017Z Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
|
|
||
| data class DownloadInfo( | ||
| val gameId: Int, | ||
| var downloadingAppIds: CopyOnWriteArrayList<Int>, |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Encapsulate the mutable collection to prevent external modification.
Exposing downloadingAppIds as a public mutable var allows external code to reassign or modify the list, which could lead to inconsistent state or unexpected behavior. Consider exposing it as a read-only List with controlled mutation methods, or at minimum make it val to prevent reassignment.
🔎 Proposed fix to expose as read-only
- var downloadingAppIds: CopyOnWriteArrayList<Int>,
+ private val _downloadingAppIds: CopyOnWriteArrayList<Int>,
+) {
+ val downloadingAppIds: List<Int>
+ get() = _downloadingAppIds
+
+ fun addDownloadingAppId(appId: Int) {
+ _downloadingAppIds.add(appId)
+ }
+
+ fun removeDownloadingAppId(appId: Int) {
+ _downloadingAppIds.remove(appId)
+ }
+
+ fun clearDownloadingAppIds() {
+ _downloadingAppIds.clear()
+ }If callers need to replace the entire list, retain the current design but change var to val:
- var downloadingAppIds: CopyOnWriteArrayList<Int>,
+ val downloadingAppIds: CopyOnWriteArrayList<Int>,Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/data/DownloadInfo.kt at line 13, The
DownloadInfo data holder exposes a public mutable var downloadingAppIds:
CopyOnWriteArrayList<Int>, which allows external reassignment and mutation;
change this to a read-only exposure by making the property immutable (val) and
type it as List<Int> (or provide a private backing CopyOnWriteArrayList<Int>
with a public val downloadingAppIds: List<Int>) and add explicit methods on
DownloadInfo (e.g., addDownloadingAppId, removeDownloadingAppId,
replaceDownloadingAppIds) to control mutation so callers cannot reassign or
directly modify the internal list.
| DownloadingAppInfo::class | ||
| ], | ||
| version = 8, | ||
| version = 9, |
There was a problem hiding this comment.
Critical: Missing database migration from version 8 to 9.
Adding a new entity and bumping the schema version without providing a corresponding migration (ROOM_MIGRATION_V8_to_V9) will trigger fallbackToDestructiveMigration, wiping all existing data including installed game state tracked in AppInfo. Users upgrading from version 8 will lose their game library.
Based on learnings, AppInfo must persist across schema changes since games remain physically installed.
🔎 Proposed fix: Add migration V8 to V9
Create the migration file app/src/main/java/app/gamenative/db/migration/RoomMigrationV8toV9.kt:
package app.gamenative.db.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
val ROOM_MIGRATION_V8_to_V9 = object : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS downloading_app_info (
appId INTEGER PRIMARY KEY NOT NULL,
dlcAppIds TEXT NOT NULL
)
""".trimIndent()
)
}
}Then register it in DatabaseModule.kt:
+import app.gamenative.db.migration.ROOM_MIGRATION_V8_to_V9
return Room.databaseBuilder(context, PluviaDatabase::class.java, DATABASE_NAME)
.addMigrations(ROOM_MIGRATION_V7_to_V8)
+ .addMigrations(ROOM_MIGRATION_V8_to_V9)
.fallbackToDestructiveMigration(true)
.build()Note: Room stores List<Int> as TEXT using the registered TypeConverter. Verify the exact column type by checking your converters.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/db/PluviaDatabase.kt around lines 40 - 42,
Add a proper Room migration from 8 to 9 instead of relying on destructive
fallback: implement ROOM_MIGRATION_V8_to_V9 as a Migration(8,9) that creates the
new DownloadingAppInfo table (matching the DownloadingAppInfo entity columns and
types, e.g., appId PRIMARY KEY and dlcAppIds stored as TEXT per your
TypeConverter) and then register ROOM_MIGRATION_V8_to_V9 in the database builder
inside DatabaseModule (where other migrations are added) so AppInfo and existing
data are preserved during upgrade.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/service/SteamService.kt (3)
499-521: Logic appears correct but could be more readable.The method identifies DLCs without proper depot DLC IDs by filtering depots with specific criteria. While the logic is sound, consider these optional improvements:
- The method name is quite long (41 characters). Consider shortening to
getMainAppDlcIdsWithoutDepotInfoor similar.- Lines 511-513: The nested conditions could be extracted to a helper method or use early returns for clarity.
🔎 Optional refactor for readability
+private fun isHiddenDlcDepot(depotInfo: DepotInfo, hiddenDlcAppIds: List<Int>, dlcId: Int): Boolean { + return depotInfo.osList.contains(OS.none) && + depotInfo.manifests.isEmpty() && + hiddenDlcAppIds.isNotEmpty() && + hiddenDlcAppIds.contains(dlcId) +} + fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { val mainAppDlcIds = mutableListOf<Int>() val hiddenDlcAppIds = getHiddenDlcAppsOf(appId).orEmpty().map { it.id } val appInfo = getAppInfoOf(appId) if (appInfo != null) { - // for each of the dlcAppId found in main depots, filter the count = 1, add that dlcAppId to dlcAppIds val checkingAppDlcIds = appInfo.depots.filter { it.value.dlcAppId != INVALID_APP_ID }.map { it.value.dlcAppId }.distinct() checkingAppDlcIds.forEach { checkingDlcId -> val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } if (checkMap.size == 1) { val depotInfo = checkMap[checkMap.keys.first()]!! - if (depotInfo.osList.contains(OS.none) && - depotInfo.manifests.isEmpty() && - hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { + if (isHiddenDlcDepot(depotInfo, hiddenDlcAppIds, checkingDlcId)) { mainAppDlcIds.add(checkingDlcId) } } } } return mainAppDlcIds }
914-935: Parameter naming is confusing in resume path.Line 917 passes
currentDownloadInfo.downloadingAppIds(which contains both the main app ID and DLC IDs) to the overloadeddownloadAppfunction, where the parameter is nameddlcAppIds(line 1107, shown asuserSelectedDlcAppIds). This is semantically confusing because the parameter name suggests it contains only DLC IDs, but the resume path includes the main app ID.While the implementation appears safe (the overloaded function rebuilds the list from scratch based on actual depots at lines 1155-1182), the naming inconsistency makes the code harder to understand.
Suggestion: Consider renaming the parameter in the overloaded function to
selectedAppIdsorappIdsToDownloadto better reflect that it can include both the main app and DLC IDs, or filter out the main app ID in line 917.🔎 Proposed clarification
Option 1: Rename parameter in the overloaded function:
-fun downloadApp(appId: Int, dlcAppIds: List<Int>): DownloadInfo? { +fun downloadApp(appId: Int, selectedAppIds: List<Int>): DownloadInfo? {Option 2: Filter main app ID when resuming:
if (currentDownloadInfo != null) { - return downloadApp(appId, currentDownloadInfo.downloadingAppIds) + val dlcAppIds = currentDownloadInfo.downloadingAppIds.filter { it != appId } + return downloadApp(appId, dlcAppIds) }
628-651: Add clarifying comment explaining why indirect DLC depots skip ownership check.The code intentionally uses different filtering strategies: main app depots are filtered with the
ownedDlcmap (line 625), while indirect DLC depots are filtered withnull(line 633). This causes the ownership check at line 584 to be skipped for indirect DLCs.This design appears intentional—indirect DLC depots are later filtered by user selection (line 1144:
userSelectedDlcAppIds.contains(depot.dlcAppId)) rather than ownership. However, there's no code comment explaining why indirect DLCs bypass the ownership check. Adding a brief comment above line 633 would clarify that indirect DLCs are treated as optional user-selected content, not ownership-validated DLC.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (8)
app/src/main/java/app/gamenative/service/SteamService.kt (8)
435-441: LGTM!The new public API methods follow the established pattern in this file for synchronous data access. The use of
runBlockingis acceptable for public APIs per project learnings.
560-591: Excellent refactoring to eliminate code duplication.The common filtering logic is well-structured and handles multiple criteria correctly:
- Encrypted manifest filtering
- OS compatibility (Windows/none)
- Architecture preference (64-bit vs 32-bit)
- DLC ownership validation
- Language matching
898-905: LGTM - proper cascade deletion of indirect DLC data.The addition ensures that when the main game is deleted, any indirectly downloaded DLC apps (with their own depot lists) are also cleaned up from the database. This is consistent with the new multi-app download model.
1110-1118: Good addition for download resume support.Persisting the
DownloadingAppInfobefore starting the download ensures that the user's DLC selections can be restored if the download is interrupted or the app is killed. The use ofrunBlockingis acceptable here per project learnings.
1155-1186: Well-structured depot filtering and app ID construction.The logic correctly:
- Separates main app depots from DLC app depots
- Filters out already-downloaded content
- Builds
downloadingAppIdsbased on which apps actually have content to download (avoiding the duplicate main app ID issue from past reviews)- Uses
CopyOnWriteArrayListfor thread safetyThe check at line 1186 is necessary because after filtering for already-downloaded content (line 1149),
selectedDepotscould be empty even ifdownloadableDepotswasn't.
1268-1310: Good control over download lifecycle.Setting
autoStartDownload = false(line 1269) and manually callingstartDownloading()(line 1310) after adding allAppItems ensures the downloader has the complete download plan before starting. The parent job is correctly propagated for cancellation handling.
1444-1451: Good cleanup on download failure.The addition at lines 1447-1449 ensures that
DownloadingAppInfois removed from the database when a download fails, preventing stale resume data from accumulating.
2029-2029: LGTM - appropriate cleanup of downloading state.Adding
downloadingAppInfoDao.deleteAll()toclearDatabase()is correct. Unlike theAppInfotable (which tracks installed games and should persist across logins per project learnings),DownloadingAppInfotracks in-progress downloads which are user-session-specific and should be cleared on logout.
3133fde to
fb7e9c6
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI Agents
In @app/src/main/java/app/gamenative/service/SteamService.kt:
- Around line 1110-1118: When saving DownloadingAppInfo, filter out the main
appId from the DLC list so the main app is never persisted as its own DLC:
change the insert call that sets dlcAppIds to use a filtered collection (e.g.,
dlcAppIds = userSelectedDlcAppIds.filter { it != appId }) instead of
userSelectedDlcAppIds directly so DownloadingAppInfo (and the DAO insert via
instance?.downloadingAppInfoDao?.insert) only stores true DLC IDs; do not mutate
the original collection if it’s reused elsewhere.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
570-573: Consider simplifying OS filter logic for readability.The double-negative condition is logically correct but hard to parse. Consider refactoring:
// Keep Windows depots, or depots without explicit Linux/macOS tags val supportedOs = depot.osList.contains(OS.windows) || depot.osList.none { it == OS.linux || it == OS.macos } if (!supportedOs) return false
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🔇 Additional comments (8)
app/src/main/java/app/gamenative/service/SteamService.kt (8)
499-521: LGTM: Edge case handling for DLC metadata entries.This function correctly identifies DLC entries in main app depots that lack proper depot data (no OS, no manifests, only metadata). The multi-condition check appropriately filters for this specific pattern.
628-651: LGTM: Indirect DLC depot handling is well-structured.The function correctly combines main app depots with indirect DLC app depots. The DepotInfo override at line 640 properly sets
dlcAppIdto enable per-DLC-app tracking during downloads.
899-904: LGTM: Cascade delete for indirect DLC is appropriate.When deleting a main app, cleaning up indirectly downloaded DLC metadata prevents orphaned database entries. The transaction ensures atomicity.
1268-1310: LGTM: Two-phase download setup is well-structured.The pattern of creating the downloader with
autoStartDownload = false, adding all AppItems, callingfinishAdding(), thenstartDownloading()allows proper batch configuration before beginning the download. All items correctly share the same install directory.
1320-1339: Verify error recovery path for partial completions.If
completeAppDownloadthrows an exception partway through (e.g., after completing the main app but before completing DLC apps), the download state will be partially updated:
- Main app marked complete in DB
- DLC apps not marked complete
downloadingAppInforemains in DB (line 1337 not reached)- Job removed by catch block (line 1348)
On the next download attempt, the resume logic at lines 920-933 should handle this scenario. Please verify this recovery path works correctly with manual testing, particularly:
- Main app complete, DLC incomplete
- Some DLC complete, others incomplete
1404-1407: LGTM: CopyOnWriteArrayList provides adequate thread safety.The use of
CopyOnWriteArrayListat line 1155 ensures that concurrentremoveIfoperations at line 1404 are thread-safe. Even if multiple DLC completions race to checkisEmpty()at line 1407, the marker operations are idempotent (file-based), so duplicate attempts are harmless.
1444-1451: LGTM: Proper cleanup on download failure.The failure handler correctly cleans up the
downloadingAppInfofrom the database and removes the job. Users can retry the download with a clean state.
2029-2029: LGTM: Database cleanup includes new DAO.Adding
downloadingAppInfoDao.deleteAll()to the database cleanup ensures no orphaned download state remains after logout.
| // Save downloading app info | ||
| runBlocking { | ||
| instance?.downloadingAppInfoDao?.insert( | ||
| DownloadingAppInfo( | ||
| appId, | ||
| dlcAppIds = userSelectedDlcAppIds | ||
| ), | ||
| ) | ||
| } |
There was a problem hiding this comment.
Major: Main appId may be incorrectly stored in DLC list.
When resuming an existing download (called from line 917), userSelectedDlcAppIds may contain the main appId (from currentDownloadInfo.downloadingAppIds). This gets persisted to the database with the main app ID listed as a DLC app ID (line 1115).
Impact: If the app is killed and resumed again, line 932 loads this corrupted data, treating the main app as its own DLC. While the current logic filters this out during depot selection (lines 1158-1163), it's semantically incorrect and could cause confusion or bugs in future code changes.
🔎 Proposed fix
+ // Filter out main appId from DLC list before persisting
+ val dlcAppIdsOnly = userSelectedDlcAppIds.filter { it != appId }
+
// Save downloading app info
runBlocking {
instance?.downloadingAppInfoDao?.insert(
DownloadingAppInfo(
appId,
- dlcAppIds = userSelectedDlcAppIds
+ dlcAppIds = dlcAppIdsOnly
),
)
}🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/service/SteamService.kt around lines 1110 -
1118, When saving DownloadingAppInfo, filter out the main appId from the DLC
list so the main app is never persisted as its own DLC: change the insert call
that sets dlcAppIds to use a filtered collection (e.g., dlcAppIds =
userSelectedDlcAppIds.filter { it != appId }) instead of userSelectedDlcAppIds
directly so DownloadingAppInfo (and the DAO insert via
instance?.downloadingAppInfoDao?.insert) only stores true DLC IDs; do not mutate
the original collection if it’s reused elsewhere.
5d1c500 to
66b5c5d
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
app/src/main/java/app/gamenative/service/SteamService.kt (3)
504-526: Consider extracting depot validation logic for readability.The method correctly implements the DLC depot pattern detection described in the PR, but the nested logic is complex. Consider extracting the depot validation check (lines 516-518) into a named helper function to improve readability.
🔎 Optional refactor suggestion
+ private fun isDepotWithoutProperDlcId(depotInfo: DepotInfo, hiddenDlcAppIds: List<Int>, checkingDlcId: Int): Boolean { + return depotInfo.osList.contains(OS.none) && + depotInfo.manifests.isEmpty() && + hiddenDlcAppIds.isNotEmpty() && + hiddenDlcAppIds.contains(checkingDlcId) + } fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { val mainAppDlcIds = mutableListOf<Int>() val hiddenDlcAppIds = getHiddenDlcAppsOf(appId).orEmpty().map { it.id } val appInfo = getAppInfoOf(appId) if (appInfo != null) { val checkingAppDlcIds = appInfo.depots.filter { it.value.dlcAppId != INVALID_APP_ID }.map { it.value.dlcAppId }.distinct() checkingAppDlcIds.forEach { checkingDlcId -> val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } if (checkMap.size == 1) { val depotInfo = checkMap[checkMap.keys.first()]!! - if (depotInfo.osList.contains(OS.none) && - depotInfo.manifests.isEmpty() && - hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { + if (isDepotWithoutProperDlcId(depotInfo, hiddenDlcAppIds, checkingDlcId)) { mainAppDlcIds.add(checkingDlcId) } } } } return mainAppDlcIds }
565-596: Consider simplifying the OS check logic.The filter correctly implements depot selection criteria, but lines 576-577 use double negatives that could be confusing:
if (!(depot.osList.contains(OS.windows) || (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))))Consider refactoring for clarity:
🔎 Proposed simplification
// 2. Supported OS - if (!(depot.osList.contains(OS.windows) || - (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) - ) + val hasWindows = depot.osList.contains(OS.windows) + val hasLinuxOrMac = depot.osList.contains(OS.linux) || depot.osList.contains(OS.macos) + val isWindowsCompatible = hasWindows || !hasLinuxOrMac + if (!isWindowsCompatible) return false
893-918: Consider deleting DownloadingAppInfo for indirect DLC apps.Line 903 deletes
DownloadingAppInfofor the main app, but lines 905-910 don't deleteDownloadingAppInfofor indirect DLC apps. If DLC apps can have their own partial downloads, this could leave orphaned state.🔎 Proposed fix
appInfoDao.deleteApp(appId) changeNumbersDao.deleteByAppId(appId) fileChangeListsDao.deleteByAppId(appId) downloadingAppInfoDao.deleteApp(appId) val indirectDlcAppIds = getDownloadableDlcAppsOf(appId).orEmpty().map { it.id } indirectDlcAppIds.forEach { dlcAppId -> appInfoDao.deleteApp(dlcAppId) changeNumbersDao.deleteByAppId(dlcAppId) fileChangeListsDao.deleteByAppId(dlcAppId) + downloadingAppInfoDao.deleteApp(dlcAppId) }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (5)
app/src/main/java/app/gamenative/service/SteamService.kt (5)
42-43: LGTM!The new imports and DAO injection are appropriate for the DLC management functionality being added.
Also applies to: 143-144, 149-149, 179-180
289-297: LGTM!The improved partial download detection correctly checks
DownloadingAppInfofirst before falling back to marker-based detection. This addresses the commit message objective of improving download completion detection.
440-462: LGTM!The new getter methods provide clean APIs for accessing DLC-related data. The use of
runBlockingis acceptable here for maintaining synchronous public API contracts.Based on learnings,
runBlockingis appropriate for public APIs maintaining synchronous contracts.
617-659: LGTM!The extended
getDownloadableDepotscorrectly implements support for DLC pattern (3) where DLC depots are placed inside separate DLC app entries. The depot merging logic properly sets thedlcAppIdfield to maintain correct ownership tracking.
1199-1369: LGTM!The download orchestration correctly implements multi-DLC downloads with:
- Proper parent job tracking for cancellation (line 1274)
- Manual download start coordination (line 1275)
- Sequential completion with appropriate error handling
- Progress persistence on failure (line 1348)
The sequential completion approach is acceptable because partial failures are handled correctly - already-completed depots are filtered out on resume (line 1155).
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/SteamService.kt (2)
504-526: Consider adding inline documentation for complex DLC detection logic.The
getMainAppDlcIdsWithoutProperDepotDlcIdsfunction implements the pattern detection described in the PR (DLC depots inside main game without valid dlcappid). While the logic appears correct, it's complex and would benefit from inline comments explaining:
- Why
count == 1matters- The significance of
OS.nonewith empty manifests- How this relates to the "included in main game" DLC pattern
Example documentation
fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { val mainAppDlcIds = mutableListOf<Int>() val hiddenDlcAppIds = getHiddenDlcAppsOf(appId).orEmpty().map { it.id } val appInfo = getAppInfoOf(appId) if (appInfo != null) { + // Pattern 2: DLC depots inside main game without valid dlcappid + // These appear as single depot entries with OS.none and no manifests val checkingAppDlcIds = appInfo.depots.filter { it.value.dlcAppId != INVALID_APP_ID }.map { it.value.dlcAppId }.distinct() checkingAppDlcIds.forEach { checkingDlcId -> val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } + // Only single-depot DLCs qualify for this pattern if (checkMap.size == 1) { val depotInfo = checkMap[checkMap.keys.first()]!! if (depotInfo.osList.contains(OS.none) && depotInfo.manifests.isEmpty() && hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { mainAppDlcIds.add(checkingDlcId) } } } }
1182-1184: Consider notifying user when no depots are available.The empty depot check correctly prevents attempting a download when no content is available. However, users may be confused when nothing happens after initiating a download.
🔎 Suggested improvement
if (selectedDepots.isEmpty()) { + instance?.notificationHelper?.notify("No downloadable content available for app $appId") return null }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/service/SteamService.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (8)
app/src/main/java/app/gamenative/service/SteamService.kt (8)
42-43: LGTM! New imports and DAO injection are appropriate.The new imports support the DLC/multi-depot download feature. The
CopyOnWriteArrayListimport is particularly good for the thread-safedownloadingAppIdstracking, and theDownloadingAppInfoDaoinjection follows the existing dependency injection pattern.Also applies to: 143-144, 149-149, 179-180
290-293: LGTM! Improved partial download detection.The addition of the
downloadingAppInfoDaocheck correctly prioritizes database state, allowing the system to detect ongoing downloads even before markers are written. This is a good defensive improvement.
565-611: LGTM! Excellent refactoring of depot filtering logic.The new
filterForDownloadableDepotsfunction centralizes complex filtering logic that was previously duplicated. The comprehensive checks (64-bit preference, language filtering, DLC ownership, OS/arch validation) are well-structured and handle the nuances of Steam's depot system correctly.The
getMainAppDepotsfunction properly separates main app depots from the overall set, which is essential for the multi-depot download orchestration.
617-659: LGTM! Enhanced depot discovery includes indirect DLC apps.The modification to
getDownloadableDepotsnow correctly handles "Pattern 3" from the PR description—DLC depots placed inside another DLC app. The logic:
- Filters main app depots using the centralized filter
- Discovers indirect DLC apps via
getDownloadableDlcAppsOf- Includes their depots with proper
dlcAppIdattributionThis is a well-structured enhancement that maintains the existing API while expanding functionality.
903-910: LGTM! Proper cascade deletion of DLC apps.The enhanced
deleteAppfunction correctly:
- Cleans up
downloadingAppInfowhen deleting an app- Cascades deletion to indirect DLC apps (which install to the main app directory)
This prevents orphaned database entries and aligns with the learnings that AppInfo should be cleared when games are deleted.
1450-1459: LGTM! Proper cleanup on download failure.The enhanced
onDownloadFailedcallback correctly:
- Marks the download as failed
- Cleans up the persisted
downloadingAppInfo- Removes the download job
- Notifies the user
This prevents orphaned state and provides user feedback.
2037-2037: LGTM! Database cleanup includes downloading app info.Adding
downloadingAppInfoDao.deleteAll()toclearDatabaseensures downloading app information is properly cleared during logout, which is appropriate for user session cleanup.
920-943: This concern is unfounded—the implementation correctly handles mixed app IDs.When
downloadingAppIds(containing the main app ID) is passed asdlcAppIdsduring resume, the subsequent call to the three-parameterdownloadApp()reconstructsdownloadingAppIdsfrom filtered depot data at lines 1153–1180. The main app ID is added only whenmainAppDepots.isNotEmpty()(line 1165), and DLC app IDs are added only when matching depots exist indlcAppDepots(lines 1156–1160). Since main app depots are identified bydlcAppId == INVALID_APP_ID(line 1133) independently of the parameter value, no duplication occurs. The depot-based filtering correctly separates the main app from DLC apps regardless of what the input parameter contains.
There was a problem hiding this comment.
Actionable comments posted: 0
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/service/SteamService.kt (1)
1350-1359: MissingDownloadingAppInfocleanup in exception handler.If an exception occurs during the completion loop (lines 1331-1341), the catch block handles the error but doesn't clean up the
DownloadingAppInfofrom the database. This could leave orphaned entries that cause issues on app restart.🔎 Proposed fix
} catch (e: Exception) { Timber.e(e, "Download failed for app $appId") di.persistProgressSnapshot() // Mark all depots as failed selectedDepots.keys.sorted().forEachIndexed { idx, _ -> di.setWeight(idx, 0) di.setProgress(1f, idx) } + // Clean up downloading app info on failure + runBlocking { + instance?.downloadingAppInfoDao?.deleteApp(appId) + } removeDownloadJob(appId) }
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
504-526: Consider returning immutable List instead of MutableList.The function returns
MutableList<Int>but callers could unintentionally modify the returned list, leading to unexpected side effects. If mutation is required by callers, consider documenting this expectation; otherwise, returnList<Int>.🔎 Proposed fix
- fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { - val mainAppDlcIds = mutableListOf<Int>() + fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): List<Int> { + val mainAppDlcIds = mutableListOf<Int>() // ... existing logic ... - return mainAppDlcIds + return mainAppDlcIds.toList() }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (2)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (4)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
GameManagerDialog(73-434)app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (2)
getGameDisplayInfo(48-711)getGameDisplayInfo(53-57)app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (1)
getGameDisplayInfo(73-179)app/src/main/java/app/gamenative/service/SteamService.kt (1)
getAppDirPath(670-692)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
🔇 Additional comments (19)
app/src/main/java/app/gamenative/service/SteamService.kt (13)
179-181: LGTM!The new
DownloadingAppInfoDaoinjection follows the established pattern in this service and properly supports the new download state persistence functionality.
289-297: LGTM!The updated
hasPartialDownloadlogic correctly prioritizes checking the database forDownloadingAppInfobefore falling back to file marker checks, providing more reliable partial download detection.
440-462: LGTM!The new accessor functions follow the established patterns in this file. The
runBlockingusage is consistent with the codebase conventions (based on learnings,runBlockingis acceptable for maintaining synchronous APIs).
565-596: LGTM!Good refactoring to extract the common depot filtering logic. The function correctly handles all the filtering criteria including encrypted manifests, OS compatibility, architecture preferences (64-bit over 32-bit), DLC ownership, and language filtering.
598-611: LGTM!The function properly extracts main app depots using the shared filtering logic.
617-659: LGTM!The updated
getDownloadableDepotscorrectly handles both main app depots and indirect DLC depots, properly tracking DLC ownership via the modifieddlcAppIdfield.
893-918: LGTM!The deletion logic correctly cleans up the new
DownloadingAppInfodata and associated DLC app data within a transaction.
920-943: LGTM!The resume/verify logic correctly distinguishes between:
- Active downloads (from
downloadJobs)- Persisted downloads (from
downloadingAppInfo)- Verify operations (rebuilding DLC list from installed apps)
The
isUpdateOrVerifyflag properly differentiates initial downloads from update/verify operations.
945-955: LGTM!Clean wrapper function that properly forwards the
isUpdateOrVerifyflag.
1270-1280: LGTM!Setting
autoStartDownload = falseand callingstartDownloading()explicitly after queuing all items is the correct approach for coordinating multi-app downloads.
1375-1428: LGTM with note on runBlocking.The implementation correctly:
- Handles null
appInfogracefully (no force unwrap)- Uses
CopyOnWriteArrayList.removeIffor thread-safe removal- Adds completion markers only when all downloads finish
The
runBlockingat line 1385 is noted in past reviews as suboptimal for internal coroutine functions. Based on learnings, this is acceptable for maintaining API compatibility in this codebase, though converting to a suspend function would be cleaner.
1452-1471: LGTM!The failure handling correctly:
- Marks the download as failed
- Cleans up the
DownloadingAppInfofrom DB- Removes the download job
- Shows a user-friendly toast message
2039-2039: LGTM!Correctly clears
downloadingAppInfoDaoduring database cleanup. This is appropriate since active download state should not persist across logouts. Note: per learnings,AppInfo(which tracks installed games) is intentionally NOT cleared here since games remain physically installed.app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (6)
51-52: LGTM - Required imports for Game Manager featureThe imports for
GameManagerDialogandGameManagerDialogStateare necessary for the new game manager functionality.
135-147: LGTM - Consistent pattern for dialog state managementThe companion object methods for managing game manager dialog states follow the same pattern as the existing
installDialogStatesanduninstallDialogAppIds. The implementation is consistent and appropriate.
454-462: LGTM - Install flow now routes through Game ManagerThe change from showing an install prompt to showing the Game Manager dialog for non-installed games aligns with the PR objectives. This allows users to select DLCs before starting the installation.
630-640: LGTM - Useful entry point for managing game contentThe new
ManageGameContentmenu option provides a convenient way for users to manage DLCs and content for already-installed games.
857-866: LGTM - Consistent state synchronization patternThe
LaunchedEffectfor tracking game manager dialog state follows the same pattern as the install dialog state synchronization above it.
1220-1234: No changes needed — the current logic is correct.The
isUpdateOrVerify = falseis appropriate here. The GameManagerDialog restricts users to selecting only DLCs that are not already installed (disabled checkboxes for installed DLCs), so the selected app IDs passed toonInstallcontain only new content. SettingisUpdateOrVerify = falseensures only missing depots are downloaded, avoiding unnecessary re-downloads of existing content.The flag semantics are:
isUpdateOrVerify = false: Download only missing depots (for adding new content)isUpdateOrVerify = true: Verify/re-download all depots (for update checks)The proposed change to make it conditional would force re-verification of all depots when the game is already installed, which wastes bandwidth and contradicts the intent of content management.
Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/db/PluviaDatabase.kt:
- Line 42: PluviaDatabase's version was bumped to 10 without providing
migrations, so users upgrading from v8 will lose AppInfo data due to
fallbackToDestructiveMigration; either implement ROOM_MIGRATION_V8_to_V9 and
ROOM_MIGRATION_V9_to_V10 (or a single ROOM_MIGRATION_V8_to_V10) that migrate the
AppInfo table schema and register them with the Room builder in
DatabaseModule.kt (replace fallbackToDestructiveMigration or add
.addMigrations(...)), or revert the version change in PluviaDatabase (set
version back to 8) until proper migration objects are implemented and
registered.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/db/PluviaDatabase.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/db/PluviaDatabase.kt
🔇 Additional comments (1)
app/src/main/java/app/gamenative/db/PluviaDatabase.kt (1)
12-12: LGTM: Entity and DAO properly registered.The
DownloadingAppInfoentity andDownloadingAppInfoDaoare correctly imported, registered in the entities list, and exposed via an abstract accessor. The structure follows Room conventions.Also applies to: 26-26, 40-40, 68-69
| DownloadingAppInfo::class | ||
| ], | ||
| version = 8, | ||
| version = 10, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for Room migrations from v8 to v10
echo "=== Searching for migration objects ==="
rg -n "ROOM_MIGRATION.*8.*9|ROOM_MIGRATION.*9.*10|ROOM_MIGRATION.*8.*10" --type kotlin
echo -e "\n=== Searching for Migration class instantiations ==="
rg -n "Migration\s*\(\s*8\s*,\s*(9|10)\s*\)|Migration\s*\(\s*9\s*,\s*10\s*\)" --type kotlin
echo -e "\n=== Searching for all migration definitions in the repo ==="
rg -n "Migration\s*\(" --type kotlin -A 3 | head -100
echo -e "\n=== Checking for DatabaseModule ==="
fd -e kt -e java "DatabaseModule|PluviaDatabase" --type f
echo -e "\n=== Checking PluviaDatabase.kt for migration registration ==="
fd -e kt "PluviaDatabase.kt" --type f --exec head -150 {}Repository: utkarshdalal/GameNative
Length of output: 3473
Add migrations for version 8→10 or revert the version bump until migrations are provided.
No migrations exist for versions 8→9 or 9→10. With fallbackToDestructiveMigration enabled, users upgrading from version 8 will lose all data in the AppInfo table, including their installed game library.
Either add ROOM_MIGRATION_V8_to_V9 and ROOM_MIGRATION_V9_to_V10 (or a direct ROOM_MIGRATION_V8_to_V10), register them in the database builder in DatabaseModule.kt, or revert the version change until migrations are ready.
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/db/PluviaDatabase.kt at line 42,
PluviaDatabase's version was bumped to 10 without providing migrations, so users
upgrading from v8 will lose AppInfo data due to fallbackToDestructiveMigration;
either implement ROOM_MIGRATION_V8_to_V9 and ROOM_MIGRATION_V9_to_V10 (or a
single ROOM_MIGRATION_V8_to_V10) that migrate the AppInfo table schema and
register them with the Room builder in DatabaseModule.kt (replace
fallbackToDestructiveMigration or add .addMigrations(...)), or revert the
version change in PluviaDatabase (set version back to 8) until proper migration
objects are implemented and registered.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/db/PluviaDatabase.kt:
- Line 42: The DB version was bumped to 11 (version = 11) but migrations for
v8→v9, v9→v10 and v10→v11 are missing; either add those migration objects (or a
direct ROOM_MIGRATION_V8_to_V11) into the migration file and register them with
the Room builder, or import/cherry-pick the existing migrations from master and
call .addMigrations(...) in DatabaseModule (instead of relying on
fallbackToDestructiveMigration(true)) so upgrades from v8 do not wipe
AppInfo/installed game state.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/db/PluviaDatabase.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/db/PluviaDatabase.kt
🔇 Additional comments (3)
app/src/main/java/app/gamenative/db/PluviaDatabase.kt (3)
12-12: LGTM! Imports are correctly added.The imports for
DownloadingAppInfoentity andDownloadingAppInfoDaoare necessary for the new download state persistence feature.Also applies to: 26-26
40-40: LGTM! Entity registration is correct.The
DownloadingAppInfoentity is properly registered in the Room database.
68-69: LGTM! DAO accessor is correctly implemented.The abstract accessor for
DownloadingAppInfoDaofollows Room conventions and will be properly exposed through the DI layer.
| DownloadingAppInfo::class | ||
| ], | ||
| version = 8, | ||
| version = 11, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for Room migrations and database version progression
echo "=== Searching for migration definitions (various patterns) ==="
rg -n "Migration\s*\(" --type kotlin -A 3 | head -50
echo -e "\n=== Searching for version number patterns in migrations ==="
rg -n "from.*to\|From.*To" --type kotlin | grep -i migr
echo -e "\n=== Looking for DatabaseModule or database builder configuration ==="
fd -e kt -e java | xargs rg -l "addMigrations\|Migration" | head -10
echo -e "\n=== Checking PluviaDatabase.kt context for migrations ==="
fd "PluviaDatabase" --type f -x cat {}Repository: utkarshdalal/GameNative
Length of output: 3363
🏁 Script executed:
#!/bin/bash
# More targeted search for migration registration and database setup
echo "=== Search for .addMigrations in database files ==="
rg -n "addMigrations" --type kotlin -B 2 -A 5
echo -e "\n=== Search for migration files or classes ==="
fd -e kt | xargs rg -l "class.*Migration\|object.*Migration" 2>/dev/null | head -20
echo -e "\n=== Check entire PluviaDatabase.kt file ==="
fd "PluviaDatabase.kt" --type f -x wc -l {}Repository: utkarshdalal/GameNative
Length of output: 1016
Add missing migrations for versions 8→11 or coordinate with master branch.
The database version jumped from 8 to 11, but only the v7→v8 migration exists in the codebase (ROOM_MIGRATION_V7_to_V8 in app/src/main/java/app/gamenative/db/migration/RoomMigration.kt). With fallbackToDestructiveMigration(true) enabled in DatabaseModule.kt, the absence of migration paths from v8→v9, v9→v10, and v10→v11 will trigger destructive migration, wiping all tables including AppInfo (installed game state).
Since your commit message references "compatibility with master," these migrations likely exist in the master branch but are not present in this PR. You must either:
- Add migrations for v8→v9, v9→v10, and v10→v11 (or a direct v8→v11 migration) to this PR
- Ensure they are registered in
DatabaseModule.ktusing.addMigrations(...) - Merge/cherry-pick the migration definitions from master if they are already implemented there
Without these migrations, all users upgrading from database v8 will lose their installed game library.
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/db/PluviaDatabase.kt at line 42, The DB
version was bumped to 11 (version = 11) but migrations for v8→v9, v9→v10 and
v10→v11 are missing; either add those migration objects (or a direct
ROOM_MIGRATION_V8_to_V11) into the migration file and register them with the
Room builder, or import/cherry-pick the existing migrations from master and call
.addMigrations(...) in DatabaseModule (instead of relying on
fallbackToDestructiveMigration(true)) so upgrades from v8 do not wipe
AppInfo/installed game state.
Introduces a new dialog for managing game content, allowing users to select which DLCs to install alongside the base game. Updates the game download process to correctly handle DLCs and ensures correct depot filtering. Enhances DLC depot selection logic by considering hidden DLC apps. Updates dependency versions and refactors code for improved stability and clarity.
98a37a0 to
267e150
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (4)
app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt (1)
4-4: Unused import.
LibraryItemis imported but never used in this file. Consider removing it.🔎 Proposed fix
import androidx.compose.runtime.saveable.mapSaver -import app.gamenative.data.LibraryItemgradle/libs.versions.toml (1)
14-14: Consider updating the comment URL.The comment still references
https://mvnrepository.com/artifact/in.dragonbra/javasteambut the library group has been changed toio.github.joshuatam. Consider updating or removing this comment to avoid confusion.app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)
6-6: Unused import.
ActivityNavigatoris imported but never used in this file.🔎 Proposed fix
-import androidx.navigation.ActivityNavigatorapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
420-422: Optional: Simplify filter logic for clarity and performance.The nested filter at line 421 is inefficient—it creates an intermediate filtered Map just to check key membership. Consider simplifying to direct lookups:
onInstall(selectedAppIds - .filter { selectedId -> selectedId.key in enabledAppIds.filter { enabledId -> enabledId.value } } - .filter { selectedId -> selectedId.value }.keys.toList()) + .filter { enabledAppIds[it.key] == true } + .filter { it.value } + .keys.toList())This avoids constructing the intermediate filtered Map and is easier to read.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (21)
app/build.gradle.ktsapp/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/data/DownloadingAppInfo.ktapp/src/main/java/app/gamenative/db/PluviaDatabase.ktapp/src/main/java/app/gamenative/db/dao/AppInfoDao.ktapp/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/di/DatabaseModule.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.ktapp/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/utils/SteamUtils.ktapp/src/main/res/values-da/strings.xmlapp/src/main/res/values-fr/strings.xmlapp/src/main/res/values-pt-rBR/strings.xmlapp/src/main/res/values-zh-rCN/strings.xmlapp/src/main/res/values-zh-rTW/strings.xmlapp/src/main/res/values/strings.xmlgradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (11)
- app/src/main/res/values/strings.xml
- app/src/main/res/values-zh-rCN/strings.xml
- app/src/main/java/app/gamenative/di/DatabaseModule.kt
- app/src/main/res/values-zh-rTW/strings.xml
- app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt
- app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt
- app/src/main/res/values-pt-rBR/strings.xml
- app/src/main/res/values-da/strings.xml
- app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
- app/src/main/java/app/gamenative/data/DownloadInfo.kt
- app/src/main/java/app/gamenative/db/PluviaDatabase.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
gradle/libs.versions.tomlapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
gradle/libs.versions.tomlapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
gradle/libs.versions.tomlapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/db/dao/SteamAppDao.ktapp/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/utils/SteamUtils.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/utils/SteamUtils.kt
🧬 Code graph analysis (2)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (4)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
GameManagerDialog(73-434)app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (1)
getGameDisplayInfo(70-74)app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (1)
getGameDisplayInfo(73-179)app/src/main/java/app/gamenative/service/SteamService.kt (1)
getAppDirPath(670-692)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (12)
app/src/main/res/values-fr/strings.xml (1)
16-16: LGTM!The French translation is accurate and properly formatted. The string resource correctly uses placeholders and line breaks, making it suitable for displaying installation size information in the Game Manager dialog.
app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt (1)
6-22: LGTM!The
GameManagerDialogStatedata class and itsSaverare correctly implemented for Compose state persistence. ThemapSaverpattern is appropriate for saving/restoring dialog visibility across configuration changes.app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt (1)
47-63: LGTM with observation on JSON array parsing.The DLC queries correctly use string manipulation to parse the JSON array stored in
license.app_ids. The pattern'%,' || app.id || ',%'with the bracket-to-comma replacement handles edge cases (first/last elements) properly.Note: If performance becomes a concern with large datasets, consider migrating to Room's native JSON support (available in SQLite 3.38+/Room 2.5+) using
json_each().app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt (1)
9-22: LGTM!Clean Room DAO implementation following standard patterns. The
OnConflictStrategy.REPLACEis appropriate for managing download state that may need updates.Based on learnings, ensure
deleteAll()is only called during appropriate cleanup scenarios (e.g., user-initiated clear) and not during logout, as download/installation state should persist across sessions if games remain installed.app/build.gradle.kts (1)
228-232: LGTM!The local build jar paths are now aligned with the version specified in
libs.versions.toml(1.8.0-4-SNAPSHOT), resolving the previous version discrepancy.app/src/main/java/app/gamenative/utils/SteamUtils.kt (2)
736-742: LGTM!Good improvement - the depots.txt file is now properly refreshed by deleting the existing file before recreating it with the sorted depot list. This ensures the file always reflects the current installed state.
785-805: LGTM!The DLC emission logic correctly tracks appended IDs to prevent duplicates. The flow properly handles:
- Installed DLC depots (sorted)
- Downloadable DLC apps (only if installed and not already appended)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (5)
135-147: LGTM!The per-game dialog state management follows the established pattern used for
installDialogStatesanduninstallDialogAppIds. The companion object functions provide a clean API for showing/hiding the dialog from different call sites.
454-462: LGTM!The install flow now correctly routes to
showGameManagerDialogfor uninstalled games, enabling DLC selection before download. This aligns with the PR objective of letting users select which DLCs to install.
630-640: LGTM!The
ManageGameContentmenu option provides access to the Game Manager dialog for installed games, allowing users to modify their DLC selections post-installation.
857-866: LGTM!The
snapshotFlowpattern correctly observes changes to the shared dialog state and updates the local composable state accordingly. This ensures the dialog visibility stays synchronized with the companion object state.
1211-1240: LGTM!The
GameManagerDialogintegration is well-structured:
- Correctly passes display info via the callback
- Removes DRM markers when the app is already installed (ensuring clean re-download)
- Captures analytics event before starting download
- Uses
Dispatchers.IOfor the download operationOne minor note:
CoroutineScope(Dispatchers.IO).launchcreates an unstructured coroutine. Consider usingrememberCoroutineScope()(already available asscopeat line 869) for better lifecycle management, though the current approach is consistent with existing patterns in this file.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/service/gog/GOGService.kt:
- Line 302: The GOGService instantiates DownloadInfo with a hardcoded gameId = 0
and an unused downloadingAppIds list, which is Steam-specific and incorrect for
GOG's string-based IDs; update the model or usage so GOG doesn't misuse those
fields: either make DownloadInfo.gameId and downloadingAppIds optional/nullable
(or overload the constructor) and pass null/empty appropriately from GOGService
(use the GOG string id where meaningful), or create a Steam-specific
subclass/wrapper (e.g., SteamDownloadInfo) that adds gameId: Int and
downloadingAppIds while keeping DownloadInfo minimal for GOG; then update
GOGService to stop hardcoding 0 and to use the appropriate type/constructor
(referencing DownloadInfo, gameId, downloadingAppIds, and GOGService).
In @app/src/main/java/app/gamenative/service/SteamService.kt:
- Around line 504-526: In getMainAppDlcIdsWithoutProperDepotDlcIds, avoid the
forced unwrap checkMap[checkMap.keys.first()]!! which can NPE; instead retrieve
the sole depot safely (e.g., use checkMap.entries.firstOrNull()?.value or
checkMap.values.firstOrNull()) and guard with a null check or let, then run the
existing conditions and add the id only if depotInfo is non-null.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
1375-1428: Consider converting to suspend function.A past review noted that using
runBlocking(line 1385) within a coroutine context is suboptimal. Since this is a private internal function called from within a coroutine, consider making itsuspendand usingwithContext(Dispatchers.IO)for the DB operations instead ofrunBlocking.Based on learnings,
runBlockingis acceptable for public APIs maintaining synchronous contracts, but this is an internal implementation detail where proper coroutine patterns would be more appropriate.♻️ Proposed refactor to suspend function
- private fun completeAppDownload( + private suspend fun completeAppDownload( downloadInfo: DownloadInfo, downloadingAppId: Int, entitledDepotIds: List<Int>, selectedDlcAppIds: List<Int>, appDirPath: String, ) { Timber.i("Item $downloadingAppId download completed, saving database") // Update database - runBlocking { + withContext(Dispatchers.IO) { val appInfo = getInstalledApp(downloadingAppId) // ... rest of DB operations } // ... rest of function }Then update callers at lines 1333 and 1340 to use this as a suspend function (they're already in coroutine context).
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/src/main/java/app/gamenative/data/DownloadInfo.ktapp/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/service/gog/GOGService.kt
🚧 Files skipped from review as they are similar to previous changes (1)
- app/src/main/java/app/gamenative/data/DownloadInfo.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/gog/GOGService.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (12)
app/src/main/java/app/gamenative/service/SteamService.kt (12)
42-43: LGTM: New dependencies properly imported and injected.The new imports for
MarkerUtils,Marker,DownloadingAppInfo,DownloadingAppInfoDao, andCopyOnWriteArrayListare appropriate for the DLC download orchestration feature. The DI field follows the existing pattern.Also applies to: 143-144, 149-149, 179-180
289-297: LGTM: Enhanced partial download detection.The function now checks the database for persisted download state before falling back to marker-based detection, improving reliability across app restarts.
440-446: LGTM: New public accessor functions follow existing patterns.Both functions use
runBlockingto maintain synchronous API contracts, consistent with the existinggetAppInfoOffunction and acceptable per learnings for public APIs.
452-462: LGTM: Centralized AppInfo retrieval reduces duplication.The refactoring to use
getInstalledAppas a central accessor for both depot lists improves maintainability.
565-659: LGTM: Well-factored depot filtering and DLC merging.The extraction of
filterForDownloadableDepotseliminates duplication, and the indirect DLC app merging (lines 633-656) properly handles the case 3 pattern mentioned in the PR description.
903-910: LGTM: Comprehensive cleanup for multi-app downloads.The deletion of both downloading app info and indirect DLC apps ensures proper cleanup of the new multi-app download state.
920-943: LGTM: Three-tier resume logic properly handles different scenarios.The function correctly handles:
- Resuming active downloads from memory
- Resuming interrupted downloads from DB
- Verifying already-installed games
The
isUpdateOrVerifyflag distinguishes between fresh installations and verification operations.
945-955: LGTM: Simplified API bridges to full implementation.This overload provides a cleaner API for callers while delegating to the full implementation.
1203-1320: LGTM: Proper multi-app download setup with DepotDownloader.The code correctly:
- Uses
CopyOnWriteArrayListfor thread-safedownloadingAppIds- Sets
autoStartDownload=falseto allow configuration before starting- Creates separate
AppIteminstances for main app and each DLC- Properly links the download job for cancellation
1330-1349: LGTM: Sequential completion with proper cleanup.The completion flow correctly:
- Completes main app and DLC apps sequentially
- Removes the download job only after all completions
- Cleans up the
downloadingAppInfoentryA past review raised concerns about partial failures leaving inconsistent state, which was reportedly addressed in commit 87fb550.
1454-1461: LGTM: Proper cleanup on download failure.The failure handler correctly cleans up both in-memory and persisted download state.
2039-2039: LGTM: Consistent cleanup of download state on logout.Clearing
downloadingAppInfoon logout is appropriate, while preserving theAppInfotable aligns with the learning that installed game state should persist across login sessions.
|
|
||
| // Create DownloadInfo for progress tracking | ||
| val downloadInfo = DownloadInfo(jobCount = 1) | ||
| val downloadInfo = DownloadInfo(jobCount = 1, gameId = 0, downloadingAppIds = CopyOnWriteArrayList<Int>()) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify DownloadInfo usage and field access patterns
echo "=== DownloadInfo constructor calls in codebase ==="
rg -n "DownloadInfo\(" --type=kotlin -B2 -A2
echo -e "\n=== Access to gameId field in DownloadInfo ==="
rg -n "\.gameId" --type=kotlin | head -20
echo -e "\n=== Access to downloadingAppIds field ==="
rg -n "\.downloadingAppIds" --type=kotlin | head -20
echo -e "\n=== DownloadInfo class definition ==="
rg -n "data class DownloadInfo\|class DownloadInfo" --type=kotlin -A10Repository: utkarshdalal/GameNative
Length of output: 18549
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/data/DownloadInfo.ktRepository: utkarshdalal/GameNative
Length of output: 11498
Address hardcoded gameId = 0 and Steam-specific fields in GOG's DownloadInfo instantiation.
The DownloadInfo constructor now requires gameId (Int) and downloadingAppIds parameters. In this GOG download flow:
- The
gameIdis hardcoded to0, even though the function receives a GOG game ID (String) on line 298 - The
downloadingAppIdsis initialized as an empty list; this field is designed for Steam's DLC depot tracking and is never accessed in GOG's download flow - GOG uses String-based game IDs (unlike Steam's integer app IDs), so storing an integer
gameIdis semantically incorrect for this context
These fields appear to be Steam-specific additions that don't apply to GOG's architecture. Consider either:
- Making these parameters optional for non-Steam services, or
- Creating a Steam-specific subclass/wrapper instead of adding unused fields to the shared
DownloadInfo
🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/service/gog/GOGService.kt at line 302, The
GOGService instantiates DownloadInfo with a hardcoded gameId = 0 and an unused
downloadingAppIds list, which is Steam-specific and incorrect for GOG's
string-based IDs; update the model or usage so GOG doesn't misuse those fields:
either make DownloadInfo.gameId and downloadingAppIds optional/nullable (or
overload the constructor) and pass null/empty appropriately from GOGService (use
the GOG string id where meaningful), or create a Steam-specific subclass/wrapper
(e.g., SteamDownloadInfo) that adds gameId: Int and downloadingAppIds while
keeping DownloadInfo minimal for GOG; then update GOGService to stop hardcoding
0 and to use the appropriate type/constructor (referencing DownloadInfo, gameId,
downloadingAppIds, and GOGService).
| fun getMainAppDlcIdsWithoutProperDepotDlcIds(appId: Int): MutableList<Int> { | ||
| val mainAppDlcIds = mutableListOf<Int>() | ||
| val hiddenDlcAppIds = getHiddenDlcAppsOf(appId).orEmpty().map { it.id } | ||
|
|
||
| val appInfo = getAppInfoOf(appId) | ||
| if (appInfo != null) { | ||
| // for each of the dlcAppId found in main depots, filter the count = 1, add that dlcAppId to dlcAppIds | ||
| val checkingAppDlcIds = appInfo.depots.filter { it.value.dlcAppId != INVALID_APP_ID }.map { it.value.dlcAppId }.distinct() | ||
| checkingAppDlcIds.forEach { checkingDlcId -> | ||
| val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId } | ||
| if (checkMap.size == 1) { | ||
| val depotInfo = checkMap[checkMap.keys.first()]!! | ||
| if (depotInfo.osList.contains(OS.none) && | ||
| depotInfo.manifests.isEmpty() && | ||
| hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) { | ||
| mainAppDlcIds.add(checkingDlcId) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return mainAppDlcIds | ||
| } |
There was a problem hiding this comment.
Potential NPE on force unwrap.
Line 515 uses checkMap[checkMap.keys.first()]!! which could throw an NPE if the map entry is unexpectedly null. While unlikely given the filter, defensive code would be safer.
💚 Proposed fix
checkingAppDlcIds.forEach { checkingDlcId ->
val checkMap = appInfo.depots.filter { it.value.dlcAppId == checkingDlcId }
if (checkMap.size == 1) {
- val depotInfo = checkMap[checkMap.keys.first()]!!
- if (depotInfo.osList.contains(OS.none) &&
+ val depotInfo = checkMap.values.first()
+ if (depotInfo.osList.contains(OS.none) &&
depotInfo.manifests.isEmpty() &&
hiddenDlcAppIds.isNotEmpty() && hiddenDlcAppIds.contains(checkingDlcId)) {
mainAppDlcIds.add(checkingDlcId)
}
}
}🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/service/SteamService.kt around lines 504 -
526, In getMainAppDlcIdsWithoutProperDepotDlcIds, avoid the forced unwrap
checkMap[checkMap.keys.first()]!! which can NPE; instead retrieve the sole depot
safely (e.g., use checkMap.entries.firstOrNull()?.value or
checkMap.values.firstOrNull()) and guard with a null check or let, then run the
existing conditions and add the id only if depotInfo is non-null.
Updates the JavaSteam dependency to the latest snapshot release. Adds functionality to persist the download progress snapshot, ensuring progress is saved more frequently.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (5)
app/src/main/java/app/gamenative/service/SteamService.kt (5)
514-515: Prefer safer access to the depot value.Using
checkMap[checkMap.keys.first()]!!is unnecessarily complex and involves a force unwrap. Since you've already verifiedcheckMap.size == 1, usecheckMap.values.first()directly for cleaner and safer code.♻️ Suggested simplification
if (checkMap.size == 1) { - val depotInfo = checkMap[checkMap.keys.first()]!! + val depotInfo = checkMap.values.first() if (depotInfo.osList.contains(OS.none) &&
920-943: Consider explicit filtering for resumed downloads.When resuming via
currentDownloadInfo.downloadingAppIds(line 923), this list may contain the mainappId. While the downstream code at lines 1158-1163 implicitly filters it out (sincedlcAppDepotswon't contain depots for the main app), an explicit filter would make the intent clearer and prevent future bugs.♻️ Suggested explicit filtering
fun downloadApp(appId: Int): DownloadInfo? { val currentDownloadInfo = downloadJobs[appId] if (currentDownloadInfo != null) { - return downloadApp(appId, currentDownloadInfo.downloadingAppIds, isUpdateOrVerify = false) + // Filter out the main appId since downloadingAppIds may include it + return downloadApp(appId, currentDownloadInfo.downloadingAppIds.filter { it != appId }, isUpdateOrVerify = false) } else {
1193-1201: Main appId may be incorrectly persisted in DLC list.When resuming a download,
userSelectedDlcAppIdsmay contain the mainappId(fromcurrentDownloadInfo.downloadingAppIds). This gets persisted to the database, semantically marking the main app as its own DLC. While the current code handles this gracefully during depot selection, it's incorrect data that could cause confusion in future code changes.♻️ Suggested fix: filter out main appId before persisting
// Save downloading app info runBlocking { + val dlcAppIdsOnly = userSelectedDlcAppIds.filter { it != appId } instance?.downloadingAppInfoDao?.insert( DownloadingAppInfo( appId, - dlcAppIds = userSelectedDlcAppIds + dlcAppIds = dlcAppIdsOnly ), ) }
1375-1428: Consider converting to suspend function.This function uses
runBlocking(line 1385) for database operations but is called from within a coroutine context (lines 1333, 1340). WhilerunBlockingis acceptable for maintaining synchronous APIs per project learnings, using it inside an existing coroutine blocks the thread unnecessarily. Converting to a suspend function withwithContext(Dispatchers.IO)would be more idiomatic.Based on learnings,
runBlockingis acceptable for public APIs, but this is a private internal function. Consider whether the performance impact is acceptable for your use case.
1184-1186: Consider adding user notification when no depots are available.Returning
nullsilently whenselectedDepots.isEmpty()may leave users confused about why the download didn't start. Adding a notification would improve user experience.♻️ Suggested improvement
Timber.i("selectedDepots is empty? " + selectedDepots.isEmpty()) if (selectedDepots.isEmpty()) { + Timber.w("No depots available to download for app $appId") + instance?.notificationHelper?.notify("No content available to download") return null }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/build.gradle.ktsapp/src/main/java/app/gamenative/service/SteamService.ktgradle/libs.versions.toml
🚧 Files skipped from review as they are similar to previous changes (2)
- app/build.gradle.kts
- gradle/libs.versions.toml
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
app/src/main/java/app/gamenative/service/SteamService.kt (4)
565-596: LGTM!The
filterForDownloadableDepotsfunction is well-structured with clear, sequential filtering criteria. The helper function improves code reuse acrossgetMainAppDepotsandgetDownloadableDepots.
625-659: LGTM!The extension to include indirect DLC depots is well-implemented. Creating new
DepotInfoobjects with the correctdlcAppIdensures proper tracking during download completion.
1452-1471: LGTM!The failure handling correctly cleans up the
downloadingAppInfostate and notifies the user. Progress persistence on chunk/depot completion ensures reliable resume capability.
2036-2049: LGTM!Correctly clears
downloadingAppInfoDaoduring database clear. Per project learnings,AppInfo(installation state) is preserved across logout, butDownloadingAppInfo(download progress state) should be cleared as it's user-session-specific.
| @Query("SELECT * FROM steam_app AS app WHERE dlc_for_app_id = :appId AND depots <> '{}' AND " + | ||
| " EXISTS (" + | ||
| " SELECT * FROM steam_license AS license " + | ||
| " WHERE license.license_type <> 0 AND " + | ||
| " REPLACE(REPLACE(app_ids, '[', ','), ']', ',') LIKE ('%,' || app.id || ',%') " + | ||
| " REPLACE(REPLACE(license.app_ids, '[', ','), ']', ',') LIKE ('%,' || app.id || ',%') " + | ||
| ")" | ||
| ) | ||
| suspend fun findDownloadableDLCApps(appId: Int): List<SteamApp>? | ||
|
|
||
| @Query("SELECT * FROM steam_app AS app WHERE dlc_for_app_id = :appId AND depots = '{}' AND " + | ||
| " EXISTS (" + | ||
| " SELECT * FROM steam_license AS license " + | ||
| " WHERE license.license_type <> 0 AND " + | ||
| " REPLACE(REPLACE(license.app_ids, '[', ','), ']', ',') LIKE ('%,' || app.id || ',%') " + |
There was a problem hiding this comment.
Question - why did we need to split this into two different queries, one with depots <> '{}' and one with depots = '{}'?
Why not leave out the depots clause entirely?
| private fun completeAppDownload( | ||
| downloadInfo: DownloadInfo, | ||
| downloadingAppId: Int, | ||
| entitledDepotIds: List<Int>, | ||
| selectedDlcAppIds: List<Int>, | ||
| appDirPath: String, | ||
| ) { | ||
| Timber.i("Item $downloadingAppId download completed, saving database") | ||
|
|
||
| // Update database | ||
| runBlocking { | ||
| val appInfo = getInstalledApp(downloadingAppId) | ||
|
|
||
| // Update Saved AppInfo | ||
| if (appInfo != null) { | ||
| val updatedDownloadedDepots = (appInfo.downloadedDepots + entitledDepotIds).distinct() | ||
| val updatedDlcDepots = (appInfo.dlcDepots + selectedDlcAppIds).distinct() | ||
|
|
||
| instance?.appInfoDao?.update( | ||
| AppInfo( | ||
| downloadingAppId, | ||
| isDownloaded = true, | ||
| downloadedDepots = updatedDownloadedDepots.sorted(), | ||
| dlcDepots = updatedDlcDepots.sorted(), | ||
| ), | ||
| ) | ||
| } else { | ||
| instance?.appInfoDao?.insert( | ||
| AppInfo( | ||
| downloadingAppId, | ||
| isDownloaded = true, | ||
| downloadedDepots = entitledDepotIds.sorted(), | ||
| dlcDepots = selectedDlcAppIds.sorted(), | ||
| ), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Remove completed appId from downloadInfo.dlcAppIds | ||
| downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId } | ||
|
|
||
| // All downloading appIds are removed | ||
| if (downloadInfo.downloadingAppIds.isEmpty()) { | ||
| // Handle completion: add markers | ||
| MarkerUtils.addMarker(appDirPath, Marker.DOWNLOAD_COMPLETE_MARKER) | ||
| PluviaApp.events.emit(AndroidEvent.LibraryInstallStatusChanged(downloadInfo.gameId)) | ||
|
|
||
| MarkerUtils.removeMarker(appDirPath, Marker.STEAM_DLL_REPLACED) | ||
| MarkerUtils.removeMarker(appDirPath, Marker.STEAM_COLDCLIENT_USED) | ||
|
|
||
| // Clear persisted bytes file on successful completion | ||
| downloadInfo.clearPersistedBytesDownloaded(appDirPath) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Should we move this back into onDownloadCompleted and remove this function?
Feat/game manager utkarsh2
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In @app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt:
- Around line 130-136: allDownloadableApps.add(...) uses
downloadableDepots.toSortedMap().values.first() which will throw if
downloadableDepots is empty; change to call firstOrNull() and guard: get val
baseDepot = downloadableDepots.toSortedMap().values.firstOrNull() and if
baseDepot is null, skip adding the base game and avoid mutating
selectedAppIds[gameId] and enabledAppIds[gameId] (or set sensible defaults),
otherwise add Pair(gameId, baseDepot) and then set selectedAppIds and
enabledAppIds as before.
🧹 Nitpick comments (5)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (1)
1245-1247: Consider using structured concurrency for the download launch.
CoroutineScope(Dispatchers.IO).launchcreates an unstructured coroutine that isn't tied to the composable's lifecycle. If the user navigates away quickly, this coroutine continues running independently. While this may be intentional for a download operation, you might consider usingrememberCoroutineScope()for consistency with other parts of the codebase (note:scopeis already available at line 869).That said, for background downloads that should persist beyond the UI, the current approach may be acceptable.
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
65-71: Consider extracting duplicateInstallSizeInfoclass.This data class is identical to the one defined in
SteamAppScreen.kt(lines 58-64). Consider moving it to a shared location (e.g., adatapackage) to avoid duplication and ease maintenance.app/src/main/java/app/gamenative/service/SteamService.kt (3)
504-526: Add inline documentation explaining DLC depot patterns.This method handles a specific DLC depot pattern mentioned in the PR objectives (case 2: DLC depots inside main game depots without valid dlcappid), but the complex filtering logic lacks explanation. Consider adding a comment like:
/** * Returns DLC app IDs that have depots listed in the main game's depots but without * proper dlcAppId metadata (pattern 2 from PR objectives). These depots typically have: * - Only 1 depot referencing the DLC app ID * - OS.none in osList * - Empty manifests * - Present in hiddenDlcAppIds list * Such DLCs are treated as included in the main game. */
575-577: Consider simplifying the OS filtering logic for readability.The nested negations make this condition hard to understand at a glance. The current logic filters out depots that target Linux or macOS exclusively, keeping Windows and indeterminate OS depots.
♻️ More readable alternative
- if (!(depot.osList.contains(OS.windows) || - (!depot.osList.contains(OS.linux) && !depot.osList.contains(OS.macos))) - ) + if (!depot.osList.contains(OS.windows) && + depot.osList.any { it == OS.linux || it == OS.macos }) return falseThis maintains the same logic (reject depots that are Linux/macOS but not Windows) while being more explicit about the intent.
1498-1520: LGTM! Consider the performance impact of frequent progress persistence.The listener now persists progress after every chunk completion (Line 1500) and depot completion (Line 1520), which provides excellent resume support. However, for large downloads with many small chunks, this could cause frequent I/O operations.
If performance becomes an issue, consider throttling persistence (e.g., max once per second, or after every N chunks) while keeping the depot completion persistence for safety.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
app/src/main/java/app/gamenative/service/SteamService.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
📚 Learning: 2025-09-28T13:56:06.888Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/service/SteamService.kt:179-180
Timestamp: 2025-09-28T13:56:06.888Z
Learning: In the GameNative project, the AppInfo table (with AppInfoDao) tracks local game installation state including which apps are downloaded and which depots were installed. This data should NOT be cleared during logout in clearDatabase() because games remain physically installed on the device and users should see their installed games when logging back in. Only user-specific Steam account data should be cleared on logout.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:07:27.941Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.
Applied to files:
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.ktapp/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-28T13:54:16.048Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 176
File: app/src/main/java/app/gamenative/utils/SteamUtils.kt:651-655
Timestamp: 2025-09-28T13:54:16.048Z
Learning: In the GameNative project, when creating depots.txt files in SteamUtils.ensureSteamSettings, only create the file when SteamService.getInstalledDepotsOf() returns actual data (non-null). Don't create empty files or write empty strings when no data is available.
Applied to files:
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.ktapp/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-12-17T05:14:05.133Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 344
File: app/src/main/java/app/gamenative/service/SteamAutoCloud.kt:415-419
Timestamp: 2025-12-17T05:14:05.133Z
Learning: In SteamAutoCloud.kt, when uploading files to Steam Cloud API (beginFileUpload and commitFileUpload), the filename parameter intentionally uses different formats: `file.path + file.filename` (relative path without placeholder) when `appInfo.ufs.saveFilePatterns.isEmpty()` is true (fallback case), and `file.prefixPath` (includes placeholder like %SteamUserData%) when patterns are configured. This difference is by design.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
📚 Learning: 2025-09-19T17:13:01.017Z
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt:136-136
Timestamp: 2025-09-19T17:13:01.017Z
Learning: In LibraryAppScreen.kt, the user prefers to use runBlocking to maintain synchronous APIs when underlying methods have been converted to suspend functions, rather than refactoring all calling code to be async. This approach prevents UI breakage and maintains API compatibility. The user confirmed this is acceptable when performance is not a concern.
Applied to files:
app/src/main/java/app/gamenative/service/SteamService.kt
🧬 Code graph analysis (3)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (4)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
GameManagerDialog(73-444)app/src/main/java/app/gamenative/ui/screen/library/appscreen/BaseAppScreen.kt (1)
getGameDisplayInfo(70-74)app/src/main/java/app/gamenative/ui/screen/library/appscreen/CustomGameAppScreen.kt (1)
getGameDisplayInfo(73-179)app/src/main/java/app/gamenative/service/SteamService.kt (1)
getAppDirPath(670-692)
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (1)
app/src/main/java/app/gamenative/ui/internal/FakeData.kt (1)
fakeAppInfo(20-95)
app/src/main/java/app/gamenative/service/SteamService.kt (1)
app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt (1)
getInstalledApp(23-24)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (22)
app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt (6)
51-52: LGTM!Clean imports for the new GameManagerDialog components.
135-147: LGTM!The gameManagerDialogStates management follows the established pattern used for installDialogStates and uninstallDialogAppIds.
630-640: LGTM!Good addition allowing users to manage DLC content for already-installed games via the menu.
857-866: LGTM!State synchronization follows the established pattern used for
installDialogState.
986-996: LGTM!Permission handling for the GameManagerDialog is correctly integrated with the existing permission launcher.
457-462: The helper functionsbuildInstallPromptStateandbuildNotEnoughSpaceState(lines 66-96) are still actively used in the codebase. They are called at lines 978-980 within a separate LaunchedEffect that handles theDialogType.INSTALL_APP_PENDINGstate. The codebase currently maintains two install flows: the legacy approach usinginstallDialogStatewith these helper functions, and the newGameManagerDialogStateapproach shown at lines 457-462. Both paths coexist and the functions should not be removed.app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt (4)
73-103: LGTM!The composable signature and state initialization are well-structured. Using
@ComposableforonGetDisplayInfoallows the caller to provide reactive display info, which is appropriate here.
246-258: LGTM with minor readability suggestion.The
installButtonEnabled()logic correctly handles both fresh install and DLC addition scenarios. Line 254's(realSelectedAppIds.size - 1) > 0could be simplified torealSelectedAppIds.size > 1for readability, but the current form is also clear.
260-443: LGTM!The dialog UI is well-structured with appropriate hero image handling, DLC list with checkboxes, and install controls. The size display and button enable logic correctly integrate with the state calculations.
446-475: LGTM!The preview function is well-structured using the existing
fakeAppInfohelper with its built-in debug guard.app/src/main/java/app/gamenative/service/SteamService.kt (12)
179-180: LGTM!The injection of
DownloadingAppInfoDaofollows the existing dependency injection pattern and is used throughout the file for tracking in-progress downloads with resume support.
289-297: LGTM!The addition of database state checking improves resume support by checking
DownloadingAppInfobefore falling back to marker file checks.
440-462: LGTM!The new accessor methods follow existing patterns in this file (e.g.,
getAppInfoOfat Line 436) and userunBlockingto maintain synchronous APIs, which is acceptable per project conventions.
598-611: LGTM!The
getMainAppDepotsmethod correctly filters main app depots using the same criteria asgetDownloadableDepots, providing a focused view of just the base game depots without DLC content.
1331-1349: LGTM!The completion flow correctly:
- Completes downloads for main app and each DLC app separately (Lines 1331-1341)
- Removes the download job (Line 1344)
- Cleans up the downloading app info from the database (Lines 1347-1349)
The exception handler at Line 1350 intentionally preserves downloading app info for resume support, which is correct.
1350-1359: LGTM!The exception handler correctly:
- Persists progress for resume support (Line 1352)
- Marks all depots as failed (Lines 1354-1357)
- Removes the download job (Line 1358)
- Intentionally preserves
DownloadingAppInfoin the database for resume support
1375-1427: LGTM! Method correctly handles per-app completion.The
completeAppDownloadmethod properly:
- Updates or inserts
AppInfowith downloaded depot IDs (Lines 1388-1408)- Tracks completion by removing the app from
downloadingAppIds(Line 1412)- Adds markers and emits events only when all apps complete (Lines 1415-1426)
Note: Line 1390 confirms that
dlcDepotsstores app IDs (not depot IDs), which validates the critical issue flagged at Lines 931-938.
2044-2044: LGTM!Correctly clears downloading app info during database cleanup, consistent with clearing other temporary download state.
1278-1320: LGTM! The download orchestration correctly:
- Creates
DepotDownloaderwithautoStartDownload = false- Adds all app items (main and DLC) to the queue
- Calls
finishAdding()to signal completion- Explicitly calls
startDownloading()to beginThis prevents premature download starts before all items are queued—the intended usage pattern for
DepotDownloader.
931-938: Remove this review comment - the code is functionally correct.There is no type confusion bug here. Both
getInstalledDlcDepotsOf(appId)(line 931) and the DLC app IDs added in the loop (lines 933-938) contain app IDs, not depot IDs. TheAppInfo.dlcDepotsfield stores DLC app IDs, as confirmed by lines 1390 and 1406 whereselectedDlcAppIds(app IDs) are assigned directly todlcDepots.The logic correctly combines previously installed DLC app IDs with newly detected ones for verification. The only minor issue is misleading method/field naming:
dlcDepotsandgetInstalledDlcDepotsOf()should be nameddlcAppIdsandgetInstalledDlcAppIdsOf()respectively to avoid confusion about what data they actually contain.Likely an incorrect or invalid review comment.
633-656: The override ofdlcAppIdat line 645 is intentional and necessary for handling indirect DLC apps. Manifest retrieval usesdepot.depotIdexclusively (notdlcAppId), and DLC ownership checks also rely ondepot.depotIdonly. ThedlcAppIdfield is used solely for ownership/filtering logic, and setting it to the actual owning DLC app ID is critical for proper UI filtering and selection of indirect DLC apps.
905-910: DLC deletion is safe and does not require changes.Lines 905-910 are protected by the database query itself. The
getDownloadableDlcAppsOf()method uses a SQL query that filters DLCs using:
dlc_for_app_id = :appId— ensures each DLC belongs only to this parent appEXISTS (SELECT * FROM steam_license WHERE license_type <> 0 AND app.id in license.app_ids)— validates that each DLC has valid ownershipAdditionally, DLCs are 1-to-1 with their parent app and cannot be shared across multiple games. Deleting a parent app's DLC entries when the parent is deleted is the correct behavior, and no separate ownership checks are needed.
Likely an incorrect or invalid review comment.
| allDownloadableApps.sortBy { it.first } | ||
|
|
||
| // Add Base Game | ||
| allDownloadableApps.add(0, Pair(gameId, downloadableDepots.toSortedMap().values.first())) | ||
| selectedAppIds[gameId] = true | ||
| enabledAppIds[gameId] = false | ||
| } |
There was a problem hiding this comment.
Potential crash if downloadableDepots is empty.
Line 133 calls .first() on downloadableDepots.toSortedMap().values, which throws NoSuchElementException if the map is empty. This could occur for games without downloadable depots.
🛡️ Suggested defensive fix
- // Add Base Game
- allDownloadableApps.add(0, Pair(gameId, downloadableDepots.toSortedMap().values.first()))
- selectedAppIds[gameId] = true
- enabledAppIds[gameId] = false
+ // Add Base Game
+ val baseGameDepot = downloadableDepots.toSortedMap().values.firstOrNull()
+ if (baseGameDepot != null) {
+ allDownloadableApps.add(0, Pair(gameId, baseGameDepot))
+ selectedAppIds[gameId] = true
+ enabledAppIds[gameId] = false
+ }🤖 Prompt for AI Agents
In @app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
around lines 130 - 136, allDownloadableApps.add(...) uses
downloadableDepots.toSortedMap().values.first() which will throw if
downloadableDepots is empty; change to call firstOrNull() and guard: get val
baseDepot = downloadableDepots.toSortedMap().values.firstOrNull() and if
baseDepot is null, skip adding the base game and avoid mutating
selectedAppIds[gameId] and enabledAppIds[gameId] (or set sensible defaults),
otherwise add Pair(gameId, baseDepot) and then set selectedAppIds and
enabledAppIds as before.

Introduces a new dialog for managing game content, allowing users to select which DLCs to install alongside the base game.
This dialog provides a user interface to manage optional DLCs, calculates installation sizes, and ensures sufficient storage space is available before initiating the installation process. It enhances the user experience by providing more control over the installed game content.
For reference, in steam data I found there are like 3 types of dlc:
Tested Games examples:
Summary by CodeRabbit
New Features
Improvements
Localization
API
✏️ Tip: You can customize this high-level summary in your review settings.