-
Notifications
You must be signed in to change notification settings - Fork 84
Epic games Integration #431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
- Epic Games authentication and login flow - Game library management and syncing - Cloud saves support with manifest parsing (binary and JSON formats) - Game installation and download management - Integration with container system - Epic-specific UI components and screens - Database entities and DAOs for Epic games - Comprehensive test coverage for manifest parsing Includes validation for empty manifests and files to prevent upload/download issues.
…how the correct download for the base game. Next up is seeing about downloading the DLC also.
Now it'll just ignore it if the file is empty.
…pload. Also removed EpicConverter since GOGConverter does the same thing.
… epic. And fixed crash with install
…s downloaded. Also fixed the download progress so that it uses the downloadSize for its progress bar.
… into epic-games-integration
|
Update: This is now fixed, downloaded 8 different games with varying sizes and all complete succesfully. Will move onto final end-to-end testing and ensure there are no outlying bugs left. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
3 issues found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt">
<violation number="1" location="app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt:712">
P2: Inconsistent logging: Use `Timber.tag(LOG_TAG).d()` instead of `Log.d()` to match the codebase's logging pattern.</violation>
<violation number="2" location="app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt:730">
P2: Inconsistent logging: Use `Timber.tag(LOG_TAG).w()` instead of `Log.w()` to match the codebase's logging pattern.</violation>
<violation number="3" location="app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt:752">
P2: Incorrect Timber API usage: `Timber.d(LOG_TAG, message)` treats LOG_TAG as a format string, not a tag. Use `Timber.tag(LOG_TAG).d(message)` to match the rest of the codebase.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt`:
- Around line 705-710: The code currently reads uncompressedSize from a
hard-coded offset (buffer.position(46)) which breaks for headers larger than 62
bytes; instead compute the last-4-bytes position from the actual header size and
read from there — e.g. after reading storedAs (using buffer.position(4 + 16 + 8)
and val storedAs/... as before), set buffer.position(buffer.limit() - 4) (or
compute pos = remainingSize - 4 if you have remainingSize) and then read val
uncompressedSize = buffer.int so the uncompressed size is always taken from the
header's final 4 bytes.
- Around line 507-512: The current verification reads the entire decompressed
file into memory via verifyChunkHashBytes(decompressedFile.readBytes(),
chunk.shaHash) which can OOM for large chunks; replace this with a streaming
file-hash verifier (similar to decompressStreamingChunkToFile) by adding a
helper like verifyChunkHashFile(file: File, expectedHash: ByteArray) that
computes SHA-1 by reading the file in a loop (e.g., 64KB buffer), compares
digests, logs mismatches with hex strings via Timber.tag("Epic").e(...) and
returns boolean, then call that helper in the existing block (replace the
verifyChunkHashBytes(...) call) so successful verification still triggers
downloadInfo.updateBytesDownloaded(chunk.fileSize) and returning
Result.success(decompressedFile).
♻️ Duplicate comments (5)
app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt (3)
118-121: Guard against an empty CDN list after Cloudflare filtering.Lines 118-121 can yield an empty list when all URLs are Cloudflare, which makes every chunk fail without a clear fallback. Consider falling back to the unfiltered list (or fail fast with a clear error).
🛠️ Proposed fix
- val cdnUrls = manifestData.cdnUrls.filter { !it.baseUrl.startsWith("https://cloudflare.epicgamescdn.com") } + val filteredCdnUrls = + manifestData.cdnUrls.filter { !it.baseUrl.startsWith("https://cloudflare.epicgamescdn.com") } + val cdnUrls = if (filteredCdnUrls.isNotEmpty()) filteredCdnUrls else manifestData.cdnUrls
337-364: FinalizeDownloadInfoeven when returning early.Lines 110-131 and 213-231 return from inside the
try, so the success/catch cleanup at Lines 337-343 doesn’t run;downloadInfocan remain active and leave the UI stuck. Move deactivation intofinallyso it always runs.🧹 Proposed fix
} finally { + if (downloadInfo.isActive()) { + downloadInfo.setActive(false) + downloadInfo.emitProgressChange() + } // Always emit download stopped event val gameId = game.id ?: 0 app.gamenative.PluviaApp.events.emitJava( app.gamenative.events.AndroidEvent.DownloadStatusChanged(gameId, false), ) }
923-934: Handle partial skips and premature EOF when assembling files.Line 925 ignores the return from
InputStream.skip, which may skip fewer bytes, and Line 934 breaks on EOF without error—this can silently truncate assembled files. Loop until the desired offset is skipped and treat unexpected EOF as a failure.🧩 Proposed fix
// Read chunk data at specified offset chunkFile.inputStream().use { input -> - input.skip(chunkPart.offset.toLong()) + var toSkip = chunkPart.offset.toLong() + while (toSkip > 0) { + val skipped = input.skip(toSkip) + if (skipped <= 0) { + throw Exception("Failed to skip to offset ${chunkPart.offset} for ${chunkPart.guidStr}") + } + toSkip -= skipped + } val buffer = ByteArray(65536) // Increased to 64KB for better I/O performance var remaining = chunkPart.size.toLong() while (remaining > 0) { val toRead = minOf(remaining, buffer.size.toLong()).toInt() val bytesRead = input.read(buffer, 0, toRead) - if (bytesRead == -1) break + if (bytesRead == -1) { + throw Exception("Unexpected EOF assembling ${fileManifest.filename} from ${chunkPart.guidStr}") + } output.write(buffer, 0, bytesRead) remaining -= bytesRead } }app/src/main/java/app/gamenative/service/epic/EpicService.kt (2)
330-335: Avoid plainrunBlockingindownloadGame.Line 333 uses
runBlockingwithout a dispatcher; if this is called from the main thread, the DB read runs on the caller thread and can stall the UI. If you keep a synchronous API, run the blocking call onDispatchers.IO(or make the function suspend). Based on learnings, keeping sync APIs is fine, but it shouldn’t run I/O on the caller thread.🛠️ Proposed fix
- val game = runBlocking { instance.epicManager.getGameById(appId) } + val game = runBlocking(Dispatchers.IO) { instance.epicManager.getGameById(appId) }
337-351: MakeactiveDownloadsinsertion atomic.Lines 337-351 use a check-then-put pattern that can race and start duplicate downloads under concurrent calls. Use
putIfAbsent(orcomputeIfAbsent) to ensure single ownership.🔒 Proposed fix
- // Check if already downloading - if (instance.activeDownloads.containsKey(appId)) { - Timber.tag("Epic").w("Download already in progress for $appId") - return Result.success(instance.activeDownloads[appId]!!) - } - // Create DownloadInfo before launching coroutine to avoid race condition val downloadInfo = DownloadInfo( jobCount = 1, gameId = appId, downloadingAppIds = CopyOnWriteArrayList<Int>(), ) - instance.activeDownloads[appId] = downloadInfo - downloadInfo.setActive(true) + val existing = instance.activeDownloads.putIfAbsent(appId, downloadInfo) + if (existing != null) { + Timber.tag("Epic").w("Download already in progress for $appId") + return Result.success(existing) + } + downloadInfo.setActive(true)
There was a problem hiding this 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
🤖 Fix all issues with AI agents
In
`@app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt`:
- Around line 786-805: The when expression in EpicAppScreen contains a
duplicated branch for app.gamenative.ui.enums.DialogType.CANCEL_APP_DOWNLOAD
causing unreachable code; remove the redundant second CANCEL_APP_DOWNLOAD branch
so only one branch handling the cancel logic remains (the branch that calls
Timber.tag(TAG).i(...), EpicService.getDownloadInfo(gameId)?.cancel(),
EpicService.cleanupDownload(gameId), BaseAppScreen.hideInstallDialog(appId), and
app.gamenative.PluviaApp.events.emit(...)) and keep the single correct
implementation instead of the duplicate.
In `@app/src/main/res/values-de/strings.xml`:
- Around line 44-51: The strings use formal "Sie" but the app uses informal "du"
elsewhere; update the German translations for epic_cancel_download_message,
epic_delete_download_message, epic_cloud_sync_starting, epic_cloud_sync_success,
epic_cloud_sync_failed, epic_cloud_sync_error, epic_uninstall_failed, and
epic_uninstall_error to the informal "du" form (e.g., "Möchtest du den Download
der App wirklich abbrechen?", "Alle heruntergeladenen Daten für dieses Spiel
löschen?", and change messages containing "fehlgeschlagen"/"Fehler" to use "du"
where applicable) so tone matches the rest of the UI.
In `@app/src/main/res/values-fr/strings.xml`:
- Around line 49-56: The new French strings (epic_cancel_download_message,
epic_delete_download_message, epic_cloud_sync_starting, epic_cloud_sync_success,
epic_cloud_sync_failed, epic_cloud_sync_error, epic_uninstall_failed,
epic_uninstall_error) use English-style punctuation; update them to follow
French typography by inserting a non-breaking space before question marks and
before colons where applicable (e.g., "l'application ?" and "Erreur de
synchronisation des sauvegardes cloud : %1$s"), matching the existing strings in
the file.
In `@app/src/main/res/values-ro/strings.xml`:
- Around line 49-50: Replace the formal Romanian phrasing in the two strings
epic_cancel_download_message and epic_delete_download_message with the informal
"tu" forms to match the rest of the file; specifically, change "Sigur doriți să
anulați descărcarea aplicației?" to "Sigur vrei să anulezi descărcarea
aplicației?" and change "Ștergeți toate datele descărcate pentru acest joc?" to
"Ștergi toate datele descărcate pentru acest joc?" so both strings use the
informal tone consistent with other entries (e.g., the Steam equivalents).
♻️ Duplicate comments (11)
app/src/main/res/values-fr/strings.xml (2)
997-997: Localize the example text to French.The example still contains English text (
authorization code: "example"). This should be fully localized to match the surrounding French strings and use consistent terminology withepic_login_auth_code_label.✏️ Suggested fix
- <string name="epic_login_auth_example">Exemple : authorization code: "example"</string> + <string name="epic_login_auth_example">Exemple : code d\'autorisation : « exemple »</string>
1000-1000: Placeholder text inconsistent with label.The label at line 999 asks only for "Code d'autorisation" but the placeholder mentions "URL", which may confuse users. Either update the label to mention URL or simplify the placeholder.
✏️ Suggested fix (if URL is not needed)
- <string name="epic_login_auth_code_placeholder">Collez le code ou l\'URL ici</string> + <string name="epic_login_auth_code_placeholder">Collez le code ici</string>app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)
1741-1741: Revert the return type toString.This was flagged in a previous review and you confirmed it should be reverted. All code paths return non-null strings, so the nullable return type is unnecessary and could cause issues at the caller site (line 1561-1562) where string concatenation is performed.
Suggested fix
-): String? { +): String {app/src/main/java/app/gamenative/service/epic/EpicDownloadManager.kt (4)
109-230: FinalizeDownloadInfoon all early exits.
Severalreturn@withContextpaths bypass the cleanup in the success/catch branches, leavingdownloadInfoactive and the UI stuck in “downloading”. Move finalization intofinally(or consolidate into a single return) so it always runs.🔧 Suggested fix (ensure cleanup always runs)
} finally { + if (downloadInfo.isActive()) { + downloadInfo.setActive(false) + } + downloadInfo.emitProgressChange() // Always emit download stopped event val gameId = game.id ?: 0 app.gamenative.PluviaApp.events.emitJava( app.gamenative.events.AndroidEvent.DownloadStatusChanged(gameId, false), ) }Also applies to: 351-362
505-512: AvoidreadBytes()for verifying large chunks.
decompressedFile.readBytes()loads the full chunk into memory and can OOM on multi‑GB chunks. Switch to a streaming hash to keep memory bounded.🔧 Suggested fix (streaming hash verification)
- if (verifyChunkHashBytes(decompressedFile.readBytes(), chunk.shaHash)) { + if (verifyChunkHashFile(decompressedFile, chunk.shaHash)) { Timber.tag("Epic").d("Chunk ${chunk.guidStr} already exists and verified, skipping") downloadInfo.updateBytesDownloaded(chunk.fileSize) return@withContext Result.success(decompressedFile) } else {+ private fun verifyChunkHashFile(file: File, expectedHash: ByteArray): Boolean { + return try { + val digest = MessageDigest.getInstance("SHA-1") + file.inputStream().use { input -> + val buffer = ByteArray(65536) + var read = input.read(buffer) + while (read != -1) { + digest.update(buffer, 0, read) + read = input.read(buffer) + } + } + val actualHash = digest.digest() + if (!actualHash.contentEquals(expectedHash)) { + val expectedHex = expectedHash.joinToString("") { "%02x".format(it) } + val actualHex = actualHash.joinToString("") { "%02x".format(it) } + Timber.tag("Epic").e("Hash mismatch: expected $expectedHex, got $actualHex") + false + } else { + true + } + } catch (e: Exception) { + Timber.tag("Epic").e(e, "Hash verification failed") + false + } + }
704-709: Uncompressed-size offset should depend on header size.
The fixedbuffer.position(46)only works for 62‑byte headers. For 66‑byte headers, the uncompressed size sits in the last 4 bytes, so this can misread and break decompression.🔧 Suggested fix (read from end of header)
- buffer.position(46) // Jump to where uncompressedSize should be (58-12=46) - val uncompressedSize = buffer.int // offset 46-49 (file offset 58-61) + val uncompressedOffset = remainingSize - 4 + buffer.position(uncompressedOffset) + val uncompressedSize = buffer.int
922-937: Guard against partialskip()and premature EOF in file assembly.
InputStream.skip()may skip fewer bytes, and hitting EOF early currently truncates files silently. Loop the skip and throw if EOF occurs before expected data is written to avoid corruption.🔧 Suggested fix (robust skip + EOF handling)
chunkFile.inputStream().use { input -> - input.skip(chunkPart.offset.toLong()) + var toSkip = chunkPart.offset.toLong() + while (toSkip > 0) { + val skipped = input.skip(toSkip) + if (skipped <= 0) { + throw Exception("Failed to skip to offset ${chunkPart.offset} for ${chunkPart.guidStr}") + } + toSkip -= skipped + } val buffer = ByteArray(65536) // Increased to 64KB for better I/O performance var remaining = chunkPart.size.toLong() while (remaining > 0) { val toRead = minOf(remaining, buffer.size.toLong()).toInt() val bytesRead = input.read(buffer, 0, toRead) - if (bytesRead == -1) break + if (bytesRead == -1) { + throw Exception("Unexpected EOF assembling ${fileManifest.filename} from ${chunkPart.guidStr}") + } output.write(buffer, 0, bytesRead) remaining -= bytesRead } }Java InputStream skip(long) return value guarantee and partial skip behaviorapp/src/main/res/values-pt-rBR/strings.xml (1)
864-864: Untranslated English text in Portuguese locale.The example text still contains English that should be translated to Portuguese for consistency.
app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt (3)
465-494: UnmanagedCoroutineScopeinperformUninstall.This function still creates an orphan
CoroutineScope(Dispatchers.IO)not tied to any lifecycle, whileperformDownloadwas updated to accept a lifecycle-aware scope parameter. Consider applying the same pattern here for consistency and to prevent potential memory leaks.
560-591: UnmanagedCoroutineScopein cloud sync handler.The cloud sync onClick handler creates an orphan scope that isn't tied to any lifecycle. This was previously flagged and should be addressed for consistency with the
performDownloadpattern.
151-166: Compose state mutation still occurs on IO dispatcher.The state update
epicGame = updatedGameat line 165 is still inside thewithContext(Dispatchers.IO)block. Compose state mutations must occur on the Main thread.
🧹 Nitpick comments (3)
app/src/main/res/values-it/strings.xml (1)
989-1028: Minor terminology inconsistency with GOG strings.The logout confirmation button uses "Esci" (line 1014), while the equivalent GOG string uses "Logout" (line 985). For UI consistency across platform integrations, consider using the same term.
Additionally, line 1001 contains English text
authorization code: "example"— this appears intentional to match what users see from Epic's auth page, so it's acceptable.Optional: Align logout button text with GOG
- <string name="epic_logout_confirm">Esci</string> + <string name="epic_logout_confirm">Logout</string>app/src/main/res/values-ro/strings.xml (1)
49-56: Consider consolidating Epic strings into a single section.These Epic strings are placed in the middle of the Steam section (between
steam_neverandsteam_continue). The main Epic Games integration section is at lines 991-1028 with proper XML comments. Moving these strings there would improve maintainability and make it easier to locate all Epic-related translations.app/src/main/res/values-pt-rBR/strings.xml (1)
50-57: Epic strings misplaced within Winlator section.These Epic-related strings are inserted in the middle of the Winlator section (between
title_ubuntufsanddirect3d). They should be moved to the Epic Games Integration section at the end of the file (near line 853) to maintain logical organization and aid translators.♻️ Suggested fix: Move strings to Epic section
<string name="title_ubuntufs">Ubuntu FS</string> - <string name="epic_cancel_download_message">Tem certeza de que deseja cancelar o download do aplicativo?</string> - <string name="epic_delete_download_message">Excluir todos os dados baixados deste jogo?</string> - <string name="epic_cloud_sync_starting">Iniciando sincronização de salvamentos na nuvem…</string> - <string name="epic_cloud_sync_success">Salvamentos na nuvem sincronizados com sucesso</string> - <string name="epic_cloud_sync_failed">Falha na sincronização de salvamentos na nuvem</string> - <string name="epic_cloud_sync_error">Erro na sincronização de salvamentos na nuvem: %1$s</string> - <string name="epic_uninstall_failed">Falha na desinstalação: %1$s</string> - <string name="epic_uninstall_error">Erro na desinstalação: %1$s</string> <!-- Winlator: Win Components -->Then add these strings to the Epic Games Integration section (after line 890):
<!-- Epic Games - Additional strings --> <string name="epic_cancel_download_message">Tem certeza de que deseja cancelar o download do aplicativo?</string> <string name="epic_delete_download_message">Excluir todos os dados baixados deste jogo?</string> <string name="epic_cloud_sync_starting">Iniciando sincronização de salvamentos na nuvem…</string> <string name="epic_cloud_sync_success">Salvamentos na nuvem sincronizados com sucesso</string> <string name="epic_cloud_sync_failed">Falha na sincronização de salvamentos na nuvem</string> <string name="epic_cloud_sync_error">Erro na sincronização de salvamentos na nuvem: %1$s</string> <string name="epic_uninstall_failed">Falha na desinstalação: %1$s</string> <string name="epic_uninstall_error">Erro na desinstalação: %1$s</string>
app/src/main/java/app/gamenative/ui/screen/library/appscreen/EpicAppScreen.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4 issues found across 10 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them.
<file name="app/src/main/res/values-fr/strings.xml">
<violation number="1" location="app/src/main/res/values-fr/strings.xml:49">
P3: Add the French typographic space before the question mark to match existing localization style.</violation>
<violation number="2" location="app/src/main/res/values-fr/strings.xml:54">
P3: Add the French typographic space before the colon to match existing localization style.</violation>
<violation number="3" location="app/src/main/res/values-fr/strings.xml:55">
P3: Add the French typographic space before the colon to match existing localization style.</violation>
<violation number="4" location="app/src/main/res/values-fr/strings.xml:56">
P3: Add the French typographic space before the colon to match existing localization style.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| // Start as foreground service | ||
| val notification = notificationHelper.createForegroundNotification("Connected") | ||
| startForeground(1, notification) // Use different ID than SteamService (which uses 1) | ||
| val notification = notificationHelper.createForegroundNotification("GOG Connected") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't do this, it creates two persistent notifications for GameNative. This should be reverted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the reason for this one was to stop the notification when the app is backgrounded, that has to do with the service running, not this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Moved back to 1 and kept as "Connected" so that we're not breaking anything and causing complications.
| fun getWineStartCommand( | ||
| libraryItem: LibraryItem, | ||
| container: com.winlator.container.Container, | ||
| bootToContainer: Boolean, | ||
| appLaunchInfo: LaunchInfo?, | ||
| envVars: com.winlator.core.envvars.EnvVars, | ||
| guestProgramLauncherComponent: com.winlator.xenvironment.components.GuestProgramLauncherComponent, | ||
| ): String { | ||
| return getInstance()?.epicManager?.getWineStartCommand( | ||
| libraryItem, container, bootToContainer, appLaunchInfo, envVars, guestProgramLauncherComponent, | ||
| ) ?: "\"explorer.exe\"" | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be moved to XServerScreen unless there's a reason to make a function here that simply calls a function in EpicManager... We handle the same logic for Steam inline in XServerScreen, and the only difference between Epic/Steam/GOG as far as I know is that we have a different method to get the path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
…retrieval and insertion.
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
…e into epic-games
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt`:
- Around line 53-54: The getAll() DAO query in EpicGameDao currently returns all
EpicGame rows including DLCs; update the SQL for the getAll() method so it
filters out DLCs (use the EpicGame isDLC column, e.g. WHERE is_dlc = false) so
the main library view shows only base games; keep DLC-specific methods
(getDLCForTitle, getAllDlcTitles) unchanged.
- Around line 81-85: The current replaceAll implementation in EpicGameDao calls
deleteAll() then insertAll(games), which causes EpicService.logout() and
GOGService.logout() (which call deleteAllGames()) to wipe install-related
fields; update the logout flow to call EpicGameDao.upsertPreservingInstallStatus
(or change replaceAll to perform an upsert that preserves
isInstalled/installPath/installSize/lastPlayed/playTime) instead of deleteAll(),
ensuring existing local install metadata is merged into the new game records;
locate EpicGameDao.replaceAll and the logout callers (EpicService.logout,
GOGService.logout) and change the deleteAll/deleteAllGames usage to the
upsertPreservingInstallStatus path so installed games remain visible after
logout.
In `@app/src/main/java/app/gamenative/service/epic/EpicService.kt`:
- Around line 307-318: The getWineStartCommand function currently returns a
nullable/Unit when getInstance() is null; update it so it always returns a
non-null String by handling the null case of getInstance()/epicManager: call
getInstance()?.epicManager?.getWineStartCommand(...) and if that yields null,
return a sensible default String or throw a clear exception; adjust the function
body around the getWineStartCommand invocation (referencing getInstance,
epicManager, and epicManager.getWineStartCommand) to ensure a concrete String is
returned in all paths.
♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/service/epic/EpicService.kt (1)
377-411: Hardcoded Toast strings should be instrings.xml.The Toast messages at lines 380, 394, and 408 use hardcoded strings. This was already flagged in a previous review.
🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/service/epic/EpicService.kt (1)
481-481: Unused variableinstance.The variable
instanceis declared but never used inonStartCommand. This appears to be dead code.♻️ Proposed fix
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Timber.tag("EPIC").d("onStartCommand() - action: ${intent?.action}") - val instance = getInstance() // Start as foreground service val notification = notificationHelper.createForegroundNotification("Connected") startForeground(3, notification)app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt (1)
12-12: Remove unused import.
Timberis imported but never used in this DAO interface.-import timber.log.Timber
| @Transaction | ||
| suspend fun replaceAll(games: List<EpicGame>) { | ||
| deleteAll() | ||
| insertAll(games) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all usages of replaceAll in the codebase
rg -n "replaceAll" --type=kt -B2 -A2Repository: utkarshdalal/GameNative
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Find all Kotlin files and search for replaceAll usage
fd "\.kt$" | head -20
echo "---"
# Search for replaceAll in all files
rg -n "replaceAll" -B3 -A3Repository: utkarshdalal/GameNative
Length of output: 5584
🏁 Script executed:
#!/bin/bash
# Search for calls to replaceAll on dao objects
rg "epicGameDao\.replaceAll|gogGameDao\.replaceAll" -B3 -A3
echo "---"
# Also check for variable-based calls
rg "\.replaceAll\(" app/src/main/java/app/gamenative --type-add "kt:*.kt" -B2 -A2 | grep -v "String\|replaceAll(\|winlator\|input"Repository: utkarshdalal/GameNative
Length of output: 70
🏁 Script executed:
#!/bin/bash
# Check for upsertPreservingInstallStatus method
rg "upsertPreservingInstallStatus" -B2 -A5
echo "---"
# Check the full EpicGameDao file to see all available methods
cat -n app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt | head -150Repository: utkarshdalal/GameNative
Length of output: 8108
🏁 Script executed:
#!/bin/bash
# Search for logout, clearDatabase, and related patterns
rg "clearDatabase|onLogout|logout" -B2 -A5
echo "---"
# Search for deleteAll on game daos
rg "\.deleteAll\(" -B2 -A2 | grep -v ".git"Repository: utkarshdalal/GameNative
Length of output: 50379
🏁 Script executed:
#!/bin/bash
# Search for replaceAll specifically in service and manager files
fd "\.kt$" app/src/main/java/app/gamenative/service | xargs grep -l "replaceAll" 2>/dev/null
echo "---"
# Also check if replaceAll is referenced in tests
fd "\.kt$" app/src/.*Test | xargs grep -l "replaceAll" 2>/dev/null
echo "---"
# Full context search for any .replaceAll calls (excluding String.replaceAll)
rg "\.replaceAll\(" app/src/main/java/app/gamenative/service -B2 -A2Repository: utkarshdalal/GameNative
Length of output: 193
🏁 Script executed:
#!/bin/bash
# Get SteamService and check clearDatabase implementation
cat -n app/src/main/java/app/gamenative/service/SteamService.kt | grep -A20 "fun clearDatabase"
echo "---"
# Check EpicService logout to see if it touches game database
cat -n app/src/main/java/app/gamenative/service/epic/EpicService.kt | grep -A30 "suspend fun logout"Repository: utkarshdalal/GameNative
Length of output: 2659
🏁 Script executed:
#!/bin/bash
# Find deleteAllGames implementation in EpicManager
rg -n "fun deleteAllGames" app/src/main/java/app/gamenative/service/epic -B2 -A10
echo "---"
# Also check EpicManager file structure
fd "EpicManager.kt"Repository: utkarshdalal/GameNative
Length of output: 1224
🏁 Script executed:
#!/bin/bash
# Check GOGService logout implementation
rg -n "suspend fun logout" app/src/main/java/app/gamenative/service/gog -A30
echo "---"
# Check GOGManager.deleteAllGames if it exists
rg -n "fun deleteAllGames" app/src/main/java/app/gamenative/service/gog -B2 -A10Repository: utkarshdalal/GameNative
Length of output: 4594
Call upsertPreservingInstallStatus instead of deleteAll() during logout to preserve local installation tracking.
Both EpicService.logout() and GOGService.logout() call deleteAllGames(), which removes all game records from the database including isInstalled, installPath, installSize, lastPlayed, and playTime. Since games remain physically installed on the device, users should see their installed games when logging back in. Replace the deleteAllGames() calls with selective clearing that preserves install state, or use a method equivalent to upsertPreservingInstallStatus during logout to maintain this critical data.
🤖 Prompt for AI Agents
In `@app/src/main/java/app/gamenative/db/dao/EpicGameDao.kt` around lines 81 - 85,
The current replaceAll implementation in EpicGameDao calls deleteAll() then
insertAll(games), which causes EpicService.logout() and GOGService.logout()
(which call deleteAllGames()) to wipe install-related fields; update the logout
flow to call EpicGameDao.upsertPreservingInstallStatus (or change replaceAll to
perform an upsert that preserves
isInstalled/installPath/installSize/lastPlayed/playTime) instead of deleteAll(),
ensuring existing local install metadata is merged into the new game records;
locate EpicGameDao.replaceAll and the logout callers (EpicService.logout,
GOGService.logout) and change the deleteAll/deleteAllGames usage to the
upsertPreservingInstallStatus path so installed games remain visible after
logout.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will chat with Utkarsh
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@phobos665, sounds good! For context, there's an existing pattern in the codebase where Steam's clearDatabase() already preserves installation state (AppInfo table) during logout for this exact reason—games stay physically installed, so users should see them when logging back in. The same principle would apply to Epic Games data. Looking forward to hearing what you and Utkarsh decide!
🐰✨
🧠 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.
…t's no longer used for debugging purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@app/src/main/java/app/gamenative/service/epic/EpicManager.kt`:
- Around line 348-378: The getInstalledExe suspend function performs heavy File
I/O (File(game.installPath).walk(), filtering/sorting) on the caller thread;
wrap the I/O-intensive work in withContext(Dispatchers.IO) so it runs off the
main thread. Specifically, move the creation of installDir, the exists check,
the File.walk() chain, and selection of mainExe into a
withContext(Dispatchers.IO) block inside getInstalledExe, preserve the same
return values (empty string or mainExe.absolutePath), and keep Timber logging
inside or around that block; mirror the pattern used by GOGManager's
withContext(Dispatchers.IO).
In `@app/src/main/res/values-ro/strings.xml`:
- Line 1005: The string resource epic_login_auth_example contains English
example text in the Romanian locale; update the value to a Romanian translation
(for example replace 'Exemplu: authorization code: "example"' with the Romanian
equivalent such as 'Exemplu: cod de autorizare: "exemplu"'), preserving
punctuation and quotes exactly as in the original so the example format remains
the same.
In `@app/src/main/res/values-zh-rCN/strings.xml`:
- Around line 992-994: Replace the English phrasing and ASCII punctuation in the
epic_login_auth_example string with fully localized Chinese wording and
punctuation: update the value for the string resource named
epic_login_auth_example to use Chinese text (e.g., “示例:授权码:‘example’” with
full‑width Chinese punctuation and quotes) so it matches the surrounding zh‑CN
translations and UI style.
♻️ Duplicate comments (13)
app/src/main/res/values-pt-rBR/strings.xml (1)
867-867: Untranslated English text in Portuguese locale.The string contains English text
authorization code: "example"that should be translated to Portuguese for consistency with the rest of the file.app/src/main/res/values-ro/strings.xml (1)
49-50: Use informal “tu” form to stay consistent.These two strings are still in formal address while the rest of the file uses informal tone.
✅ Suggested fix
- <string name="epic_cancel_download_message">Sigur doriți să anulați descărcarea aplicației?</string> - <string name="epic_delete_download_message">Ștergeți toate datele descărcate pentru acest joc?</string> + <string name="epic_cancel_download_message">Sigur vrei să anulezi descărcarea aplicației?</string> + <string name="epic_delete_download_message">Ștergi toate datele descărcate pentru acest joc?</string>app/src/main/res/values/strings.xml (1)
1022-1029: Polishepic_logout_failedwording for consistency.
Line 1028 still reads a bit off compared to other logout failure strings.💬 Suggested wording
- <string name="epic_logout_failed">Failure to logout of Epic Games, please try again</string> + <string name="epic_logout_failed">Failed to log out of Epic Games, please try again</string>app/src/main/java/app/gamenative/service/epic/EpicManager.kt (3)
914-916: UseappIdin the missing-game log message.
Line 915 logs$game.appNamewhengameis null, which prints a misleading value.💬 Suggested fix
- Timber.tag("Epic").w("Game not found in database: $game.appName") + Timber.tag("Epic").w("Game not found in database: $appId")
288-300: Close the OkHttpResponseinfetchLibrary.
Line 290 executes the call withoutuse, which can leak connections during large library syncs. This is a consistent pattern throughout the codebase where.execute().use()is the standard approach (see lines 777, 883, and other network calls).🧩 Suggested fix
- val response = httpClient.newCall(request).execute() - - if (!response.isSuccessful) { - val error = response.body?.string() ?: "Unknown error" - Timber.tag("Epic").e("Library fetch failed: ${response.code} - $error") - return@withContext Result.failure(Exception("HTTP ${response.code}: $error")) - } - - val body = response.body?.string() + val body = httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + val error = response.body?.string() ?: "Unknown error" + Timber.tag("Epic").e("Library fetch failed: ${response.code} - $error") + return@withContext Result.failure(Exception("HTTP ${response.code}: $error")) + } + + response.body?.string() + }
416-425: Close the OkHttpResponseinfetchGameInfoandfetchLibrary.Both functions call
execute()withoutuse, causing connection leaks when early returns occur (on HTTP errors or empty responses). This contradicts the pattern used elsewhere in the codebase (e.g., in the manifest fetch just below).🧩 Suggested fix
- val response = httpClient.newCall(request).execute() - - if (!response.isSuccessful) { - Timber.w("Failed to fetch game info for ${game.catalogItemId}: ${response.code}") - return@withContext Result.failure(Exception("Could not fetch game info: ${response.code}")) - } - - val body = response.body?.string() + val body = httpClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + Timber.w("Failed to fetch game info for ${game.catalogItemId}: ${response.code}") + return@withContext Result.failure(Exception("Could not fetch game info: ${response.code}")) + } + + response.body?.string() + }app/src/main/res/values-de/strings.xml (1)
44-51: Use informal “du” to match the rest of the German UI.These Epic strings should align with the informal tone used elsewhere (e.g., Steam strings).
✏️ Suggested tweak
- <string name="epic_cancel_download_message">Möchten Sie den Download der App wirklich abbrechen?</string> + <string name="epic_cancel_download_message">Möchtest du den Download der App wirklich abbrechen?</string>app/src/main/res/values-zh-rTW/strings.xml (1)
1006-1012: Expose the actual logout error inepic_logout_failed.Add a
%splaceholder and pass the error message, matching the GOG flow.✏️ Suggested tweak
- <string name="epic_logout_failed">無法從 Epic Games 登出,請重試</string> + <string name="epic_logout_failed">無法從 Epic Games 登出:%s</string>app/src/main/res/values-fr/strings.xml (2)
49-56: Add French typographic spaces before “?” and “:”.Matches the style used elsewhere in this file.
✏️ Suggested tweak
- <string name="epic_cancel_download_message">Êtes-vous sûr de vouloir annuler le téléchargement de l\'application?</string> - <string name="epic_delete_download_message">Supprimer toutes les données téléchargées pour ce jeu?</string> + <string name="epic_cancel_download_message">Êtes-vous sûr de vouloir annuler le téléchargement de l\'application ?</string> + <string name="epic_delete_download_message">Supprimer toutes les données téléchargées pour ce jeu ?</string> <string name="epic_cloud_sync_starting">Démarrage de la synchronisation des sauvegardes cloud…</string> <string name="epic_cloud_sync_success">Sauvegardes cloud synchronisées avec succès</string> <string name="epic_cloud_sync_failed">Échec de la synchronisation des sauvegardes cloud</string> - <string name="epic_cloud_sync_error">Erreur de synchronisation des sauvegardes cloud: %1$s</string> - <string name="epic_uninstall_failed">Échec de la désinstallation: %1$s</string> - <string name="epic_uninstall_error">Erreur de désinstallation: %1$s</string> + <string name="epic_cloud_sync_error">Erreur de synchronisation des sauvegardes cloud : %1$s</string> + <string name="epic_uninstall_failed">Échec de la désinstallation : %1$s</string> + <string name="epic_uninstall_error">Erreur de désinstallation : %1$s</string>
1000-1003: Localize the auth example and align the placeholder with the flow.The example is still English, and the placeholder mentions an URL while the flow says “code”.
✏️ Suggested tweak
- <string name="epic_login_auth_example">Exemple : authorization code: "example"</string> + <string name="epic_login_auth_example">Exemple : code d’autorisation : « exemple »</string> @@ - <string name="epic_login_auth_code_placeholder">Collez le code ou l\'URL ici</string> + <string name="epic_login_auth_code_placeholder">Collez le code ici</string>app/src/main/java/app/gamenative/service/epic/EpicService.kt (3)
317-322:runBlockinghere can still block the caller thread.Even with sync APIs, prefer
Dispatchers.IO(or make this suspend) to avoid ANR if invoked from UI.🛠️ Suggested change
- val game = runBlocking { instance.epicManager.getGameById(appId) } + val game = runBlocking(Dispatchers.IO) { instance.epicManager.getGameById(appId) }
324-338: MakeactiveDownloadsinsertion atomic.
containsKey+ assignment can race under concurrent calls and start duplicate downloads.🔒 Suggested change
- if (instance.activeDownloads.containsKey(appId)) { - Timber.tag("Epic").w("Download already in progress for $appId") - return Result.success(instance.activeDownloads[appId]!!) - } + instance.activeDownloads[appId]?.let { existing -> + Timber.tag("Epic").w("Download already in progress for $appId") + return Result.success(existing) + } @@ - instance.activeDownloads[appId] = downloadInfo - downloadInfo.setActive(true) + val existing = instance.activeDownloads.putIfAbsent(appId, downloadInfo) + if (existing != null) { + Timber.tag("Epic").w("Download already in progress for $appId") + return Result.success(existing) + } + downloadInfo.setActive(true)
363-397: Move user‑facing strings to resources.Toasts and the foreground notification text should be localized and maintainable via
strings.xml.💡 Suggested refactor
- "Download completed successfully!", + context.getString(R.string.epic_download_success), @@ - "Download failed: ${error?.message ?: "Unknown error"}", + context.getString( + R.string.epic_download_failed, + error?.message ?: context.getString(R.string.unknown_error) + ), @@ - "Download error: ${e.message ?: "Unknown error"}", + context.getString( + R.string.epic_download_error, + e.message ?: context.getString(R.string.unknown_error) + ), @@ - val notification = notificationHelper.createForegroundNotification("Connected") + val notification = notificationHelper.createForegroundNotification( + getString(R.string.epic_service_connected) + )Also applies to: 470-471
🧹 Nitpick comments (2)
app/src/main/res/values-pt-rBR/strings.xml (1)
50-57: Consider moving these Epic strings to the Epic Games section.These Epic-related strings are placed in the middle of the Winlator section (between
title_ubuntufsand the "Win Components" comment). For better maintainability and consistency, consider relocating them to the main Epic Games integration section at lines 855–893.app/src/main/java/app/gamenative/ui/model/MainViewModel.kt (1)
11-35: Unused imports detected.
LibraryItem(line 11) does not appear to be used in this file.EpicCloudSavesManager(line 19) is imported but line 321 uses the fully qualified name instead.Consider removing unused imports or using the short import name at the call site for consistency.
♻️ Suggested fix
-import app.gamenative.data.LibraryItem import app.gamenative.di.IAppThemeAnd either remove the
EpicCloudSavesManagerimport:-import app.gamenative.service.epic.EpicCloudSavesManagerOr use the short name at line 321:
- val syncSuccess = app.gamenative.service.epic.EpicCloudSavesManager.syncCloudSaves( + val syncSuccess = EpicCloudSavesManager.syncCloudSaves(
| suspend fun getInstalledExe(appId: Int): String { | ||
| // Strip EPIC_ prefix to get the raw Epic app name | ||
| val game = getGameById(appId) | ||
| if (game == null || !game.isInstalled || game.installPath.isEmpty()) { | ||
| Timber.tag("Epic").e("Game not installed: $appId") | ||
| return "" | ||
| } | ||
|
|
||
| // For now, return the install path - actual executable detection would require | ||
| // parsing the game's launch manifest or config files | ||
| // Most Epic games have a .exe in the root or Binaries folder | ||
| val installDir = File(game.installPath) | ||
| if (!installDir.exists()) { | ||
| Timber.tag("Epic").e("Install directory does not exist: ${game.installPath}") | ||
| return "" | ||
| } | ||
|
|
||
| // Try to find the main executable | ||
| // Common patterns: Game.exe, GameName.exe, or in Binaries/Win64/ | ||
| val exeFiles = installDir.walk() | ||
| .filter { it.extension.equals("exe", ignoreCase = true) } | ||
| .filter { !it.name.contains("UnityCrashHandler", ignoreCase = true) } | ||
| .filter { !it.name.contains("UnrealCEFSubProcess", ignoreCase = true) } | ||
| .sortedBy { it.absolutePath.length } // Prefer shorter paths (usually main exe) | ||
| .toList() | ||
|
|
||
| val mainExe = exeFiles.firstOrNull() | ||
| if (mainExe != null) { | ||
| Timber.tag("Epic").i("Found executable: ${mainExe.absolutePath}") | ||
| return mainExe.absolutePath | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
rg -n "getInstalledExe\(" --type=kt app/src/main/java/app/gamenativeRepository: utkarshdalal/GameNative
Length of output: 93
🏁 Script executed:
rg -n "getInstalledExe\(" app/src/main/java/app/gamenativeRepository: utkarshdalal/GameNative
Length of output: 1699
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/service/epic/EpicService.kt | sed -n '300,310p'Repository: utkarshdalal/GameNative
Length of output: 532
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt | sed -n '1940,1955p'Repository: utkarshdalal/GameNative
Length of output: 733
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt | sed -n '1890,1900p'Repository: utkarshdalal/GameNative
Length of output: 608
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt | sed -n '1,50p'Repository: utkarshdalal/GameNative
Length of output: 2626
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt | sed -n '2020,2030p'Repository: utkarshdalal/GameNative
Length of output: 623
🏁 Script executed:
cat -n app/src/main/java/app/gamenative/service/gog/GOGManager.kt | sed -n '574,590p'Repository: utkarshdalal/GameNative
Length of output: 979
Ensure getInstalledExe runs on Dispatchers.IO to avoid ANRs.
The File.walk() + sort operation on line 367 is I/O-intensive. This function is called from the UI layer (XServerScreen uses runBlocking at line 1948), which means blocking operations execute on the main thread. GOGManager already follows the correct pattern with withContext(Dispatchers.IO) at line 574—apply the same approach here.
🧩 Suggested fix
-suspend fun getInstalledExe(appId: Int): String {
+suspend fun getInstalledExe(appId: Int): String = withContext(Dispatchers.IO) {
// Strip EPIC_ prefix to get the raw Epic app name
val game = getGameById(appId)
if (game == null || !game.isInstalled || game.installPath.isEmpty()) {
Timber.tag("Epic").e("Game not installed: $appId")
- return ""
+ return@withContext ""
}
@@
if (!installDir.exists()) {
Timber.tag("Epic").e("Install directory does not exist: ${game.installPath}")
- return ""
+ return@withContext ""
}
@@
if (mainExe != null) {
Timber.tag("Epic").i("Found executable: ${mainExe.absolutePath}")
- return mainExe.absolutePath
+ return@withContext mainExe.absolutePath
}
Timber.tag("Epic").w("No executable found in ${game.installPath}")
- return ""
+ return@withContext ""
}🤖 Prompt for AI Agents
In `@app/src/main/java/app/gamenative/service/epic/EpicManager.kt` around lines
348 - 378, The getInstalledExe suspend function performs heavy File I/O
(File(game.installPath).walk(), filtering/sorting) on the caller thread; wrap
the I/O-intensive work in withContext(Dispatchers.IO) so it runs off the main
thread. Specifically, move the creation of installDir, the exists check, the
File.walk() chain, and selection of mainExe into a withContext(Dispatchers.IO)
block inside getInstalledExe, preserve the same return values (empty string or
mainExe.absolutePath), and keep Timber logging inside or around that block;
mirror the pattern used by GOGManager's withContext(Dispatchers.IO).
| <!-- Epic Login Dialog --> | ||
| <string name="epic_login_title">Autentificare Epic Games</string> | ||
| <string name="epic_login_auto_auth_info">Apasă \'Deschide autentificare Epic\' și autentifică-te. După autentificare, copiază codul de autorizare din ecranul de rezultat și lipește-l mai jos</string> | ||
| <string name="epic_login_auth_example">Exemplu: authorization code: "example"</string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Localize the example text.
This example is still in English inside a Romanian locale string.
✅ Suggested fix
- <string name="epic_login_auth_example">Exemplu: authorization code: "example"</string>
+ <string name="epic_login_auth_example">Exemplu: cod de autorizare: „exemplu”</string>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <string name="epic_login_auth_example">Exemplu: authorization code: "example"</string> | |
| <string name="epic_login_auth_example">Exemplu: cod de autorizare: „exemplu"</string> |
🤖 Prompt for AI Agents
In `@app/src/main/res/values-ro/strings.xml` at line 1005, The string resource
epic_login_auth_example contains English example text in the Romanian locale;
update the value to a Romanian translation (for example replace 'Exemplu:
authorization code: "example"' with the Romanian equivalent such as 'Exemplu:
cod de autorizare: "exemplu"'), preserving punctuation and quotes exactly as in
the original so the example format remains the same.
| <string name="epic_login_auto_auth_info">点击\'打开 Epic 登录\'并登录。登录后,从结果屏幕复制授权码并粘贴到下方</string> | ||
| <string name="epic_login_auth_example">示例: authorization code: "example"</string> | ||
| <string name="epic_login_open_button">打开 Epic 登录</string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Localize the auth example text for zh‑CN.
epic_login_auth_example still uses English phrasing and ASCII punctuation; consider a fully localized example for consistency with the rest of the UI.
💬 Suggested wording
- <string name="epic_login_auth_example">示例: authorization code: "example"</string>
+ <string name="epic_login_auth_example">示例:授权码:“示例”</string>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <string name="epic_login_auto_auth_info">点击\'打开 Epic 登录\'并登录。登录后,从结果屏幕复制授权码并粘贴到下方</string> | |
| <string name="epic_login_auth_example">示例: authorization code: "example"</string> | |
| <string name="epic_login_open_button">打开 Epic 登录</string> | |
| <string name="epic_login_auto_auth_info">点击\'打开 Epic 登录\'并登录。登录后,从结果屏幕复制授权码并粘贴到下方</string> | |
| <string name="epic_login_auth_example">示例:授权码:"示例"</string> | |
| <string name="epic_login_open_button">打开 Epic 登录</string> |
🤖 Prompt for AI Agents
In `@app/src/main/res/values-zh-rCN/strings.xml` around lines 992 - 994, Replace
the English phrasing and ASCII punctuation in the epic_login_auth_example string
with fully localized Chinese wording and punctuation: update the value for the
string resource named epic_login_auth_example to use Chinese text (e.g.,
“示例:授权码:‘example’” with full‑width Chinese punctuation and quotes) so it matches
the surrounding zh‑CN translations and UI style.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@app/src/main/res/values/strings.xml`:
- Around line 1012-1013: Update the two string resources
epic_login_auto_auth_info and epic_login_auth_example to use user‑friendly
wording instead of the variable-like “authorizationCode”: change
epic_login_auto_auth_info to something like "Tap 'Open Epic Login' and sign in.
Once logged in, copy the authorization code from the result screen and paste it
below." and change epic_login_auth_example to a cleaner example such as
"Example: authorization code: example" (remove variable casing/quotes).
- Around line 48-64: Add the eight missing Ukrainian translations to
values-uk/strings.xml by defining string resources for
epic_cancel_download_message, epic_delete_download_message,
epic_cloud_sync_starting, epic_cloud_sync_success, epic_cloud_sync_failed,
epic_cloud_sync_error, epic_uninstall_failed, and epic_uninstall_error; ensure
the translated text preserves any placeholders like %1$s and any
punctuation/ellipsis formatting from the original English entries so runtime
formatting remains correct.
♻️ Duplicate comments (1)
app/src/main/res/values/strings.xml (1)
1028-1028: Align Epic logout failure phrasing with other providers.
The current wording is inconsistent with existing logout error strings.💡 Suggested fix
- <string name="epic_logout_failed">Failure to logout of Epic Games, please try again</string> + <string name="epic_logout_failed">Failed to logout from Epic Games, please try again</string>
| <string name="epic_uninstall_game_title">Uninstall Game</string> | ||
| <string name="epic_uninstall_game_message">Are you sure you want to uninstall %1$s? This action cannot be undone.</string> | ||
| <string name="epic_install_game_title">Download Game</string> | ||
| <string name="epic_install_game_message">The app being installed has the following space requirements. Would you like to proceed?\n\n\tDownload Size: %1$s\n\tSize on Disk: %2$s\n\tAvailable Space: %3$s</string> | ||
| <string name="epic_game_not_found">Game not found</string> | ||
| <string name="epic_starting_download">Starting download: %1$s</string> | ||
| <string name="epic_starting_download_with_dlcs">Starting download: %1$s + DLCs</string> | ||
| <string name="epic_download_failed">Failed to start download: %1$s</string> | ||
| <string name="epic_download_error">Download error: %1$s</string> | ||
| <string name="epic_cancel_download_message">Are you sure you want to cancel the download of the app?</string> | ||
| <string name="epic_delete_download_message">Delete all downloaded data for this game?</string> | ||
| <string name="epic_cloud_sync_starting">Starting cloud save sync…</string> | ||
| <string name="epic_cloud_sync_success">Cloud saves synced successfully</string> | ||
| <string name="epic_cloud_sync_failed">Cloud save sync failed</string> | ||
| <string name="epic_cloud_sync_error">Cloud save sync error: %1$s</string> | ||
| <string name="epic_uninstall_failed">Uninstall failed: %1$s</string> | ||
| <string name="epic_uninstall_error">Uninstall error: %1$s</string> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, list all localized strings files to understand the structure
find app/src/main/res -name "strings.xml" -type f | sortRepository: utkarshdalal/GameNative
Length of output: 465
🏁 Script executed:
#!/bin/bash
# Run the verification script from the review comment
python - <<'PY'
import glob, xml.etree.ElementTree as ET
base = "app/src/main/res/values/strings.xml"
def extract(path):
tree = ET.parse(path)
return {el.attrib.get("name") for el in tree.getroot().iter("string") if "name" in el.attrib}
base_keys = {k for k in extract(base) if k and k.startswith("epic_")}
print(f"Total epic_* keys in base file: {len(base_keys)}")
print(f"Epic keys: {sorted(base_keys)[:10]}...") # Show first 10
for path in sorted(glob.glob("app/src/main/res/values-*/strings.xml")):
try:
keys = extract(path)
except Exception as e:
print(f"{path}: parse error: {e}")
continue
missing = sorted(base_keys - keys)
if missing:
print(f"\n{path}: missing {len(missing)} epic_ keys:")
for k in missing[:5]: # Show first 5
print(f" - {k}")
if len(missing) > 5:
print(f" ... and {len(missing) - 5} more")
else:
print(f"{path}: all {len(base_keys)} epic_ keys present")
PYRepository: utkarshdalal/GameNative
Length of output: 153
🏁 Script executed:
#!/bin/bash
# Check lines 1002-1029 for Epic login/logout strings mentioned in scratchpad
if [ -f "app/src/main/res/values/strings.xml" ]; then
echo "=== Lines 1000-1035 from strings.xml ==="
sed -n '1000,1035p' app/src/main/res/values/strings.xml
fiRepository: utkarshdalal/GameNative
Length of output: 2141
🏁 Script executed:
#!/bin/bash
# Count epic_* strings in base and localized files
echo "=== Epic string coverage by locale ==="
base_count=$(rg 'name="epic_' app/src/main/res/values/strings.xml | wc -l)
echo "Base file (values/strings.xml): $base_count epic_* strings"
for file in app/src/main/res/values-*/strings.xml; do
locale=$(basename $(dirname $file))
count=$(rg 'name="epic_' "$file" | wc -l)
echo "$locale: $count epic_* strings"
doneRepository: utkarshdalal/GameNative
Length of output: 428
🏁 Script executed:
#!/bin/bash
# Check specific epic_* keys that appear in lines 48-64 in localized files
echo "=== Checking if lines 48-64 Epic strings exist in localized files ==="
keys=("epic_uninstall_game_title" "epic_install_game_title" "epic_cloud_sync_starting" "epic_download_failed")
for locale_dir in app/src/main/res/values-*/; do
locale=$(basename $locale_dir)
file="${locale_dir}strings.xml"
if [ -f "$file" ]; then
missing=()
for key in "${keys[@]}"; do
if ! grep -q "name=\"$key\"" "$file"; then
missing+=("$key")
fi
done
if [ ${`#missing`[@]} -gt 0 ]; then
echo "$locale: MISSING ${`#missing`[@]} keys (e.g., ${missing[0]})"
else
echo "$locale: All checked keys present"
fi
fi
doneRepository: utkarshdalal/GameNative
Length of output: 495
🏁 Script executed:
#!/bin/bash
# Find which epic_* keys are missing from Ukrainian locale
echo "=== Epic strings missing from values-uk ==="
rg 'name="epic_' app/src/main/res/values/strings.xml -o | sed 's/.*name="//;s/".*//' | sort > /tmp/base_keys.txt
rg 'name="epic_' app/src/main/res/values-uk/strings.xml -o | sed 's/.*name="//;s/".*//' | sort > /tmp/uk_keys.txt
comm -23 /tmp/base_keys.txt /tmp/uk_keys.txtRepository: utkarshdalal/GameNative
Length of output: 158
🏁 Script executed:
#!/bin/bash
# Verify the grammar issue in epic_logout_failed
echo "=== Checking epic_logout_failed string ==="
rg 'epic_logout_failed' app/src/main/res/values/strings.xml -A 0 -B 0Repository: utkarshdalal/GameNative
Length of output: 206
🏁 Script executed:
#!/bin/bash
# Get the exact list of missing epic_* keys in Ukrainian
echo "=== Missing epic_* keys in values-uk (Ukrainian) ==="
comm -23 <(rg 'name="epic_' app/src/main/res/values/strings.xml -o | sed 's/.*name="//;s/".*//' | sort) <(rg 'name="epic_' app/src/main/res/values-uk/strings.xml -o | sed 's/.*name="//;s/".*//' | sort)Repository: utkarshdalal/GameNative
Length of output: 180
🏁 Script executed:
#!/bin/bash
# Try a different approach to find missing keys
base_file="app/src/main/res/values/strings.xml"
uk_file="app/src/main/res/values-uk/strings.xml"
# Extract all epic_* keys from base file
grep 'name="epic_' "$base_file" | sed 's/.*name="//;s/".*//' | sort > /tmp/base_epic_keys.txt
# Extract all epic_* keys from Ukrainian file
grep 'name="epic_' "$uk_file" | sed 's/.*name="//;s/".*//' | sort > /tmp/uk_epic_keys.txt
echo "=== Base file has $(wc -l < /tmp/base_epic_keys.txt) epic_* keys ==="
echo "=== Ukrainian file has $(wc -l < /tmp/uk_epic_keys.txt) epic_* keys ==="
echo ""
echo "=== Missing keys in Ukrainian (first 10) ==="
grep -v -f /tmp/uk_epic_keys.txt /tmp/base_epic_keys.txt | head -10Repository: utkarshdalal/GameNative
Length of output: 386
🏁 Script executed:
#!/bin/bash
# Double-check that the 8 missing strings are indeed in the lines 48-64 range or elsewhere
echo "=== Locating the 8 missing Ukrainian strings in base file ==="
grep -n 'name="epic_cancel_download_message\|epic_cloud_sync_error\|epic_cloud_sync_failed\|epic_cloud_sync_starting\|epic_cloud_sync_success\|epic_delete_download_message\|epic_uninstall_error\|epic_uninstall_failed"' app/src/main/res/values/strings.xmlRepository: utkarshdalal/GameNative
Length of output: 824
Add translations for 8 missing Epic strings in Ukrainian locale (values-uk).
The following strings from lines 57–64 are missing from values-uk/strings.xml:
- epic_cancel_download_message
- epic_delete_download_message
- epic_cloud_sync_starting
- epic_cloud_sync_success
- epic_cloud_sync_failed
- epic_cloud_sync_error
- epic_uninstall_failed
- epic_uninstall_error
All other supported locales (Danish, German, French, Italian, Portuguese, Romanian, Simplified Chinese, Traditional Chinese) have complete translations. Ukrainian needs these 8 strings translated.
🤖 Prompt for AI Agents
In `@app/src/main/res/values/strings.xml` around lines 48 - 64, Add the eight
missing Ukrainian translations to values-uk/strings.xml by defining string
resources for epic_cancel_download_message, epic_delete_download_message,
epic_cloud_sync_starting, epic_cloud_sync_success, epic_cloud_sync_failed,
epic_cloud_sync_error, epic_uninstall_failed, and epic_uninstall_error; ensure
the translated text preserves any placeholders like %1$s and any
punctuation/ellipsis formatting from the original English entries so runtime
formatting remains correct.
Overview
This PR is to add Epic Games Integration
Features
Architecture
Epic
EpicApiClient - All Auth responsibilities for API Requests (Currently authClient, but will refactor later)
EpicAuthManager - Manages Auth responsibilities
EpicManager - Library Management
EpicService - Interface for all Epic operations
EpicConstants - Constants for Epic-related functionality such as IDs, names etc.
EpicDownloadManager - Handles Downloading Logic
EpicCloudSavesManager - Handles Cloud Saves
Manifest/
EpicManifest - Handles overall Logic for Manifests including Binary Manifest Parsing
JsonManifestParser - Handles parsing of JSON manifests
ManifestUtls - Common utils required for parsing both JSON and Binary manifests
Other Files:
Testing
I have created tests for the most complex areas, mostly manifest parsing.
Summary by cubic
Add Epic Games Store support: login, library sync, installs/downloads, launching, and cloud saves with new UI and database pieces. This brings Epic games into the library with proper filtering, icons, and service wiring.
New Features
Migration
Written for commit a09a8b6. Summary will update on new commits.
Summary by CodeRabbit
New Features
UI
Infrastructure
Database
Tests
Strings
✏️ Tip: You can customize this high-level summary in your review settings.