Skip to content

Feat: Game Manager#366

Merged
utkarshdalal merged 8 commits intoutkarshdalal:masterfrom
joshuatam:feat/game-manager
Jan 8, 2026
Merged

Feat: Game Manager#366
utkarshdalal merged 8 commits intoutkarshdalal:masterfrom
joshuatam:feat/game-manager

Conversation

@joshuatam
Copy link
Contributor

@joshuatam joshuatam commented Dec 26, 2025

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:

  1. dlc depots are appeared inside main game's depots, and each dlc depot has a valid value of dlcappid (current GN supported)
  2. Same as case 1, but some of the dlc depots do not have a valid dlcappid (currently GN treat it to be included in the main game)
  3. The dlc depots are placed inside the another dlc app and do not appear in main game depots (currently GN not supported yet)

Tested Games examples:

  • Witcher 3 (case 1)
  • Dont starve (Case 2)
  • Halo MCC (Case 1)
  • Cyberpunk 2077 (Case 3)

Summary by CodeRabbit

  • New Features

    • Full-screen Game Manager to view/select base game + DLCs and start installs.
    • Per-game dialog controls and new install UI entry points.
  • Improvements

    • DLC-aware download orchestration with refined depot selection, per‑DLC grouping and resume support.
    • Cancellation now records and surfaces a provided reason for clearer reporting.
  • Localization

    • New standalone install-size label added across languages.
  • API

    • Database schema bumped; added per-app download state storage and service accessors.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 26, 2025

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary of changes (attention points)
Service & download logic
app/src/main/java/app/gamenative/service/SteamService.kt, app/src/main/java/app/gamenative/data/DownloadInfo.kt
SteamService: added per-app/DLC depot selection APIs, download orchestration, persistence via DownloadingAppInfoDao, changed downloadApp overloads and completion/failure flows, many public accessors added. DownloadInfo: added val gameId: Int, var downloadingAppIds: CopyOnWriteArrayList<Int>, and cancel()/cancel(message)/failedToDownload() — review concurrency, persisted-progress clearing, and cancellation message propagation.
Database: entities & DAOs
app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt, app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt, app/src/main/java/app/gamenative/db/PluviaDatabase.kt, app/src/main/java/app/gamenative/di/DatabaseModule.kt
New Room entity DownloadingAppInfo and DAO (insert/get/delete/deleteAll); registered entity in PluviaDatabase, DB version bumped 9 → 10, and DI provider added — check migrations and schema compatibility.
DAO / query changes
app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt, app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt
SteamAppDao: removed findApp, added findDownloadableDLCApps(appId) and findHiddenDLCApps(appId) with depot-empty vs non-empty filtering and license.app_ids matching. AppInfoDao: renamed getInstalledDepots(...)getInstalledApp(...) — update call sites.
UI: GameManager dialog & state
app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt, app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt, app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt, app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
Adds Compose full-screen GameManagerDialog (+ Preview) and GameManagerDialogState (with Saver); new ManageGameContent menu option and per‑game dialog management APIs in SteamAppScreen (show/hide/get). Dialog selection triggers download flows and side effects — review state persistence, enabled/selection logic, and integration points.
Utils & depot emission
app/src/main/java/app/gamenative/utils/SteamUtils.kt
Rewrites depots emission: recreates depots.txt deterministically, uses installed + downloadable DLC app sources, deduplicates/sorts DLC IDs, refines hidden DLC emission, and adds safe DX fetch — verify ordering and duplicate suppression.
Resources (localizations)
app/src/main/res/values*/strings.xml
Added steam_install_space string to multiple locales (values, values-da, values-fr, values-pt-rBR, values-zh-rCN, values-zh-rTW) — UI may reference the new key.
Build / dependency metadata
app/build.gradle.kts, gradle/libs.versions.toml
Updated local jar paths and changed javasteam/javasteam-depotdownloader group to io.github.joshuatam with version ref 1.8.0-5-SNAPSHOT — confirm artifact coordinates and local build jars.
GOG service usage
app/src/main/java/app/gamenative/service/gog/GOGService.kt
DownloadInfo instantiation updated from DownloadInfo(jobCount = 1) to DownloadInfo(jobCount = 1, gameId = 0, downloadingAppIds = CopyOnWriteArrayList<Int>()) — check downstream handling of added fields.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

"I'm a rabbit in a meadow, tapping keys with glee,
Depots and DLCs in bundles — hop, select, and be free! 🐇
Checkboxes checked, markers set, bytes drift like a stream,
I nibble through the changelog while your installs gleam.
Hooray — more games to manage, more carrots for the team!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat: Game Manager' clearly and concisely describes the main feature being introduced—a new Game Manager component that allows users to manage DLC installations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@joshuatam joshuatam marked this pull request as ready for review December 28, 2025 00:58
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (6)
app/src/main/java/app/gamenative/data/DownloadInfo.kt (1)

11-14: Consider thread-safety for downloadingAppIds.

The downloadingAppIds property is declared as var List<Int>, making it reassignable but not thread-safe. Since DownloadInfo is used in concurrent download scenarios (as evidenced by CopyOnWriteArrayList for speedSamples), 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 val would 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.

LibraryItem is 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() and getHiddenDlcAppsOf() directly in the composable body executes on every recomposition. These should be wrapped in remember with appropriate keys or moved into the LaunchedEffect.

🔎 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 with remember or derivedStateOf.

🔎 Suggested fix
val installSizeInfo by remember(downloadableDepots.keys.toSet(), selectedAppIds.toMap()) {
    derivedStateOf { getInstallSizeInfo() }
}

Then use installSizeInfo directly 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 dlcAppId data. 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

📥 Commits

Reviewing files that changed from the base of the PR and between efbf14f and ce6b532.

📒 Files selected for processing (16)
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/enums/Marker.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt
  • app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/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.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/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.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • 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/enums/Marker.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • 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/utils/SteamUtils.kt
  • 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)
🔇 Additional comments (22)
app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt (1)

22-23: LGTM! Clean enum addition.

The new ManageGameContent enum 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 consistent

The new steam_install_space string mirrors steam_install_space_prompt and library_download_prompt_message in both wording and placeholder ordering; newline usage and %1$s/%2$s/%3$s are correct. No changes needed.

app/src/main/res/values-fr/strings.xml (1)

15-16: steam_install_space FR entry is aligned with existing prompt

The steam_install_space string reuses the same labels, order, and %1$s/%2$s/%3$s placeholders 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-formed

The steam_install_space string correctly mirrors the prompt variant and Library dialog (same labels, order, and %1$s/%2$s/%3$s placeholders). 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 reuse

The new steam_install_space resource cleanly extracts the size block from the existing prompt, with identical text and %1$s/%2$s/%3$s ordering. 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_MARKER enum 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 getInstalledDepots to getInstalledApp better reflects the return type (AppInfo). However, getInstalledApp and get now 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:

  1. Renames to getInstalledDlcDepotsOf for clarity
  2. Adds getDownloadableDlcAppsOf to retrieve DLC apps
  3. Tracks appendedDlcIds to prevent duplicate entries
  4. Iterates through downloadable DLC apps to get their installed depots
  5. Only adds hidden DLCs if not already present

The duplicate-prevention logic using appendedDlcIds is 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 installDialogStates and uninstallDialogAppIds, maintaining consistency across the codebase.


457-462: Install flow now uses GameManagerDialog.

The change from the simple install prompt to GameManagerDialog enables 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:

  1. MODIFYING_MARKER is added to indicate ongoing modification
  2. 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. The SteamService.downloadApp(gameId, dlcAppIds) call correctly matches the existing method signature fun 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 the app_ids column. 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 on steam_license for 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 runBlocking for synchronous access to suspend functions is consistent with the existing codebase pattern (per learnings).


536-582: LGTM! Clean extraction of filter logic.

The filterForDownloadableDepots function 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:

  1. Starts with main app depots
  2. Iterates through indirect DLC apps
  3. Creates new DepotInfo objects with the correct dlcAppId attribution

This 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 downloadingAppIds for resuming downloads with the same DLC selection.


1216-1254: LGTM! Multi-app download setup.

The listener pattern correctly handles:

  1. Main app listener with its depot-to-index mapping
  2. Separate listeners for each DLC app
  3. Each DLC app added as a separate AppItem to the downloader

The 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:

  1. Cancelling the main download job explicitly
  2. Adding a DOWNLOAD_FAILED_MARKER to distinguish from user cancellation

1089-1094: LGTM! Incremental download logic.

The MODIFYING_MARKER check correctly filters out already-downloaded depots when updating an existing installation. This enables incremental DLC downloads without re-downloading the base game.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 !! on SteamService.getAppInfoOf(gameId) will throw NullPointerException if 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() throws NoSuchElementException if 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) and installButtonEnabled() (line 188), causing duplicate iterations over downloadableDepots. 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 derivedStateOf to 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

📥 Commits

Reviewing files that changed from the base of the PR and between ce6b532 and 63b2837.

📒 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. The PluviaTheme wrapper provides proper theming context.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 !! on SteamService.getAppInfoOf(gameId) will throw NullPointerException if 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() throws NoSuchElementException if the map is empty. Add a guard or use firstOrNull().


233-251: Missing accessibility: No content description for hero image.

The CoilImage lacks a contentDescription for screen readers. For decorative images, explicitly pass contentDescription = null in ImageOptions.

🧹 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 gameId and downloadingAppIds properties lack documentation. Adding KDoc would clarify:

  • What gameId represents (e.g., the main game's app ID)
  • What downloadingAppIds tracks (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() and selectedAppIds.toMap() as remember keys 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 derivedStateOf already tracks reads from downloadableDepots and selectedAppIds (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) > 0 is 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.releaseDate is 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 1000L ensures 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 despite MODIFYING_MARKER being 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

📥 Commits

Reviewing files that changed from the base of the PR and between 63b2837 and 7a9d421.

📒 Files selected for processing (4)
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/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.kt
  • 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/ui/component/dialog/GameManagerDialog.kt
  • 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/ui/component/dialog/GameManagerDialog.kt
  • 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 (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 like add(), remove(), and clear(). The use of CopyOnWriteArrayList already handles thread-safety for content modifications, and no reference replacement occurs in the codebase. While using val is 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() returns EnumSet.of(OS.none) for null, empty, or invalid OS strings, making OS.none the proper sentinel value for "no valid OS information." Since osList is an EnumSet<OS> that always contains at least OS.none when no valid OS is specified, checking osList.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.

Comment on lines +103 to +107
LaunchedEffect(visible) {
scrollState.animateScrollTo(0)

downloadableDepots.clear()
allDownloadableApps.clear()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 !! on SteamService.getAppInfoOf(gameId) will throw NullPointerException if 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 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()

133-134: Potential crash on empty depot map.

downloadableDepots.toSortedMap().values.first() throws NoSuchElementException if the map is empty. Add a guard or use firstOrNull().

🔎 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 CoilImage lacks a contentDescription for screen readers. For decorative images, explicitly mark as null; 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 app

For 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

📥 Commits

Reviewing files that changed from the base of the PR and between d2b1e08 and 0048be3.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/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.kt
  • app/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.kt
  • app/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 installDialogStates and uninstallDialogAppIds, 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_depots values 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_MARKER for 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_depots values 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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:

  1. Separating main app depots from DLC app depots
  2. Filtering already-downloaded depots when modifying
  3. Handling DLCs without proper depot structures (mainAppDlcIds)
  4. 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 mainAppDlcIds and why calculatedDlcAppIds is cleared when dlcAppDepots.isEmpty().

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0048be3 and 6dfa7c9.

📒 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 filterForDownloadableDepots and getMainAppDepots improves 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 getDownloadableDepots now properly aggregates depots from both the main app and indirect DLC apps. The custom DepotInfo construction 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 downloadApp methods 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 onDownloadCompleted properly:

  • 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 CopyOnWriteArrayList for thread-safe concurrent access

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6dfa7c9 and be43956.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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 filterForDownloadableDepots function cleanly consolidates the eligibility checks (OS, architecture, DLC ownership, language). The getDownloadableDepots function correctly aggregates main app depots with indirect DLC app depots, ensuring each depot carries the correct dlcAppId for 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 downloadingAppIds complete (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 that getDownloadableDepots() includes optional DLCs from the uncommented filter, and GameManagerDialog presents 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 visible changes to false, 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() throws NoSuchElementException if the map is empty. Use firstOrNull() 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 CoilImage lacks a contentDescription, which is important for screen readers. For decorative images, explicitly set to null; 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 filter calls make it difficult to understand which apps are being passed to onInstall. 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

📥 Commits

Reviewing files that changed from the base of the PR and between be43956 and d55461f.

📒 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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_MARKER existence 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 passes currentDownloadInfo?.downloadingAppIds (which already contains the main appId) to the downloadApp(appId, dlcAppIds) overload. This causes the main appId to 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 appId from downloadingAppIds before passing it as dlcAppIds at line 911, or skip adding appId if 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 getMainAppDlcIdsWithoutProperDepotDlcIds is quite verbose. Consider a shorter alternative like getStubDlcIds or getEmptyDepotDlcIds to 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

📥 Commits

Reviewing files that changed from the base of the PR and between d55461f and 887a031.

📒 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 getDownloadableDlcAppsOf and getInstalledApp methods provide cleaner access to DLC and installation data. The runBlocking pattern is consistent with existing code patterns in this service.


588-649: LGTM: Depot filtering logic correctly aggregates main and DLC depots.

The getMainAppDepots and getDownloadableDepots functions properly leverage the centralized filterForDownloadableDepots helper. The custom DepotInfo creation 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 CopyOnWriteArrayList for downloadingAppIds is 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:

  1. Line 1366: downloadInfo.downloadingAppIds.removeIf { it == downloadingAppId }
  2. Line 1369: if (downloadInfo.downloadingAppIds.isEmpty())

While CopyOnWriteArrayList makes individual operations thread-safe, the compound check-then-act pattern here isn't atomic. If two DLC listeners complete simultaneously:

  • Both could see isEmpty() = true after their respective removals
  • Both would attempt cleanup (lines 1370-1382)
  • Or both could see isEmpty() = false, and neither would clean up

A past review flagged this same issue (supposedly addressed in commit 7a9d421), but the current code lacks synchronization. Please verify whether:

  1. The cleanup operations are idempotent (safe to run multiple times)
  2. There's synchronization elsewhere that prevents this scenario
  3. 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 the dlcAppIds parameter. The main implementation then:

  • Line 1125 adds all dlcAppIds to the new list
  • Line 1131 adds appId again if mainAppDepots.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 filter appId from 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:

  1. The checkMap.size == 1 check guarantees the map isn't empty, but using checkMap.values.first() would be clearer than checkMap[checkMap.keys.first()]!!
  2. 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 filterForDownloadableDepots function 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

📥 Commits

Reviewing files that changed from the base of the PR and between dc244d4 and 74e88ff.

📒 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: MarkerUtils and Marker for marker-based state management, and CopyOnWriteArrayList for thread-safe tracking of concurrent DLC downloads.

Also applies to: 147-147


430-432: LGTM! New public APIs for DLC management.

The getDownloadableDlcAppsOf and getInstalledApp methods follow the established pattern of using runBlocking(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 getInstalledDepotsOf and getInstalledDlcDepotsOf to use the new getInstalledApp helper improves code maintainability and consistency.


588-601: LGTM! Clean separation for main app depot selection.

The getMainAppDepots function provides a focused API for retrieving only the main app's downloadable depots, properly delegating filtering to the new filterForDownloadableDepots helper.


607-649: LGTM! Correctly handles DLC depots from separate DLC apps.

The expanded getDownloadableDepots function now supports case 3 from the PR description (DLC depots placed inside another DLC app). The intentional passing of null for ownedDlc on 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 deleteApp function 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_MARKER exists (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 downloadingAppIds is now a CopyOnWriteArrayList (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:

  1. Successfully remove their respective IDs
  2. Both observe isEmpty() == true
  3. 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)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74e88ff and ffd8b5e.

📒 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.

Comment on lines +785 to +791
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}")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 passes currentDownloadInfo?.downloadingAppIds as the dlcAppIds parameter. If this list already contains appId (which it will if the main app is being downloaded), then at line 1129, appId is added again when mainAppDepots.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 appId from dlcAppIds at line 911 before passing it forward, or check that appId is not already in downloadingAppIds before 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 calculatedDlcAppIds to mainAppDlcIds
  • Clears calculatedDlcAppIds and downloadingAppIds
  • Adds only main appId to downloadingAppIds

This 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:

  1. User selects DLCs for installation
  2. All selected DLC depots are in the main app's depot list
  3. The DLC IDs should still be tracked for database persistence

The current code adds them to mainAppDlcIds which 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 onInstall callback launches a coroutine to download the app and selected DLCs, but doesn't include error handling. If SteamService.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

📥 Commits

Reviewing files that changed from the base of the PR and between e656852 and b9bca11.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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.kt
  • app/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.kt
  • 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
🧬 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 getDownloadableDlcAppsOf and getInstalledApp methods properly wrap DAO calls using runBlocking(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 dlcAppId values 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 filterForDownloadableDepots helper consolidates filtering logic and correctly prioritizes 64-bit depots when available. The getMainAppDepots method 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 getDownloadableDepots correctly handles the case where DLC depots are placed inside separate DLC apps (case 3 from PR description). The custom DepotInfo objects properly set dlcAppId to track which DLC each depot belongs to.


893-899: LGTM: Cascade deletion correctly cleans up indirect DLC data.

The updated deleteApp properly 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:

  1. Removes the completed app from downloadingAppIds (line 1369 uses correct == comparison)
  2. Only triggers final completion when all apps are downloaded (line 1372)
  3. Sets markers and emits events only once (lines 1374-1383)

The use of CopyOnWriteArrayList provides 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 installDialogStates and uninstallDialogAppIds, 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 snapshotFlow to observe changes in the GameManagerDialog state, consistent with the existing installDialogState tracking 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 via StatFs) 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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:

  1. Breaking down the logic into smaller, well-named helper methods
  2. Adding inline comments explaining the business logic for each filter condition
  3. Renaming to something clearer like getDlcIdsWithoutManifests or getPlaceholderDlcIds
🔎 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 on mainAppDlcIds.

The logic modifying mainAppDlcIds in 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

📥 Commits

Reviewing files that changed from the base of the PR and between b9bca11 and cf4b27e.

📒 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 CopyOnWriteArrayList for downloadingAppIds is 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:

  1. Safely retrieves existing AppInfo (nullable)
  2. Merges depot lists using distinct() to avoid duplicates
  3. Updates or inserts as appropriate
  4. Uses sorted() for consistent ordering
  5. Thread-safely removes completed app ID from downloadingAppIds
  6. 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:

  1. Creates separate listeners for main app and each DLC
  2. Passes mainAppDlcIds only to the main app listener (for DB persistence)
  3. Uses empty list for DLC listeners since DLC-of-DLC is not supported (matches PR description)
  4. Creates proper depot-to-index mappings for progress tracking
  5. 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:

  1. DLCs without proper depots: Line 1102-1106 explicitly checks depot.manifests.isNotEmpty(), which filters OUT the improper depots mentioned in getMainAppDlcIdsWithoutProperDepotDlcIds (those with empty manifests on OS.none). This is correct defensive programming.

  2. 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.

  3. Downloaded depots: mainAppDepots correctly filters out downloadedDepots at line 1116. For dlcAppDepots, the source getDownloadableDlcAppsOf() 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 installedDlcIds check visible in GameManagerDialog.kt (line 124) and only delete DLC data if it's neither independently owned nor installed elsewhere.

Comment on lines 1371 to 1385
// 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)
}
}
Copy link
Owner

Choose a reason for hiding this comment

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

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

Copy link
Owner

Choose a reason for hiding this comment

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

image

I don't know if it's from the code changes or from JavaSteam - but it looks broken. onDepotDownloaded isn't triggering.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 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.

🔎 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 CoilImage lacks a contentDescription for screen readers. For decorative images, explicitly mark as null; 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 throw NoSuchElementException if the collection is empty. Since SteamService.getDownloadableDepots(gameId) can return an empty map when getAppInfoOf(appId) returns null (SteamService:604), this crash is possible. Add a guard to check if downloadable depots exist before calling .first(), or use firstOrNull() with appropriate error handling.

app/src/main/java/app/gamenative/utils/SteamUtils.kt (1)

787-793: Potential NullPointerException on appInfo!!.

appInfo is nullable (returned from getAppInfoOf(steamAppId) at line 739), but line 790 uses force unwrap appInfo!! 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 <= 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, this should be == 0 or use none { } 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 CopyOnWriteArrayList provides thread-safe modifications, the sequence of removeIf followed by isEmpty() 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

📥 Commits

Reviewing files that changed from the base of the PR and between cf4b27e and 407a7de.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/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.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • 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/utils/SteamUtils.kt
  • 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/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/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.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • 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)
🪛 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 ManageGameContent enum 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.txt when 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 getInstallSizeInfo aggregation 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 -1 adjustment 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 runBlocking is acceptable here per project learnings, given these were converted from synchronous implementations.

Based on learnings, runBlocking is 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 filterForDownloadableDepots function 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 DepotInfo reconstruction correctly reassigns dlcAppId to 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 CopyOnWriteArrayList for thread-safe downloadingAppIds tracking
  • Creates isolated AppDownloadListener instances 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 downloadingAppIds using correct predicate
  • Defers final cleanup until all apps complete (downloadingAppIds.isEmpty())
  • Updates or inserts AppInfo appropriately based on installation state

The fixes from previous reviews (null safety at line 1337, correct removeIf predicate 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 new downloadingAppIds list when a download is already active, eliminating the duplicate concern. When resuming a download, the existing DownloadInfo object 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 main appId only 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>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 currentDownloadInfo which is passed to the second overload, but line 1090 returns early before using the dlcAppIds parameter. 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 mainDepots twice 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 returns null without 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

📥 Commits

Reviewing files that changed from the base of the PR and between 887d1ab and e8c95c6.

📒 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, and CopyOnWriteArrayList are 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, and getInstalledDlcDepotsOf properly 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 filterForDownloadableDepots is 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 DepotInfo objects with the proper dlcAppId ensures 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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.

  1. Line 1338 uses runBlocking which blocks the calling coroutine thread. A past review suggested converting to a suspend function with withContext(Dispatchers.IO). Since this is called from within the download job coroutine, blocking could impact performance or cause issues depending on the dispatcher.

  2. Lines 1367-1370 have a potential race condition. If multiple DLC completions occur concurrently:

    • Line 1367: removeIf and line 1370: isEmpty() are not atomic
    • CopyOnWriteArrayList doesn't make these operations atomic together
    • Multiple threads could all see !isEmpty() and skip adding completion markers, or multiple threads could all see isEmpty() and add markers multiple times

Recommended fixes:

🔎 Proposed improvements
  1. 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
     }
  1. 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 completeAppDownload call succeeds (updating DB, removing from downloadingAppIds) but a later call fails, the download will be partially completed:

  • Some apps marked as downloaded in the database
  • downloadingAppIds partially 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?.downloadingAppIds already includes the main appId. This list is passed as dlcAppIds to the overload at line 910, then at lines 1119-1120 it's copied to downloadingAppIds, and finally at line 1125 the main appId is added again, resulting in a duplicate.

A past review flagged this (supposedly addressed in commit 887a031), but the issue remains. Consider either:

  1. Filter out appId from downloadingAppIds before passing it:
    return downloadApp(appId, currentDownloadInfo?.downloadingAppIds?.filter { it != appId } ?: emptyList())
  2. Store selected DLC IDs separately in DownloadInfo to avoid ambiguity.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)

490-512: Consider returning immutable List<Int> instead of MutableList<Int>.

The function constructs and returns a MutableList<Int>, but callers don't appear to require mutability (line 1129 only reads from it). Returning List<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

📥 Commits

Reviewing files that changed from the base of the PR and between e8c95c6 and 86a1982.

📒 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, and CopyOnWriteArrayList are 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 like getAppInfoOf. 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 nullable AppInfo returned by getInstalledApp, 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 null for ownedDlc when filtering indirect DLC depots (line 624) correctly skips the ownership check, as these DLCs are already validated by getDownloadableDlcAppsOf.


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 AppItem instances 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 of downloadInfo.gameId.

The change from item.appId to downloadInfo.gameId correctly identifies the main app's download job to remove on failure, since the job is keyed by the main app ID.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 completeAppDownload sequentially 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:

  1. Some apps marked completed in DB with their IDs removed from downloadingAppIds
  2. Later apps not completed
  3. The downloadingAppIds.isEmpty() check (line 1371) never succeeds
  4. 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 completeAppDownload update 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca6cfb and 7d1ffc4.

📒 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 runBlocking is 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 dlcAppId to 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 mainAppDlcIds for pattern 2 DLCs is also correct.


1225-1226: LGTM on autoStartDownload = false!

Setting autoStartDownload = false correctly 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 depotIdToIndex mapping 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 invokeOnCompletion handler 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 onDownloadFailed handler correctly removes the download job and shows a user-facing error message. Using downloadInfo.gameId ensures the correct job is removed.


1371-1381: No race condition concern: completeAppDownload is called sequentially from a single coroutine.

completeAppDownload is only invoked at lines 1292 and 1299, both sequentially within the same try-catch block after depotDownloader.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 to removeIf on the same downloadingAppIds instance cannot occur, eliminating the race condition scenario described.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 CoilImage lacks a contentDescription for screen readers. For decorative images, explicitly set it to null.

🔎 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:

  1. The effect runs when visible becomes false as well, clearing state unnecessarily. Add an early return guard.
  2. Line 131 uses .first() which throws if downloadableDepots is 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 on appInfo!! 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 <= 1 on 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 in downloadingAppIds.

This is a duplicate of past review comments. Lines 1118-1126 construct downloadingAppIds by:

  1. Adding all dlcAppIds (line 1119)
  2. Adding appId if main app has depots (line 1125)

When resuming (line 907), dlcAppIds comes from currentDownloadInfo?.downloadingAppIds which already contains the main app ID, causing duplication. This breaks completion tracking at line 1371 where the code checks downloadingAppIds.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 completeAppDownload sequentially 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
  • downloadingAppIds is 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 passes currentDownloadInfo?.downloadingAppIds as the dlcAppIds parameter (line 907). At line 1119, downloadingAppIds.addAll(dlcAppIds) adds all passed app IDs, then at line 1125, the main appId is added again if mainAppDepots.isNotEmpty(). If the existing downloadingAppIds already contains the main app ID, it will be duplicated in the tracking list. Add deduplication logic (e.g., toSet()) or filter out appId from dlcAppIds before 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 InstallSizeInfo data class is duplicated from SteamAppScreen.kt (lines 58-64). Consider extracting it to a shared location (e.g., a data package) 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 runBlocking at 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 appInfo without 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 completeAppDownload to a suspend function and replace runBlocking with 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, runBlocking is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 7d1ffc4 and 73c6391.

📒 Files selected for processing (17)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt
  • app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml
  • gradle/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.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/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.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • 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/utils/SteamUtils.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • 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/utils/SteamUtils.kt
  • 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/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_prompt for 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 ManageGameContent enum 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 installDialogStates and uninstallDialogAppIds, 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 GameManagerDialog for installed games, allowing users to manage DLC content post-installation.


857-866: LGTM!

The dialog state observation follows the same snapshotFlow pattern used for installDialogState, maintaining consistency.


1210-1240: LGTM!

The dialog integration correctly handles installation by:

  1. Hiding the dialog first
  2. Removing DRM markers if the app is already installed
  3. Capturing analytics
  4. Launching the download on an IO dispatcher

The use of CoroutineScope(Dispatchers.IO).launch is 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 installButtonEnabled logic 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 gameId and downloadingAppIds enables per-game and per-DLC download tracking. Using CopyOnWriteArrayList is appropriate here since downloadingAppIds is 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 getInstalledDepots to getInstalledApp better reflects that the method returns the full AppInfo object, not just depot IDs. This aligns with the usage pattern in SteamService.kt where callers extract various fields from the returned AppInfo.

app/src/main/java/app/gamenative/utils/SteamUtils.kt (3)

736-742: LGTM! Clean depots.txt recreation ensures consistency.

Deleting the existing depots.txt and 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 getInstalledDepotsOf returns 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.ini configuration accurately reflects installed content.


1036-1038: LGTM! Defensive error handling for external API call.

Wrapping the fetchDirect3DMajor callback 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:

  • getDownloadableDlcAppsOf fetches DLC apps from database
  • getInstalledApp returns full AppInfo (replacing old getInstalledDepots)
  • getInstalledDepotsOf and getInstalledDlcDepotsOf derive data from getInstalledApp

Based on learnings, using runBlocking to 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 dlcAppId associations (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 filterForDownloadableDepots function 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)

getMainAppDepots correctly applies this filter to main app depots only, establishing a clear separation from the full getDownloadableDepots that also includes indirect DLC depots.


603-645: LGTM! Extended depot resolution for indirect DLCs.

The enhanced getDownloadableDepots now 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 DepotInfo objects with dlcAppId set 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 DownloadInfo with gameId and downloadingAppIds for 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 AppDownloadListener instances 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 AppDownloadListener constructor has been streamlined to only accept downloadInfo and depotIdToIndex, 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.gameId for job removal on failure (line 1408)

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
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the current state of gradle/libs.versions.toml
head -n 80 gradle/libs.versions.toml

Repository: 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 -i

Repository: utkarshdalal/GameNative

Length of output: 94


🏁 Script executed:

# Search for app/build.gradle.kts and check its contents
fd -e "build.gradle.kts" | head -20

Repository: 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 2

Repository: 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 -5

Repository: 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 kts

Repository: utkarshdalal/GameNative

Length of output: 94


🏁 Script executed:

# Find build.gradle.kts files properly
find . -name "build.gradle.kts" -type f

Repository: 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 3

Repository: 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 -5

Repository: 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 -40

Repository: utkarshdalal/GameNative

Length of output: 49


Update the javasteam version declaration and comment to reflect the library migration.

Three issues need to be addressed:

  1. 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 uses io.github.utkarshdalal. Update the comment to reflect the new source or remove it if no longer applicable.

  2. Version downgrade to 1.8.0-SNAPSHOT: The version was downgraded from 1.8.1-SNAPSHOT (currently referenced in app/build.gradle.kts localBuild) to 1.8.0-SNAPSHOT. Confirm this downgrade is intentional for the forked library.

  3. Version discrepancy with app/build.gradle.kts: The localBuild conditional in app/build.gradle.kts references 1.8.1-SNAPSHOT jars, while gradle/libs.versions.toml now declares 1.8.0-SNAPSHOT. When localBuild = false (default), the new version will be used instead. Ensure this transition is intentional and update app/build.gradle.kts if 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.

Comment on lines +72 to +75
#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" }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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 2

Repository: 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 java

Repository: 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 -100

Repository: 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.utkarshdalal fork maintains the in.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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 completeAppDownload sequentially 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, dlcAppIds contains currentDownloadInfo?.downloadingAppIds, which already includes the main appId. The code then:

  1. Adds all dlcAppIds to downloadingAppIds at line 1119 (including the main app ID)
  2. Adds appId again at line 1125 if main depots exist

This 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 getMainAppDlcIdsWithoutProperDepotDlcIds implements 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 for OS.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 runBlocking at line 1335 to perform database operations. While learnings indicate runBlocking is acceptable for public APIs maintaining synchronous contracts, this is a private function called from within a coroutine context (the download job).

Using runBlocking here:

  • 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 runBlocking is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 73c6391 and ad0b97d.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • gradle/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: MarkerUtils and Marker for completion signaling, and CopyOnWriteArrayList for thread-safe tracking of downloading app IDs.

Also applies to: 147-147


551-582: LGTM: Well-structured depot filtering logic.

The filterForDownloadableDepots function 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 AppItem and AppDownloadListener instances 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 AppDownloadListener has a cleaner constructor signature and properly delegates completion handling to the separate completeAppDownload function. Progress tracking logic correctly handles cumulative bytes and deltas.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between ad0b97d and b74dd9f.

📒 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

Comment on lines +72 to +73
javasteam = { group = "io.github.joshuatam", name = "javasteam", version.ref = "javasteam" }
javasteam-depotdownloader = { group = "io.github.joshuatam", name = "javasteam-depotdownloader", version.ref = "javasteam" }
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 6, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, check the context in gradle/libs.versions.toml
head -80 gradle/libs.versions.toml | tail -15

Repository: 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 -n

Repository: utkarshdalal/GameNative

Length of output: 49


🏁 Script executed:

# Search for ClassForName references related to javasteam
rg "Class\.forName.*javasteam" --type kotlin --type java -n

Repository: utkarshdalal/GameNative

Length of output: 212


🏁 Script executed:

# Check settings.gradle.kts for repository configuration
cat settings.gradle.kts | head -50

Repository: utkarshdalal/GameNative

Length of output: 722


🏁 Script executed:

# Check build.gradle.kts files for javasteam version references
rg "javasteam" --type gradle --type kts -B2 -A2

Repository: 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 -n

Repository: utkarshdalal/GameNative

Length of output: 212


🏁 Script executed:

# Check if there are other references to javasteam
rg "javasteam" --type kotlin --type java -C2

Repository: 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.kt

Repository: 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 -100

Repository: 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:

  1. Compilation failures on all imported classes
  2. 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).

Copy link
Owner

Choose a reason for hiding this comment

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

it works chill

Copy link
Contributor

Choose a reason for hiding this comment

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

@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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 null silently 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 runBlocking to execute database operations within a coroutine context. While a past learning indicates runBlocking is acceptable for maintaining synchronous public APIs, this is a private function called from within a coroutine (the download job). Using runBlocking here:

  1. Blocks the calling coroutine thread
  2. Could cause deadlocks depending on the dispatcher
  3. 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 runBlocking is 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

📥 Commits

Reviewing files that changed from the base of the PR and between b74dd9f and 75b6f9b.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • gradle/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 for getMainAppDlcIdsWithoutProperDepotDlcIds and 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 completeAppDownload sequentially within the same coroutine job. Each download job maintains its own independent DownloadInfo object, so there is no concurrent modification of downloadingAppIds. The removeIf operation at line 1368 executes sequentially on a single thread per job, and CopyOnWriteArrayList provides thread-safe iteration semantics. The AppDownloadListener does not modify downloadingAppIds, only reads from downloadInfo. No race condition exists.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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.

getInstalledDlcDepotsOf returns values from AppInfo.dlcDepots, which actually stores DLC app IDs (see line 1351 where selectedDlcAppIds is stored). Consider renaming to getInstalledDlcAppIdsOf for 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 dlcDepots field in AppInfo to dlcAppIds if 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

📥 Commits

Reviewing files that changed from the base of the PR and between dc867e2 and 86f45ad.

📒 Files selected for processing (3)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • gradle/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 CopyOnWriteArrayList import addresses the previously flagged race condition on downloadingAppIds, 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 dlcAppId values. The force unwrap at line 501 is safe due to the checkMap.size == 1 guard.


551-597: LGTM: Well-structured depot filtering logic.

The filterForDownloadableDepots function 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 DepotInfo objects with the proper dlcAppId assignment 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 downloadInfo and depotIdToIndex parameters. 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 = false with manual finishAdding() and startDownloading() 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 CopyOnWriteArrayList for 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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 86f45ad and 87fb550.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt
  • app/src/main/java/app/gamenative/db/PluviaDatabase.kt
  • app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt
  • app/src/main/java/app/gamenative/di/DatabaseModule.kt
  • 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: 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.kt
  • app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt
  • app/src/main/java/app/gamenative/db/PluviaDatabase.kt
  • app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt
  • app/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.kt
  • 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 (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 gameId property 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 core cancel(message: String) handles persistence, cleanup, and job cancellation correctly.

app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt (2)

12-13: TypeConverter for List<Int> is already properly implemented.

A TypeConverter for List<Int> exists in LicenseConverter and is registered in PluviaDatabase. The implementation uses JSON serialization via toIntList()/fromIntList() methods, which correctly handles the conversion between List<Int> and SQLite-compatible strings.


7-14: The current implementation is correct. DownloadingAppInfo is properly cleared during logout via downloadingAppInfoDao.deleteAll() in clearDatabase(), while AppInfo (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. CopyOnWriteArrayList is an appropriate choice for thread-safe concurrent access to downloadingAppIds.

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 use runBlocking to maintain synchronous contracts, which is acceptable for public APIs per learnings. The refactoring of getInstalledDepotsOf and getInstalledDlcDepotsOf to use getInstalledApp reduces duplication and improves maintainability.

Based on learnings, runBlocking is 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:

  • getMainAppDlcIdsWithoutProperDepotDlcIds correctly identifies DLCs that lack proper depot DLC IDs by checking for single-depot entries with no OS and no manifests
  • filterForDownloadableDepots consolidates filtering logic (manifest, OS, architecture, DLC ownership, language) into a reusable function, reducing duplication
  • getMainAppDepots appropriately 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:

  • getDownloadableDepots now includes depots from indirect DLC apps, correctly setting dlcAppId for each depot
  • deleteApp cascades deletion to indirect DLC apps, ensuring database consistency when removing a game

Also applies to: 898-905


2029-2029: Verify downloading app info cleanup aligns with resume semantics.

Line 2029 clears downloadingAppInfoDao during 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—downloadingAppInfoDao is user-specific download state, while appInfoDao tracks installation state. Based on learnings, appInfoDao is 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 runBlocking at lines 1111-1118 to insert downloading app info can cause performance issues or deadlocks, as this is called from various contexts including coroutine contexts. The downloadApp function is already executed within coroutine contexts (called from UI or other async code).

🔎 Recommended approach

Option 1: Convert the entire downloadApp function 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 of runBlocking (though this still blocks the caller).

Then update callers to handle the suspend function properly.

Based on learnings, runBlocking is 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>,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Comment on lines +40 to +42
DownloadingAppInfo::class
],
version = 8,
version = 9,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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:

  1. The method name is quite long (41 characters). Consider shortening to getMainAppDlcIdsWithoutDepotInfo or similar.
  2. 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 overloaded downloadApp function, where the parameter is named dlcAppIds (line 1107, shown as userSelectedDlcAppIds). 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 selectedAppIds or appIdsToDownload to 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 ownedDlc map (line 625), while indirect DLC depots are filtered with null (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

📥 Commits

Reviewing files that changed from the base of the PR and between 87fb550 and 3133fde.

📒 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 runBlocking is 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 DownloadingAppInfo before 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 of runBlocking is acceptable here per project learnings.


1155-1186: Well-structured depot filtering and app ID construction.

The logic correctly:

  1. Separates main app depots from DLC app depots
  2. Filters out already-downloaded content
  3. Builds downloadingAppIds based on which apps actually have content to download (avoiding the duplicate main app ID issue from past reviews)
  4. Uses CopyOnWriteArrayList for thread safety

The check at line 1186 is necessary because after filtering for already-downloaded content (line 1149), selectedDepots could be empty even if downloadableDepots wasn't.


1268-1310: Good control over download lifecycle.

Setting autoStartDownload = false (line 1269) and manually calling startDownloading() (line 1310) after adding all AppItems 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 DownloadingAppInfo is 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() to clearDatabase() is correct. Unlike the AppInfo table (which tracks installed games and should persist across logins per project learnings), DownloadingAppInfo tracks in-progress downloads which are user-session-specific and should be cleared on logout.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3133fde and fb7e9c6.

📒 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 dlcAppId to 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, calling finishAdding(), then startDownloading() 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 completeAppDownload throws 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
  • downloadingAppInfo remains 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:

  1. Main app complete, DLC incomplete
  2. Some DLC complete, others incomplete

1404-1407: LGTM: CopyOnWriteArrayList provides adequate thread safety.

The use of CopyOnWriteArrayList at line 1155 ensures that concurrent removeIf operations at line 1404 are thread-safe. Even if multiple DLC completions race to check isEmpty() 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 downloadingAppInfo from 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.

Comment on lines +1110 to +1118
// Save downloading app info
runBlocking {
instance?.downloadingAppInfoDao?.insert(
DownloadingAppInfo(
appId,
dlcAppIds = userSelectedDlcAppIds
),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 DownloadingAppInfo for the main app, but lines 905-910 don't delete DownloadingAppInfo for 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb7e9c6 and 3c888cc.

📒 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 DownloadingAppInfo first 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 runBlocking is acceptable here for maintaining synchronous public API contracts.

Based on learnings, runBlocking is appropriate for public APIs maintaining synchronous contracts.


617-659: LGTM!

The extended getDownloadableDepots correctly implements support for DLC pattern (3) where DLC depots are placed inside separate DLC app entries. The depot merging logic properly sets the dlcAppId field 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).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 getMainAppDlcIdsWithoutProperDepotDlcIds function 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 == 1 matters
  • The significance of OS.none with 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3c888cc and c4d0f37.

📒 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 CopyOnWriteArrayList import is particularly good for the thread-safe downloadingAppIds tracking, and the DownloadingAppInfoDao injection 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 downloadingAppInfoDao check 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 filterForDownloadableDepots function 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 getMainAppDepots function 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 getDownloadableDepots now correctly handles "Pattern 3" from the PR description—DLC depots placed inside another DLC app. The logic:

  1. Filters main app depots using the centralized filter
  2. Discovers indirect DLC apps via getDownloadableDlcAppsOf
  3. Includes their depots with proper dlcAppId attribution

This is a well-structured enhancement that maintains the existing API while expanding functionality.


903-910: LGTM! Proper cascade deletion of DLC apps.

The enhanced deleteApp function correctly:

  1. Cleans up downloadingAppInfo when deleting an app
  2. 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 onDownloadFailed callback correctly:

  1. Marks the download as failed
  2. Cleans up the persisted downloadingAppInfo
  3. Removes the download job
  4. Notifies the user

This prevents orphaned state and provides user feedback.


2037-2037: LGTM! Database cleanup includes downloading app info.

Adding downloadingAppInfoDao.deleteAll() to clearDatabase ensures 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 as dlcAppIds during resume, the subsequent call to the three-parameter downloadApp() reconstructs downloadingAppIds from filtered depot data at lines 1153–1180. The main app ID is added only when mainAppDepots.isNotEmpty() (line 1165), and DLC app IDs are added only when matching depots exist in dlcAppDepots (lines 1156–1160). Since main app depots are identified by dlcAppId == 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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: Missing DownloadingAppInfo cleanup 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 DownloadingAppInfo from 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, return List<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

📥 Commits

Reviewing files that changed from the base of the PR and between c4d0f37 and 81af1a7.

📒 Files selected for processing (2)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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.kt
  • app/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.kt
  • 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 (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 DownloadingAppInfoDao injection follows the established pattern in this service and properly supports the new download state persistence functionality.


289-297: LGTM!

The updated hasPartialDownload logic correctly prioritizes checking the database for DownloadingAppInfo before 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 runBlocking usage is consistent with the codebase conventions (based on learnings, runBlocking is 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 getDownloadableDepots correctly handles both main app depots and indirect DLC depots, properly tracking DLC ownership via the modified dlcAppId field.


893-918: LGTM!

The deletion logic correctly cleans up the new DownloadingAppInfo data and associated DLC app data within a transaction.


920-943: LGTM!

The resume/verify logic correctly distinguishes between:

  1. Active downloads (from downloadJobs)
  2. Persisted downloads (from downloadingAppInfo)
  3. Verify operations (rebuilding DLC list from installed apps)

The isUpdateOrVerify flag properly differentiates initial downloads from update/verify operations.


945-955: LGTM!

Clean wrapper function that properly forwards the isUpdateOrVerify flag.


1270-1280: LGTM!

Setting autoStartDownload = false and calling startDownloading() 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 appInfo gracefully (no force unwrap)
  • Uses CopyOnWriteArrayList.removeIf for thread-safe removal
  • Adds completion markers only when all downloads finish

The runBlocking at 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:

  1. Marks the download as failed
  2. Cleans up the DownloadingAppInfo from DB
  3. Removes the download job
  4. Shows a user-friendly toast message

2039-2039: LGTM!

Correctly clears downloadingAppInfoDao during 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 feature

The imports for GameManagerDialog and GameManagerDialogState are necessary for the new game manager functionality.


135-147: LGTM - Consistent pattern for dialog state management

The companion object methods for managing game manager dialog states follow the same pattern as the existing installDialogStates and uninstallDialogAppIds. The implementation is consistent and appropriate.


454-462: LGTM - Install flow now routes through Game Manager

The 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 content

The new ManageGameContent menu option provides a convenient way for users to manage DLCs and content for already-installed games.


857-866: LGTM - Consistent state synchronization pattern

The LaunchedEffect for 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 = false is 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 to onInstall contain only new content. Setting isUpdateOrVerify = false ensures 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 81af1a7 and dec0261.

📒 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 DownloadingAppInfo entity and DownloadingAppInfoDao are 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,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between dec0261 and 98a37a0.

📒 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 DownloadingAppInfo entity and DownloadingAppInfoDao are necessary for the new download state persistence feature.

Also applies to: 26-26


40-40: LGTM! Entity registration is correct.

The DownloadingAppInfo entity is properly registered in the Room database.


68-69: LGTM! DAO accessor is correctly implemented.

The abstract accessor for DownloadingAppInfoDao follows Room conventions and will be properly exposed through the DI layer.

DownloadingAppInfo::class
],
version = 8,
version = 11,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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:

  1. Add migrations for v8→v9, v9→v10, and v10→v11 (or a direct v8→v11 migration) to this PR
  2. Ensure they are registered in DatabaseModule.kt using .addMigrations(...)
  3. 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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt (1)

4-4: Unused import.

LibraryItem is imported but never used in this file. Consider removing it.

🔎 Proposed fix
 import androidx.compose.runtime.saveable.mapSaver
-import app.gamenative.data.LibraryItem
gradle/libs.versions.toml (1)

14-14: Consider updating the comment URL.

The comment still references https://mvnrepository.com/artifact/in.dragonbra/javasteam but the library group has been changed to io.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.

ActivityNavigator is imported but never used in this file.

🔎 Proposed fix
-import androidx.navigation.ActivityNavigator
app/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

📥 Commits

Reviewing files that changed from the base of the PR and between 98a37a0 and 267e150.

📒 Files selected for processing (21)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/data/DownloadingAppInfo.kt
  • app/src/main/java/app/gamenative/db/PluviaDatabase.kt
  • app/src/main/java/app/gamenative/db/dao/AppInfoDao.kt
  • app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/di/DatabaseModule.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/state/GameManagerDialogState.kt
  • app/src/main/java/app/gamenative/ui/enums/AppOptionMenuType.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/utils/SteamUtils.kt
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml
  • gradle/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.toml
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • 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:

  • gradle/libs.versions.toml
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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.toml
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • 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/ui/component/dialog/GameManagerDialog.kt
  • app/src/main/java/app/gamenative/ui/screen/library/appscreen/SteamAppScreen.kt
  • app/src/main/java/app/gamenative/db/dao/SteamAppDao.kt
  • app/src/main/java/app/gamenative/db/dao/DownloadingAppInfoDao.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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 GameManagerDialogState data class and its Saver are correctly implemented for Compose state persistence. The mapSaver pattern 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.REPLACE is 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:

  1. Installed DLC depots (sorted)
  2. 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 installDialogStates and uninstallDialogAppIds. 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 showGameManagerDialog for 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 ManageGameContent menu option provides access to the Game Manager dialog for installed games, allowing users to modify their DLC selections post-installation.


857-866: LGTM!

The snapshotFlow pattern 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 GameManagerDialog integration 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.IO for the download operation

One minor note: CoroutineScope(Dispatchers.IO).launch creates an unstructured coroutine. Consider using rememberCoroutineScope() (already available as scope at line 869) for better lifecycle management, though the current approach is consistent with existing patterns in this file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 it suspend and using withContext(Dispatchers.IO) for the DB operations instead of runBlocking.

Based on learnings, runBlocking is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 267e150 and 30b0aa6.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/data/DownloadInfo.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/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.kt
  • 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 (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, and CopyOnWriteArrayList are 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 runBlocking to maintain synchronous API contracts, consistent with the existing getAppInfoOf function and acceptable per learnings for public APIs.


452-462: LGTM: Centralized AppInfo retrieval reduces duplication.

The refactoring to use getInstalledApp as a central accessor for both depot lists improves maintainability.


565-659: LGTM: Well-factored depot filtering and DLC merging.

The extraction of filterForDownloadableDepots eliminates 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:

  1. Resuming active downloads from memory
  2. Resuming interrupted downloads from DB
  3. Verifying already-installed games

The isUpdateOrVerify flag 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 CopyOnWriteArrayList for thread-safe downloadingAppIds
  • Sets autoStartDownload=false to allow configuration before starting
  • Creates separate AppItem instances 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 downloadingAppInfo entry

A 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 downloadingAppInfo on logout is appropriate, while preserving the AppInfo table 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>())
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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 -A10

Repository: utkarshdalal/GameNative

Length of output: 18549


🏁 Script executed:

cat -n app/src/main/java/app/gamenative/data/DownloadInfo.kt

Repository: 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 gameId is hardcoded to 0, even though the function receives a GOG game ID (String) on line 298
  • The downloadingAppIds is 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 gameId is semantically incorrect for this context

These fields appear to be Steam-specific additions that don't apply to GOG's architecture. Consider either:

  1. Making these parameters optional for non-Steam services, or
  2. 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).

Comment on lines +504 to +526
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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 verified checkMap.size == 1, use checkMap.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 main appId. While the downstream code at lines 1158-1163 implicitly filters it out (since dlcAppDepots won'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, userSelectedDlcAppIds may contain the main appId (from currentDownloadInfo.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). While runBlocking is acceptable for maintaining synchronous APIs per project learnings, using it inside an existing coroutine blocks the thread unnecessarily. Converting to a suspend function with withContext(Dispatchers.IO) would be more idiomatic.

Based on learnings, runBlocking is 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 null silently when selectedDepots.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

📥 Commits

Reviewing files that changed from the base of the PR and between 30b0aa6 and 9fddd01.

📒 Files selected for processing (3)
  • app/build.gradle.kts
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • gradle/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 filterForDownloadableDepots function is well-structured with clear, sequential filtering criteria. The helper function improves code reuse across getMainAppDepots and getDownloadableDepots.


625-659: LGTM!

The extension to include indirect DLC depots is well-implemented. Creating new DepotInfo objects with the correct dlcAppId ensures proper tracking during download completion.


1452-1471: LGTM!

The failure handling correctly cleans up the downloadingAppInfo state and notifies the user. Progress persistence on chunk/depot completion ensures reliable resume capability.


2036-2049: LGTM!

Correctly clears downloadingAppInfoDao during database clear. Per project learnings, AppInfo (installation state) is preserved across logout, but DownloadingAppInfo (download progress state) should be cleared as it's user-session-specific.

Comment on lines +47 to +60
@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 || ',%') " +
Copy link
Owner

Choose a reason for hiding this comment

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

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?

Comment on lines +1375 to +1429
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)
}
}

Copy link
Owner

Choose a reason for hiding this comment

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

Should we move this back into onDownloadCompleted and remove this function?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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).launch creates 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 using rememberCoroutineScope() for consistency with other parts of the codebase (note: scope is 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 duplicate InstallSizeInfo class.

This data class is identical to the one defined in SteamAppScreen.kt (lines 58-64). Consider moving it to a shared location (e.g., a data package) 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 false

This 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9fddd01 and 1906ee2.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/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.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • app/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.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GameManagerDialog.kt
  • 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/ui/component/dialog/GameManagerDialog.kt
  • 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 (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 functions buildInstallPromptState and buildNotEnoughSpaceState (lines 66-96) are still actively used in the codebase. They are called at lines 978-980 within a separate LaunchedEffect that handles the DialogType.INSTALL_APP_PENDING state. The codebase currently maintains two install flows: the legacy approach using installDialogState with these helper functions, and the new GameManagerDialogState approach 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 @Composable for onGetDisplayInfo allows 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) > 0 could be simplified to realSelectedAppIds.size > 1 for 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 fakeAppInfo helper with its built-in debug guard.

app/src/main/java/app/gamenative/service/SteamService.kt (12)

179-180: LGTM!

The injection of DownloadingAppInfoDao follows 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 DownloadingAppInfo before falling back to marker file checks.


440-462: LGTM!

The new accessor methods follow existing patterns in this file (e.g., getAppInfoOf at Line 436) and use runBlocking to maintain synchronous APIs, which is acceptable per project conventions.


598-611: LGTM!

The getMainAppDepots method correctly filters main app depots using the same criteria as getDownloadableDepots, providing a focused view of just the base game depots without DLC content.


1331-1349: LGTM!

The completion flow correctly:

  1. Completes downloads for main app and each DLC app separately (Lines 1331-1341)
  2. Removes the download job (Line 1344)
  3. 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:

  1. Persists progress for resume support (Line 1352)
  2. Marks all depots as failed (Lines 1354-1357)
  3. Removes the download job (Line 1358)
  4. Intentionally preserves DownloadingAppInfo in the database for resume support

1375-1427: LGTM! Method correctly handles per-app completion.

The completeAppDownload method properly:

  1. Updates or inserts AppInfo with downloaded depot IDs (Lines 1388-1408)
  2. Tracks completion by removing the app from downloadingAppIds (Line 1412)
  3. Adds markers and emits events only when all apps complete (Lines 1415-1426)

Note: Line 1390 confirms that dlcDepots stores 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:

  1. Creates DepotDownloader with autoStartDownload = false
  2. Adds all app items (main and DLC) to the queue
  3. Calls finishAdding() to signal completion
  4. Explicitly calls startDownloading() to begin

This 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. The AppInfo.dlcDepots field stores DLC app IDs, as confirmed by lines 1390 and 1406 where selectedDlcAppIds (app IDs) are assigned directly to dlcDepots.

The logic correctly combines previously installed DLC app IDs with newly detected ones for verification. The only minor issue is misleading method/field naming: dlcDepots and getInstalledDlcDepotsOf() should be named dlcAppIds and getInstalledDlcAppIdsOf() respectively to avoid confusion about what data they actually contain.

Likely an incorrect or invalid review comment.


633-656: The override of dlcAppId at line 645 is intentional and necessary for handling indirect DLC apps. Manifest retrieval uses depot.depotId exclusively (not dlcAppId), and DLC ownership checks also rely on depot.depotId only. The dlcAppId field 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 app
  • EXISTS (SELECT * FROM steam_license WHERE license_type <> 0 AND app.id in license.app_ids) — validates that each DLC has valid ownership

Additionally, 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.

Comment on lines +130 to +136
allDownloadableApps.sortBy { it.first }

// Add Base Game
allDownloadableApps.add(0, Pair(gameId, downloadableDepots.toSortedMap().values.first()))
selectedAppIds[gameId] = true
enabledAppIds[gameId] = false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@utkarshdalal utkarshdalal merged commit 175e715 into utkarshdalal:master Jan 8, 2026
1 of 2 checks passed
@joshuatam joshuatam deleted the feat/game-manager branch January 11, 2026 07:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants