Skip to content

feat: add ContentProvider for querying installed games#751

Open
shahmir-k wants to merge 5 commits intoutkarshdalal:masterfrom
shahmir-k:feature/installed-games-content-provider
Open

feat: add ContentProvider for querying installed games#751
shahmir-k wants to merge 5 commits intoutkarshdalal:masterfrom
shahmir-k:feature/installed-games-content-provider

Conversation

@shahmir-k
Copy link

@shahmir-k shahmir-k commented Mar 7, 2026

Summary

  • Adds a read-only ContentProvider that exposes installed games to external apps
  • Enables companion apps (game launchers, shortcut generators, automation tools) to discover which games are installed in GameNative
  • No permission required — game metadata (names, IDs, icon hashes) is not sensitive, consistent with the already-exported LAUNCH_GAME intent

Details

Authority: app.gamenative.games
URI: content://app.gamenative.games/installed

Column Type Description
app_id INTEGER Numeric game ID
name TEXT Game name
game_source TEXT STEAM, GOG, EPIC, or AMAZON
icon_hash TEXT Icon hash or URL for constructing image URLs
is_installed INTEGER Whether game files are downloaded (1 = yes)

Game sources queried:

  • Steamsteam_app INNER JOIN app_info where is_downloaded = 1
  • GOGgog_games where is_installed = 1
  • Epicepic_games where is_installed = 1 (excludes DLC)
  • Amazonamazon_games where is_installed = 1

Usage from an external app:

val cursor = contentResolver.query(
    Uri.parse("content://app.gamenative.games/installed"),
    null, null, null, null
)

Test plan

  • Install GameNative with this change
  • Log in and install a game from any source
  • Query the ContentProvider via adb shell content query --uri content://app.gamenative.games/installed
  • Verify the installed game appears with correct columns
  • Verify uninstalled games do not appear
  • Test from an external app that the cursor is readable without any permission

Closes #750

🤖 Generated with Claude Code


Summary by cubic

Adds a read-only ContentProvider for external apps to query installed games. Fulfills Linear #750.

  • New Features
    • URI: content://${applicationId}.games/installed (authority ${applicationId}.games, dynamic)
    • Columns: app_id, name, game_source, icon_ref, is_installed
    • Behavior: aggregates Steam/GOG/Epic/Amazon; excludes DLC and Steam Spacewar (id 480); exported, read-only; no permission; logs and skips non-numeric GOG IDs; isolates per-source errors and returns partial results

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

Summary by CodeRabbit

  • New Features
    • External apps can now discover and read a consolidated, read-only list of installed games aggregated from multiple platforms (Steam, GOG, Epic, Amazon).
    • The app exposes a standardized, queryable games feed so other apps can display or use your installed-games data without modifying it.

Add a read-only ContentProvider that exposes installed games to external
apps, enabling companion apps like game launchers and shortcut generators
to discover which games are available in GameNative.

Authority: app.gamenative.games
URI: content://app.gamenative.games/installed
Columns: app_id, name, game_source, icon_hash, is_installed

Supports Steam, GOG, Epic, and Amazon game sources.

Closes utkarshdalal#750

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Adds a read-only ContentProvider, InstalledGamesProvider, registered under authority ${applicationId}.games that exposes installed games at /installed. It queries the app Room database via a Hilt entry point, aggregates multiple sources into a MatrixCursor, and disallows insert/update/delete.

Changes

Cohort / File(s) Summary
Manifest
app/src/main/AndroidManifest.xml
Registers an exported provider android:name=".provider.InstalledGamesProvider" with android:authorities="${applicationId}.games".
ContentProvider Implementation
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt
Adds a read-only ContentProvider exposing content://<authority>/installed. Implements Uri matching, obtains PluviaDatabase via a Hilt EntryPoint, aggregates Steam/GOG/Epic/Amazon rows into a MatrixCursor with columns app_id, name, game_source, icon_ref, is_installed, sets notification URI, and throws on insert/update/delete.

Sequence Diagram(s)

sequenceDiagram
    participant ExternalApp as "External App"
    participant Provider as "InstalledGamesProvider"
    participant Matcher as "UriMatcher"
    participant HiltEP as "Hilt EntryPoint"
    participant DB as "PluviaDatabase / SQLite"

    ExternalApp->>Provider: query(content://{appId}.games/installed, projection)
    Provider->>Matcher: match(uri)
    Matcher-->>Provider: CODE_INSTALLED
    Provider->>HiltEP: obtain database()
    HiltEP-->>Provider: PluviaDatabase instance
    Provider->>DB: run source-specific queries (Steam, GOG, Epic, Amazon)
    DB-->>Provider: rows per source
    Provider->>Provider: build MatrixCursor (app_id, name, game_source, icon_ref, is_installed)
    Provider-->>ExternalApp: return Cursor
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped into manifests with glee,
Opened a window for others to see,
Steam, GOG, Epic, Amazon in line,
Rows in a cursor — read-only design,
No edits, just carrots of game marquee.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a ContentProvider for querying installed games, which is the primary objective of the PR.
Linked Issues check ✅ Passed The PR implements all core requirements from #750: read-only ContentProvider at authority app.gamenative.games/installed, exposes required columns (app_id, name, game_source, icon_ref, is_installed), aggregates from multiple sources (Steam, GOG, Epic, Amazon), and allows external apps to discover installed games.
Out of Scope Changes check ✅ Passed All changes are directly related to the ContentProvider feature: AndroidManifest.xml registers the provider, InstalledGamesProvider implements the core logic, and commit messages document error handling and Steam Spacewar filtering—all in scope.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link
Contributor

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

Choose a reason for hiding this comment

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

2 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt">

<violation number="1" location="app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:146">
P2: GOG game IDs are coerced to Int and non-numeric IDs are silently skipped, causing missing installed-game rows in the ContentProvider output.</violation>
</file>

<file name="app/src/main/AndroidManifest.xml">

<violation number="1" location="app/src/main/AndroidManifest.xml:140">
P1: Hardcoded ContentProvider authority can collide across build variants that use `applicationIdSuffix`, causing conflicting-provider install failures.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


db.query(sql).use { c ->
while (c.moveToNext()) {
val numericId = c.getString(0).toIntOrNull() ?: continue
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 7, 2026

Choose a reason for hiding this comment

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

P2: GOG game IDs are coerced to Int and non-numeric IDs are silently skipped, causing missing installed-game rows in the ContentProvider output.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt, line 146:

<comment>GOG game IDs are coerced to Int and non-numeric IDs are silently skipped, causing missing installed-game rows in the ContentProvider output.</comment>

<file context>
@@ -0,0 +1,204 @@
+
+        db.query(sql).use { c ->
+            while (c.moveToNext()) {
+                val numericId = c.getString(0).toIntOrNull() ?: continue
+                addRow(cursor, cols, numericId, c.getString(1), "GOG", c.getString(2) ?: "", c.getInt(3))
+            }
</file context>
Fix with Cubic

Copy link
Author

Choose a reason for hiding this comment

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

GOG product IDs are always numeric integers from the GOG API (https://embed.gog.com/user/data/games returns an "owned" array of integers). They're stored as strings internally but are converted to Int throughout the app — LibraryItem.gameId, GOGDownloadManager, and intent handling all use toIntOrNull(). Skipping unparseable IDs is a defensive guard, not a data loss path. The previous implementation used hashCode() as a fallback, which would produce IDs that break round-tripping with the LAUNCH_GAME intent — skipping is strictly safer.

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/AndroidManifest.xml`:
- Around line 137-141: The provider InstalledGamesProvider is exported without a
read permission; update its manifest declaration to add android:readPermission
(choose a protectionLevel like "signature" for first‑party only or a "dangerous"
permission you declare for opt‑in third parties) so queries to
InstalledGamesProvider require that permission; ensure the chosen permission is
declared in the manifest with the appropriate protectionLevel and update any
callers/tests to request/grant that permission as needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Around line 138-147: The icon_hash column is being populated with
source-specific URL/art strings (e.g., GOG using c.getString(2)) which makes the
public schema inconsistent; update InstalledGamesProvider (the block that builds
rows calling addRow with columns cols) to normalize icon_hash across providers:
either populate icon_hash with a canonical hash (e.g., compute a stable hash of
the icon URL/art) or leave icon_hash null/empty and add a separate
icon_url/icon_ref field in the cursor output, and update every addRow call for
GOG/Epic/Amazon (the calls around the shown block and the blocks at 155-163 and
171-179) to use the chosen normalized value instead of passing raw URLs.
- Around line 87-89: The catch in InstalledGamesProvider currently logs an
exception and returns the cursor, hiding failures; instead, in the catch block
(in InstalledGamesProvider where you currently "catch (e: Exception) {
Timber.e...; return cursor }") close any opened resources (e.g., close the
cursor if non-null) and then rethrow the original exception (or throw a wrapped
RuntimeException) so the query fails visibly to callers rather than returning a
potentially misleading empty/partial cursor.
- Around line 82-86: The export fan-out in InstalledGamesProvider currently
calls addSteamGames, addGOGGames, addEpicGames, and addAmazonGames but omits
locally added CUSTOM_GAME titles; update the merge to include custom/local game
rows by calling the function that handles custom titles (e.g., addCustomGames or
addLocalCustomGames) after the other adds (or implement that function if
missing) so CUSTOM_GAME rows are included in the exported feed and use the same
cursor/cols parameters as the other add* methods.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bd72b51-e633-4b76-97a0-ba520e60cc69

📥 Commits

Reviewing files that changed from the base of the PR and between c37289a and d6b1665.

📒 Files selected for processing (2)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt

Comment on lines +137 to +141
<!-- ContentProvider for external apps to query installed games -->
<provider
android:name=".provider.InstalledGamesProvider"
android:authorities="app.gamenative.games"
android:exported="true" />
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 7, 2026

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, find and examine the AndroidManifest.xml around the specified lines
find . -name "AndroidManifest.xml" -type f | head -5

Repository: utkarshdalal/GameNative

Length of output: 141


🏁 Script executed:

# Check the structure to understand the repository
ls -la

Repository: utkarshdalal/GameNative

Length of output: 1809


🏁 Script executed:

# Look for the provider implementation file
find . -path "*/provider/InstalledGamesProvider*" -type f

Repository: utkarshdalal/GameNative

Length of output: 136


🏁 Script executed:

# Read the AndroidManifest.xml around lines 137-141
cat -n app/src/main/AndroidManifest.xml | sed -n '130,150p'

Repository: utkarshdalal/GameNative

Length of output: 1062


🏁 Script executed:

# Read the InstalledGamesProvider implementation
cat -n app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt

Repository: utkarshdalal/GameNative

Length of output: 8184


🏁 Script executed:

# Search for any permission definitions related to READ_GAMES in the manifest
grep -n "permission" app/src/main/AndroidManifest.xml

Repository: utkarshdalal/GameNative

Length of output: 1176


Protect the exported provider with a read permission.

android:exported="true" with no android:readPermission lets any installed app query the complete list of installed games (Steam, GOG, Epic, Amazon). The InstalledGamesProvider.query() method has no permission checks and returns game names and install status without restrictions—this exposes sensitive user data that reveals gaming habits and interests.

Add android:readPermission to gate access, selecting a protection level that matches your intent: dangerous for opt-in third-party access or signature for first-party-only access.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/AndroidManifest.xml` around lines 137 - 141, The provider
InstalledGamesProvider is exported without a read permission; update its
manifest declaration to add android:readPermission (choose a protectionLevel
like "signature" for first‑party only or a "dangerous" permission you declare
for opt‑in third parties) so queries to InstalledGamesProvider require that
permission; ensure the chosen permission is declared in the manifest with the
appropriate protectionLevel and update any callers/tests to request/grant that
permission as needed.

Copy link
Author

Choose a reason for hiding this comment

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

Intentionally omitted. The data exposed is non-sensitive game metadata (names, IDs, icon hashes) — no credentials, tokens, or personal information. The existing LAUNCH_GAME intent is already exported with no permission gate. Adding a permission creates friction for third-party developers with no meaningful security benefit.

Copy link
Contributor

Choose a reason for hiding this comment

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

@shahmir-k, understood — the omission is intentional and your reasoning is sound. The LAUNCH_GAME intent precedent is a fair parallel (launching is a higher-privilege action than querying names/IDs), and the exposed columns contain no credentials or personal data. I'll note this for future reviews.

ʕ •`ᴥ•´ʔ ← thoughtful rabbit, noted!


✏️ Learnings added
Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/AndroidManifest.xml:137-141
Timestamp: 2026-03-08T00:45:59.484Z
Learning: In the GameNative (utkarshdalal/GameNative) repository, the `InstalledGamesProvider` (ContentProvider with authority `app.gamenative.games`) is intentionally exported without an `android:readPermission`. The design decision is deliberate: the data exposed is non-sensitive game metadata (names, IDs, icon hashes, install status), the existing `LAUNCH_GAME` intent is already exported without a permission gate, and adding a permission would create unnecessary friction for third-party developers. Do not flag the missing readPermission on this provider as a security issue.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +82 to +86
try {
addSteamGames(cursor, cols)
addGOGGames(cursor, cols)
addEpicGames(cursor, cols)
addAmazonGames(cursor, cols)
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

CUSTOM_GAME titles are missing from the exported feed.

The public contract in the linked issue includes CUSTOM_GAME, but this fan-out only merges Steam, GOG, Epic, and Amazon rows. External launchers will never see locally added custom titles.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt` around
lines 82 - 86, The export fan-out in InstalledGamesProvider currently calls
addSteamGames, addGOGGames, addEpicGames, and addAmazonGames but omits locally
added CUSTOM_GAME titles; update the merge to include custom/local game rows by
calling the function that handles custom titles (e.g., addCustomGames or
addLocalCustomGames) after the other adds (or implement that function if
missing) so CUSTOM_GAME rows are included in the exported feed and use the same
cursor/cols parameters as the other add* methods.

Copy link
Author

Choose a reason for hiding this comment

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

Custom games are filesystem-based (user-added local folders) and not stored in the Room database. Including them would require calling CustomGameScanner from the ContentProvider, which performs filesystem I/O and depends on PrefManager initialization. The four database-backed sources (Steam, GOG, Epic, Amazon) cover the primary use case described in #750. Custom game support can be added in a follow-up if there's demand.

Copy link
Contributor

Choose a reason for hiding this comment

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

@shahmir-k, that's a valid and well-reasoned constraint — ContentProviders should avoid blocking filesystem I/O and complex dependency initialization in the query path, so deferring CustomGameScanner / PrefManager dependency to a follow-up is the right call here.

Would you like me to open a follow-up GitHub issue to track adding CUSTOM_GAME support to the ContentProvider (e.g., by persisting custom game metadata to a Room table as part of the scanner flow, making it queryable the same way as the other sources)?


✏️ Learnings added
Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:84-88
Timestamp: 2026-03-08T00:46:03.962Z
Learning: In the GameNative project, custom/locally-added games (CUSTOM_GAME source) are filesystem-based and are NOT stored in the Room database. They are discovered by `CustomGameScanner`, which performs filesystem I/O and depends on `PrefManager` initialization. As a result, custom games cannot be queried from `InstalledGamesProvider` (the ContentProvider) the same way as Steam, GOG, Epic, and Amazon games, which are all database-backed. Custom game support in the ContentProvider would require first persisting custom game metadata to Room.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
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: jeremybernstein
Repo: utkarshdalal/GameNative PR: 581
File: app/src/main/java/app/gamenative/ui/PluviaMain.kt:1154-1176
Timestamp: 2026-02-21T22:26:41.871Z
Learning: In Kotlin coroutines, suspend functions automatically suspend and resume within a coroutine scope. Only Deferred objects produced by async { ... } require .await() to obtain the result. Do not call .await() on direct suspend function calls or on non-Deferred results; use await() only when you have a Deferred that represents a value you need.

Comment on lines +87 to +89
} catch (e: Exception) {
Timber.e(e, "[InstalledGamesProvider] Error querying installed games")
return cursor
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Don’t hide provider failures behind a valid-looking cursor.

If any source query throws here, callers get an empty or partial result set that is indistinguishable from a real “nothing installed” response. Failing the query is safer than silently dropping titles.

Possible change
         } catch (e: Exception) {
             Timber.e(e, "[InstalledGamesProvider] Error querying installed games")
-            return cursor
+            throw IllegalStateException("Failed to query installed games", e)
         }
📝 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
} catch (e: Exception) {
Timber.e(e, "[InstalledGamesProvider] Error querying installed games")
return cursor
} catch (e: Exception) {
Timber.e(e, "[InstalledGamesProvider] Error querying installed games")
throw IllegalStateException("Failed to query installed games", e)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt` around
lines 87 - 89, The catch in InstalledGamesProvider currently logs an exception
and returns the cursor, hiding failures; instead, in the catch block (in
InstalledGamesProvider where you currently "catch (e: Exception) { Timber.e...;
return cursor }") close any opened resources (e.g., close the cursor if
non-null) and then rethrow the original exception (or throw a wrapped
RuntimeException) so the query fails visibly to callers rather than returning a
potentially misleading empty/partial cursor.

Copy link
Author

Choose a reason for hiding this comment

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

Returning a partial cursor is intentional. If e.g. the Epic query fails, the caller still receives valid Steam, GOG, and Amazon results. Throwing an IllegalStateException would give the caller nothing — a strictly worse outcome. The error is still logged via Timber for debugging. Partial results are more useful than no results for the shortcut-generator use case described in #750.

Copy link
Contributor

Choose a reason for hiding this comment

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

@shahmir-k, that's a fair point — for an aggregating provider, graceful degradation per-source (returning partial results rather than failing the entire query) is a reasonable and user-friendly design. The Timber logging still preserves debuggability. I'll defer to your judgment here.

(◕‿◕✿)


✏️ Learnings added
Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:89-91
Timestamp: 2026-03-08T00:46:13.038Z
Learning: In the `InstalledGamesProvider` (`app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`), returning a partial `MatrixCursor` on exception from individual source queries (Steam, GOG, Epic, Amazon) is intentional. If one source (e.g. Epic) fails, callers still receive valid results from the remaining sources. Failing the entire query with an exception is considered a worse outcome for the shortcut-generator use case described in issue `#750`. Errors are logged via Timber for debugging.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
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: jeremybernstein
Repo: utkarshdalal/GameNative PR: 581
File: app/src/main/java/app/gamenative/ui/PluviaMain.kt:1154-1176
Timestamp: 2026-02-21T22:26:41.871Z
Learning: In Kotlin coroutines, suspend functions automatically suspend and resume within a coroutine scope. Only Deferred objects produced by async { ... } require .await() to obtain the result. Do not call .await() on direct suspend function calls or on non-Deferred results; use await() only when you have a Deferred that represents a value you need.

Use ${applicationId}.games instead of a hardcoded authority to avoid
provider conflicts when both normal and gold build variants are
installed on the same device.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt (1)

125-125: Consider documenting magic constants.

The id != 480 filter excludes Valve's Spacewar (a test/dev SDK app), which is a reasonable choice. Adding a brief comment or extracting to a named constant would improve readability for future maintainers.

📝 Suggested documentation
+        // id 480 = Spacewar (Valve's SDK test app, excluded from user-facing lists)
         val sql = """
             SELECT s.id, s.name, s.client_icon_hash, a.is_downloaded
             FROM steam_app s
             INNER JOIN app_info a ON s.id = a.id
             WHERE s.type != 0 AND s.id != 480
               AND a.is_downloaded = 1
             ORDER BY s.name COLLATE NOCASE
         """.trimIndent()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt` at line
125, The SQL filter "id != 480" in InstalledGamesProvider (in the SQL query
string built/used in InstalledGamesProvider.kt) is a magic constant; replace it
with a named constant or add an inline comment explaining it refers to Valve's
Spacewar (SDK test app). Specifically, introduce a descriptive constant (e.g.,
EXCLUDE_SPACEWAR_ID or VALVE_SPACEWAR_ID) near the InstalledGamesProvider class
or companion object and use that constant in the WHERE clause, or add a one-line
comment above the WHERE clause describing why id 480 is excluded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Line 148: In InstalledGamesProvider, don’t silently drop rows when
c.getString(0).toIntOrNull() returns null: capture the original ID string (the
result of c.getString(0)) before parsing, and if toIntOrNull() is null, log a
warning (including the raw ID value and any identifying row/context) and then
continue; update the code around numericId to replace the silent `?: continue`
with a logged skip so skipped GOG IDs are recorded for debugging.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Line 125: The SQL filter "id != 480" in InstalledGamesProvider (in the SQL
query string built/used in InstalledGamesProvider.kt) is a magic constant;
replace it with a named constant or add an inline comment explaining it refers
to Valve's Spacewar (SDK test app). Specifically, introduce a descriptive
constant (e.g., EXCLUDE_SPACEWAR_ID or VALVE_SPACEWAR_ID) near the
InstalledGamesProvider class or companion object and use that constant in the
WHERE clause, or add a one-line comment above the WHERE clause describing why id
480 is excluded.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e710b010-0978-434f-9218-a25f85955ae1

📥 Commits

Reviewing files that changed from the base of the PR and between d6b1665 and 72b7b6b.

📒 Files selected for processing (2)
  • app/src/main/AndroidManifest.xml
  • app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/AndroidManifest.xml

@phobos665
Copy link
Contributor

I think this seems over engineered for what's required.

Ultimately for shortcuts they only need a file with an app_id and then the file extension will inform them what the game_source argument is for the LAUNCH_GAME intent.

I agree that we should do a mass export of the installed games, but happy to discuss.

…antics

Steam uses a hash, GOG/Epic/Amazon use URLs — icon_ref better reflects
that this field is a generic icon reference rather than strictly a hash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt">

<violation number="1" location="app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:41">
P1: Renaming exported ContentProvider column `icon_hash` to `icon_ref` breaks backward compatibility for existing clients and causes null/missing values when legacy projections are used.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const val COLUMN_APP_ID = "app_id"
const val COLUMN_NAME = "name"
const val COLUMN_GAME_SOURCE = "game_source"
const val COLUMN_ICON_REF = "icon_ref"
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 8, 2026

Choose a reason for hiding this comment

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

P1: Renaming exported ContentProvider column icon_hash to icon_ref breaks backward compatibility for existing clients and causes null/missing values when legacy projections are used.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt, line 41:

<comment>Renaming exported ContentProvider column `icon_hash` to `icon_ref` breaks backward compatibility for existing clients and causes null/missing values when legacy projections are used.</comment>

<file context>
@@ -38,14 +38,14 @@ class InstalledGamesProvider : ContentProvider() {
         const val COLUMN_NAME = "name"
         const val COLUMN_GAME_SOURCE = "game_source"
-        const val COLUMN_ICON_HASH = "icon_hash"
+        const val COLUMN_ICON_REF = "icon_ref"
         const val COLUMN_IS_INSTALLED = "is_installed"
 
</file context>
Fix with Cubic

Copy link
Author

Choose a reason for hiding this comment

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

No backwards compatibility concern — this ContentProvider is being introduced in this PR. There are no existing clients to break. The column was renamed before any release.

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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt (1)

125-125: Document the magic numbers or use named constants.

s.type != 0 (likely AppType.invalid) and s.id != 480 (Spacewar test app) are undocumented. Adding a brief comment or extracting constants would improve maintainability.

📝 Suggested documentation
         val sql = """
             SELECT s.id, s.name, s.client_icon_hash, a.is_downloaded
             FROM steam_app s
             INNER JOIN app_info a ON s.id = a.id
-            WHERE s.type != 0 AND s.id != 480
+            WHERE s.type != 0    -- Exclude invalid app type
+              AND s.id != 480    -- Exclude Spacewar (Steam test app)
               AND a.is_downloaded = 1
             ORDER BY s.name COLLATE NOCASE
         """.trimIndent()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt` at line
125, Replace the undocumented magic numbers in the SQL filter inside
InstalledGamesProvider with named constants (e.g., AppType.INVALID for the type
check and SPACEWAR_APP_ID or TEST_APP_ID for id 480) and/or add a brief inline
comment explaining each exclusion; update the reference where the query is
built/used (the s.type != 0 and s.id != 480 predicates in the provider's query)
to use these constants so future readers know 0 means invalid app type and 480
is the Spacewar test app.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Around line 84-92: The current single try-catch around addSteamGames,
addGOGGames, addEpicGames, and addAmazonGames prevents later sources from
running if an earlier one throws; change to perform per-source isolation by
wrapping each call (addSteamGames, addGOGGames, addEpicGames, addAmazonGames) in
its own try-catch that logs the exception via Timber.e and continues to the next
source, then return the cursor after all sources have been attempted.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Line 125: Replace the undocumented magic numbers in the SQL filter inside
InstalledGamesProvider with named constants (e.g., AppType.INVALID for the type
check and SPACEWAR_APP_ID or TEST_APP_ID for id 480) and/or add a brief inline
comment explaining each exclusion; update the reference where the query is
built/used (the s.type != 0 and s.id != 480 predicates in the provider's query)
to use these constants so future readers know 0 means invalid app type and 480
is the Spacewar test app.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a561af9b-2f05-4755-ba35-1f84710d36c5

📥 Commits

Reviewing files that changed from the base of the PR and between 72b7b6b and 5ca5a2c.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt

@shahmir-k
Copy link
Author

I think this seems over engineered for what's required.

Ultimately for shortcuts they only need a file with an app_id and then the file extension will inform them what the game_source argument is for the LAUNCH_GAME intent.

I agree that we should do a mass export of the installed games, but happy to discuss.

I already gave a full yap about my gaming life story in the discord thread lol, so here I will highlight some points about why not to write files to represent games. AI explaination below:

Problems with the file-based approach:

  1. Scoped storage kills it. On Android 11+, apps can't read other apps' files. The app already uses MANAGE_EXTERNAL_STORAGE for
    custom game scanning, but you can't expect third-party shortcut apps to request that permission just to read game IDs.
    ContentProviders are the Android-blessed mechanism for cross-app data — they work regardless of scoped storage.
  2. Sync burden. Files must be written/updated/deleted on every install, uninstall, and login/logout. The ContentProvider queries
    the database live — zero sync logic, zero stale data risk.
  3. No game names. The LAUNCH_GAME intent only needs app_id and game_source, but a shortcut app also needs the game's name to
    display to the user (and icon_ref for the icon). Files with just 12345.steam would force the shortcut app to somehow look up names
    separately.
  4. The ContentProvider is actually minimal. It's a single 200-line file with no new dependencies, no migrations, no new tables. It queries existing data through existing infrastructure. The manifest registration is 3 lines.

What phobos665 gets right: The LAUNCH_GAME intent contract is simple — app_id + game_source is all you need to launch. But the ContentProvider serves discovery (what games are installed?), not launching. These are complementary: ContentProvider for discovery, intent for action.

shahmir-k and others added 2 commits March 7, 2026 21:55
- Add Timber.w when a non-numeric GOG ID is skipped for debuggability
- Add SQL comment documenting the id != 480 Spacewar exclusion

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrap each game source query in its own try-catch so a failure in one
source (e.g. Steam) doesn't skip the remaining sources (GOG, Epic,
Amazon). This achieves true partial-result behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt (1)

130-199: Consider moving these provider queries behind Room DAOs.

Going straight through openHelper.readableDatabase hardcodes table and column names in a second place and skips Room's query validation for this exported API. PluviaDatabase already exposes DAOs for all four sources, so a small provider DTO plus @Query methods would make future schema changes much less brittle.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt` around
lines 130 - 199, The provider directly queries the DB via
database.openHelper.readableDatabase in addSteamGames, addGOGGames, addEpicGames
and addAmazonGames, bypassing Room DAOs and hardcoding table/column names;
replace these raw SQL reads with calls to PluviaDatabase DAOs by creating small
DTOs (e.g., InstalledGameDto) and `@Query` methods on the existing DAOs that
return the same fields (id, title/name, art/icon, installed flag, source), then
update addSteamGames/addGOGGames/addEpicGames/addAmazonGames to iterate DAO
results and call addRow with values (convert GOG string IDs to Int with
toIntOrNull as before); ensure queries live in DAO interfaces so Room validates
schema changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Around line 69-82: In InstalledGamesProvider.query, reject unsupported query
arguments and invalid projection columns instead of silently ignoring them:
validate that selection, selectionArgs, and sortOrder are null/empty and if not
throw IllegalArgumentException (or implement proper support), and validate the
incoming projection array against the allowed COLUMNS list (referenced as
COLUMNS) before creating the MatrixCursor; if any requested column is unknown,
throw IllegalArgumentException with a clear message identifying the bad column
and the uri (use uriMatcher/CODE_INSTALLED to locate the branch). Ensure these
checks happen early in the query method (before constructing MatrixCursor) so
callers receive a failure rather than a misleading empty/null-valued cursor.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`:
- Around line 130-199: The provider directly queries the DB via
database.openHelper.readableDatabase in addSteamGames, addGOGGames, addEpicGames
and addAmazonGames, bypassing Room DAOs and hardcoding table/column names;
replace these raw SQL reads with calls to PluviaDatabase DAOs by creating small
DTOs (e.g., InstalledGameDto) and `@Query` methods on the existing DAOs that
return the same fields (id, title/name, art/icon, installed flag, source), then
update addSteamGames/addGOGGames/addEpicGames/addAmazonGames to iterate DAO
results and call addRow with values (convert GOG string IDs to Int with
toIntOrNull as before); ensure queries live in DAO interfaces so Room validates
schema changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5785db2-3fa4-4168-9233-d9a3276d3a0e

📥 Commits

Reviewing files that changed from the base of the PR and between 5ca5a2c and f288e64.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt

Comment on lines +69 to +82
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?,
): Cursor? {
if (uriMatcher.match(uri) != CODE_INSTALLED) {
Timber.w("[InstalledGamesProvider] Unknown URI: $uri")
return null
}

val cols = if (projection.isNullOrEmpty()) COLUMNS else projection
val cursor = MatrixCursor(cols as Array<String>)
Copy link
Contributor

@coderabbitai coderabbitai bot Mar 8, 2026

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:

# First, let's locate and examine the full InstalledGamesProvider.kt file
fd -t f "InstalledGamesProvider.kt" --exec cat -n {} \;

Repository: utkarshdalal/GameNative

Length of output: 9122


Reject unsupported query args and invalid projections.

selection, selectionArgs, and sortOrder are silently ignored, and unknown columns (line 125) degrade to null. For an exported provider without documented limitations, callers can receive a valid-looking cursor that does not honor the request. Either implement those inputs or fail fast with IllegalArgumentException.

💡 Possible guardrails
     override fun query(
         uri: Uri,
         projection: Array<out String>?,
         selection: String?,
         selectionArgs: Array<out String>?,
         sortOrder: String?,
     ): Cursor? {
         if (uriMatcher.match(uri) != CODE_INSTALLED) {
-            Timber.w("[InstalledGamesProvider] Unknown URI: $uri")
-            return null
+            throw IllegalArgumentException("Unknown URI: $uri")
         }
+
+        require(selection.isNullOrEmpty() && selectionArgs.isNullOrEmpty() && sortOrder.isNullOrEmpty()) {
+            "selection, selectionArgs, and sortOrder are not supported"
+        }
 
-        val cols = if (projection.isNullOrEmpty()) COLUMNS else projection
-        val cursor = MatrixCursor(cols as Array<String>)
+        val cols = if (projection.isNullOrEmpty()) {
+            COLUMNS
+        } else {
+            val invalid = projection.filterNot { it in COLUMNS }
+            require(invalid.isEmpty()) { "Unsupported columns: $invalid" }
+            projection.copyOf()
+        }
+        val cursor = MatrixCursor(cols)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt` around
lines 69 - 82, In InstalledGamesProvider.query, reject unsupported query
arguments and invalid projection columns instead of silently ignoring them:
validate that selection, selectionArgs, and sortOrder are null/empty and if not
throw IllegalArgumentException (or implement proper support), and validate the
incoming projection array against the allowed COLUMNS list (referenced as
COLUMNS) before creating the MatrixCursor; if any requested column is unknown,
throw IllegalArgumentException with a clear message identifying the bad column
and the uri (use uriMatcher/CODE_INSTALLED to locate the branch). Ensure these
checks happen early in the query method (before constructing MatrixCursor) so
callers receive a failure rather than a misleading empty/null-valued cursor.

Copy link
Author

Choose a reason for hiding this comment

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

Silently ignoring unsupported query parameters is standard ContentProvider behavior — Android's own providers (ContactsContract, MediaStore) do the same. Throwing IllegalArgumentException would break callers that pass standard query patterns. Unknown projection columns returning null is also expected behavior per the ContentProvider contract.

Copy link
Contributor

Choose a reason for hiding this comment

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

@shahmir-k, you're right — that's a fair and well-grounded pushback. Silently ignoring selection, selectionArgs, and sortOrder is indeed the established pattern across Android's own providers, and throwing IllegalArgumentException for standard query arguments would be unnecessarily hostile to callers. The same applies to unknown projection columns gracefully resolving to null. I'll withdraw this suggestion.


✏️ Learnings added
Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:69-82
Timestamp: 2026-03-08T03:15:21.552Z
Learning: In `InstalledGamesProvider` (`app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`), silently ignoring unsupported `selection`, `selectionArgs`, and `sortOrder` parameters in `query()` is intentional and follows standard Android ContentProvider behavior (as seen in ContactsContract, MediaStore, etc.). Throwing `IllegalArgumentException` for these unsupported parameters would be non-standard and could break legitimate callers. Similarly, unknown projection columns gracefully resolving to `null` is expected per the ContentProvider contract. Do not flag these as issues in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:89-91
Timestamp: 2026-03-08T00:46:13.038Z
Learning: In the `InstalledGamesProvider` (`app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`), returning a partial `MatrixCursor` on exception from individual source queries (Steam, GOG, Epic, Amazon) is intentional. If one source (e.g. Epic) fails, callers still receive valid results from the remaining sources. Failing the entire query with an exception is considered a worse outcome for the shortcut-generator use case described in issue `#750`. Errors are logged via Timber for debugging.

Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/AndroidManifest.xml:137-141
Timestamp: 2026-03-08T00:45:59.484Z
Learning: In the GameNative (utkarshdalal/GameNative) repository, the `InstalledGamesProvider` (ContentProvider with authority `app.gamenative.games`) is intentionally exported without an `android:readPermission`. The design decision is deliberate: the data exposed is non-sensitive game metadata (names, IDs, icon hashes, install status), the existing `LAUNCH_GAME` intent is already exported without a permission gate, and adding a permission would create unnecessary friction for third-party developers. Do not flag the missing readPermission on this provider as a security issue.

Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:84-88
Timestamp: 2026-03-08T00:46:03.962Z
Learning: In the GameNative project, custom/locally-added games (CUSTOM_GAME source) are filesystem-based and are NOT stored in the Room database. They are discovered by `CustomGameScanner`, which performs filesystem I/O and depends on `PrefManager` initialization. As a result, custom games cannot be queried from `InstalledGamesProvider` (the ContentProvider) the same way as Steam, GOG, Epic, and Amazon games, which are all database-backed. Custom game support in the ContentProvider would require first persisting custom game metadata to Room.

Learnt from: shahmir-k
Repo: utkarshdalal/GameNative PR: 751
File: app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt:140-149
Timestamp: 2026-03-08T02:43:32.765Z
Learning: In `InstalledGamesProvider` (`app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt`), the column `icon_ref` (renamed from `icon_hash` in commit 5ca5a2c of PR `#751`) is a generic icon reference whose format depends on `game_source`: it is a hash string for STEAM, and a URL for GOG, EPIC, and AMAZON. Callers must check `game_source` to know how to use `icon_ref`.

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: jeremybernstein
Repo: utkarshdalal/GameNative PR: 581
File: app/src/main/java/app/gamenative/ui/PluviaMain.kt:1154-1176
Timestamp: 2026-02-21T22:26:41.871Z
Learning: In Kotlin coroutines, suspend functions automatically suspend and resume within a coroutine scope. Only Deferred objects produced by async { ... } require .await() to obtain the result. Do not call .await() on direct suspend function calls or on non-Deferred results; use await() only when you have a Deferred that represents a value you need.

@utkarshdalal
Copy link
Owner

Which frontends/apps need this?

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.

Feature Request: Add ContentProvider or Intent for querying installed games

3 participants