feat: add ContentProvider for querying installed games#751
feat: add ContentProvider for querying installed games#751shahmir-k wants to merge 5 commits intoutkarshdalal:masterfrom
Conversation
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>
📝 WalkthroughWalkthroughAdds a read-only ContentProvider, InstalledGamesProvider, registered under authority Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
app/src/main/AndroidManifest.xmlapp/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt
| <!-- ContentProvider for external apps to query installed games --> | ||
| <provider | ||
| android:name=".provider.InstalledGamesProvider" | ||
| android:authorities="app.gamenative.games" | ||
| android:exported="true" /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and examine the AndroidManifest.xml around the specified lines
find . -name "AndroidManifest.xml" -type f | head -5Repository: utkarshdalal/GameNative
Length of output: 141
🏁 Script executed:
# Check the structure to understand the repository
ls -laRepository: utkarshdalal/GameNative
Length of output: 1809
🏁 Script executed:
# Look for the provider implementation file
find . -path "*/provider/InstalledGamesProvider*" -type fRepository: 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.ktRepository: 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.xmlRepository: 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
| try { | ||
| addSteamGames(cursor, cols) | ||
| addGOGGames(cursor, cols) | ||
| addEpicGames(cursor, cols) | ||
| addAmazonGames(cursor, cols) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
| } catch (e: Exception) { | ||
| Timber.e(e, "[InstalledGamesProvider] Error querying installed games") | ||
| return cursor |
There was a problem hiding this comment.
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.
| } 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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>
There was a problem hiding this comment.
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 != 480filter 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
📒 Files selected for processing (2)
app/src/main/AndroidManifest.xmlapp/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
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt
Outdated
Show resolved
Hide resolved
|
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>
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(likelyAppType.invalid) ands.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
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt
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:
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. |
- 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>
There was a problem hiding this comment.
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.readableDatabasehardcodes table and column names in a second place and skips Room's query validation for this exported API.PluviaDatabasealready exposes DAOs for all four sources, so a small provider DTO plus@Querymethods 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
📒 Files selected for processing (1)
app/src/main/java/app/gamenative/provider/InstalledGamesProvider.kt
| 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>) |
There was a problem hiding this comment.
🧩 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
|
Which frontends/apps need this? |
Summary
ContentProviderthat exposes installed games to external appsLAUNCH_GAMEintentDetails
Authority:
app.gamenative.gamesURI:
content://app.gamenative.games/installedapp_idnamegame_sourceicon_hashis_installedGame sources queried:
steam_appINNER JOINapp_infowhereis_downloaded = 1gog_gameswhereis_installed = 1epic_gameswhereis_installed = 1(excludes DLC)amazon_gameswhereis_installed = 1Usage from an external app:
Test plan
adb shell content query --uri content://app.gamenative.games/installedCloses #750
🤖 Generated with Claude Code
Summary by cubic
Adds a read-only ContentProvider for external apps to query installed games. Fulfills Linear #750.
content://${applicationId}.games/installed(authority${applicationId}.games, dynamic)Written for commit f288e64. Summary will update on new commits.
Summary by CodeRabbit