-
-
Notifications
You must be signed in to change notification settings - Fork 410
Speed up external #1779
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
Speed up external #1779
Changes from all commits
8e5f360
3c7fd27
8a78fd7
bad4d39
65c3213
5039202
74b7a7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -316,6 +316,16 @@ class AmazonService : Service() { | |
| return if (game.isInstalled && game.installPath.isNotEmpty()) game.installPath else null | ||
| } | ||
|
|
||
| /** Persist a new install path for [appId] after an on-disk migration. */ | ||
| fun updateInstallPath(appId: Int, path: String) { | ||
| runBlocking(Dispatchers.IO) { | ||
| val game = instance?.amazonManager?.getGameByAppId(appId) ?: return@runBlocking | ||
| if (game.isInstalled && game.installPath != path) { | ||
| instance?.amazonManager?.markInstalled(game.productId, path, game.installSize, game.versionId) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Prompt for AI agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Convert appId to productId via DB lookup. */ | ||
| fun getProductIdByAppId(appId: Int): String? { | ||
| return getAmazonGameByAppId(appId)?.productId | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -366,6 +366,15 @@ class EpicService : Service() { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| fun updateInstallPath(appId: Int, path: String) { | ||||||||||||||||||||||||||||||||||
| runBlocking(Dispatchers.IO) { | ||||||||||||||||||||||||||||||||||
| val game = getInstance()?.epicManager?.getGameById(appId) ?: return@runBlocking | ||||||||||||||||||||||||||||||||||
| if (game.installPath != path) { | ||||||||||||||||||||||||||||||||||
| getInstance()?.epicManager?.updateGame(game.copy(installPath = path)) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+369
to
+375
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Double getInstance() call creates a silent-failure window if the service is destroyed between the two calls. Capture the instance once at the top of the runBlocking block and reuse it, same as the AmazonService.updateInstallPath pattern. Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| suspend fun getInstalledExe(appId: Int): String { | ||||||||||||||||||||||||||||||||||
| return getInstance()?.epicManager?.getInstalledExe(appId) ?: "" | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1043,11 +1043,30 @@ object ContainerUtils { | |
| } | ||
| } | ||
|
|
||
| if (gameFolderPath != null) { | ||
| val resolvedGameFolderPath = if (gameSource == GameSource.CUSTOM_GAME) { | ||
| gameFolderPath | ||
| } else { | ||
| StorageUtils.resolveLegacyGameDir(gameFolderPath) | ||
| } | ||
|
|
||
| if (resolvedGameFolderPath != null && resolvedGameFolderPath != gameFolderPath) { | ||
| when (gameSource) { | ||
| GameSource.GOG -> | ||
| GOGService.updateInstallPath(extractGameIdFromContainerId(appId).toString(), resolvedGameFolderPath) | ||
| GameSource.EPIC -> | ||
| EpicService.updateInstallPath(extractGameIdFromContainerId(appId), resolvedGameFolderPath) | ||
| GameSource.AMAZON -> | ||
| runCatching { extractGameIdFromContainerId(appId) }.getOrNull() | ||
| ?.let { AmazonService.updateInstallPath(it, resolvedGameFolderPath) } | ||
| else -> {} | ||
| } | ||
| } | ||
|
Comment on lines
+1046
to
+1063
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Make legacy-directory migration and provider metadata updates atomic or retryable. The directory can be moved successfully while the provider update silently no-ops because its service is not running, leaving the container path and database path inconsistent.
📍 Affects 4 files
🤖 Prompt for AI Agents |
||
|
|
||
| if (resolvedGameFolderPath != null) { | ||
| // Check if A: drive is already mapped to the correct path | ||
| var hasCorrectADrive = false | ||
| for (drive in Container.drivesIterator(container.drives)) { | ||
| if (drive[0] == "A" && drive[1] == gameFolderPath) { | ||
| if (drive[0] == "A" && drive[1] == resolvedGameFolderPath) { | ||
| hasCorrectADrive = true | ||
| break | ||
| } | ||
|
|
@@ -1058,7 +1077,7 @@ object ContainerUtils { | |
| val currentDrives = container.drives | ||
| // Rebuild drives string, excluding existing A: drive and adding new one | ||
| val drivesBuilder = StringBuilder() | ||
| drivesBuilder.append("A:$gameFolderPath") | ||
| drivesBuilder.append("A:$resolvedGameFolderPath") | ||
|
|
||
| // Add all other drives (excluding A:) | ||
| for (drive in Container.drivesIterator(currentDrives)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,6 +98,55 @@ object StorageUtils { | |
| return result | ||
| } | ||
|
|
||
| private const val PUBLIC_INSTALL_DIR_NAME = "GameNative" | ||
|
|
||
| /** | ||
| * Maps an app-specific dir (<volume>/Android/data/<pkg>/files) to a public install root | ||
| * (<volume>/GameNative). MediaProvider disables FUSE kernel caching under Android/data, | ||
| * making per-open metadata ops ~1000x slower there; public dirs get normal dcache treatment. | ||
| */ | ||
| fun publicInstallRoot(appFilesDir: File): File? { | ||
| val path = appFilesDir.absolutePath | ||
| val idx = path.indexOf("/Android/data/") | ||
| if (idx <= 0) return null | ||
| return File(path.substring(0, idx), PUBLIC_INSTALL_DIR_NAME) | ||
| } | ||
|
|
||
| fun ensureInstallRoot(dir: File): Boolean { | ||
| if (!dir.isDirectory && !dir.mkdirs()) return false | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Modern builds cannot create the proposed Prompt for AI agents |
||
| runCatching { File(dir, ".nomedia").createNewFile() } | ||
| return true | ||
| } | ||
|
|
||
| fun preferredInstallRoot(appFilesDir: File): String { | ||
| val public = publicInstallRoot(appFilesDir) | ||
| if (public != null && ensureInstallRoot(public)) return public.absolutePath | ||
| return appFilesDir.absolutePath | ||
| } | ||
|
|
||
| fun resolveLegacyGameDir(path: String?): String? { | ||
| if (path.isNullOrBlank()) return path | ||
| val idx = path.indexOf("/Android/data/") | ||
| if (idx <= 0) return path | ||
| val filesIdx = path.indexOf("/files/", idx) | ||
| if (filesIdx < 0) return path | ||
| val legacyRoot = File(path.substring(0, filesIdx + "/files".length)) | ||
| val rel = path.substring(filesIdx + "/files/".length) | ||
| val src = File(path) | ||
| val publicRoot = publicInstallRoot(legacyRoot) ?: return path | ||
| val dst = File(publicRoot, rel) | ||
| if (!src.isDirectory) return if (dst.isDirectory) dst.absolutePath else path | ||
| if (dst.exists() || !ensureInstallRoot(publicRoot)) return path | ||
| dst.parentFile?.mkdirs() | ||
| return if (src.renameTo(dst)) { | ||
| Timber.i("Migrated game dir $path to ${dst.absolutePath}") | ||
| dst.absolutePath | ||
| } else { | ||
| Timber.w("Could not migrate $path; leaving in place") | ||
| path | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Gets all app-specific external files directories, using StorageManager as a fallback | ||
| * for cases where context.getExternalFilesDirs(null) might return null or incomplete results | ||
|
|
||
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.
P1: Existing external GOG, Epic, and Amazon installs become unreachable after startup migration: only the preference moves, while game directories stay under
Android/data/.../files. Migrate those directories (or retain legacy roots for each service) before changing the persisted root.Prompt for AI agents