summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java/org/dolphinemu
diff options
context:
space:
mode:
authorJosJuice <josjuice@gmail.com>2026-11-04 19:46:41 +0100
committerGitHub <noreply@github.com>2026-11-04 19:46:41 +0100
commit584366cb6859f5e371b1314bf0d92bb59d4d1b80 (patch)
treeeacc9d6f545183321717955ed5c4f0589d125564 /Source/Android/app/src/main/java/org/dolphinemu
parent112e1a436ae8c81ccd6d002460094172e72a600f (diff)
parent49ebb1cfea589e66134039d4ecca911e0e14af43 (diff)
Merge pull request #14794 from JosJuice/android-title-databaseHEADmaster
Android: Use TitleDatabase for game list
Diffstat (limited to 'Source/Android/app/src/main/java/org/dolphinemu')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt5
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt6
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.kt2
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TitleDatabase.kt17
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt105
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt5
8 files changed, 98 insertions, 48 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt
index dbcfdd42e2..e7ea8e5519 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt
@@ -61,13 +61,14 @@ class GameAdapter : RecyclerView.Adapter<GameViewHolder>(),
val gameFile = mGameFiles[position]
holder.apply {
+ val title = gameFile.getTitle(GameFileCacheManager.getTitleDatabase())
if (BooleanSetting.MAIN_SHOW_GAME_TITLES.boolean) {
- binding.textGameTitle.text = gameFile.getTitle()
+ binding.textGameTitle.text = title
binding.textGameTitle.visibility = View.VISIBLE
binding.textGameTitleInner.visibility = View.GONE
binding.textGameCaption.visibility = View.VISIBLE
} else {
- binding.textGameTitleInner.text = gameFile.getTitle()
+ binding.textGameTitleInner.text = title
binding.textGameTitleInner.visibility = View.VISIBLE
binding.textGameTitle.visibility = View.GONE
binding.textGameCaption.visibility = View.GONE
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt
index 4f43601e68..7cbb3000e2 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameRowPresenter.kt
@@ -48,7 +48,7 @@ class GameRowPresenter : Presenter() {
holder.apply {
imageScreenshot.setImageDrawable(null)
- cardParent.titleText = gameFile.getTitle()
+ cardParent.titleText = gameFile.getTitle(GameFileCacheManager.getTitleDatabase())
holder.gameFile = gameFile
// Set the background color of the card
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt
index 0384da89c0..b97fd987e6 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/GameDetailsDialog.kt
@@ -51,7 +51,7 @@ class GameDetailsDialog : DialogFragment() {
textTimePlayed.visibility = View.GONE
}
- textGameTitle.text = gameFile.getTitle()
+ textGameTitle.text = gameFile.getTitle(GameFileCacheManager.getTitleDatabase())
textDescription.text = gameFile.getDescription()
if (gameFile.getDescription().isEmpty()) {
textDescription.visibility = View.GONE
@@ -118,7 +118,7 @@ class GameDetailsDialog : DialogFragment() {
textTimePlayed.visibility = View.GONE
}
- textGameTitle.text = gameFile.getTitle()
+ textGameTitle.text = gameFile.getTitle(GameFileCacheManager.getTitleDatabase())
textDescription.text = gameFile.getDescription()
if (gameFile.getDescription().isEmpty()) {
tvBinding.textDescription.visibility = View.GONE
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
index c3602facf4..97f456c7a6 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/netplay/ui/NetplayScreen.kt
@@ -112,6 +112,7 @@ import org.dolphinemu.dolphinemu.features.netplay.model.Player
import org.dolphinemu.dolphinemu.features.netplay.model.SaveTransferProgress
import org.dolphinemu.dolphinemu.features.netplay.model.TraversalState
import org.dolphinemu.dolphinemu.model.GameFile
+import org.dolphinemu.dolphinemu.services.GameFileCacheManager
import org.dolphinemu.dolphinemu.ui.theme.DolphinScaffold
import org.dolphinemu.dolphinemu.ui.theme.DolphinTheme
import org.dolphinemu.dolphinemu.ui.theme.MenuSpacer
@@ -855,6 +856,7 @@ private fun GameGridItem(
gameFile: GameFile,
onClick: () -> Unit,
) {
+ val titleDatabase = GameFileCacheManager.getTitleDatabase()
Card(
onClick = onClick,
) {
@@ -864,7 +866,7 @@ private fun GameGridItem(
.data(gameFile)
.error(R.drawable.no_banner)
.build(),
- contentDescription = gameFile.getTitle(),
+ contentDescription = gameFile.getTitle(titleDatabase),
contentScale = ContentScale.Crop,
imageLoader = CoilUtils.imageLoader,
modifier = Modifier
@@ -872,7 +874,7 @@ private fun GameGridItem(
.aspectRatio(0.7f)
)
Text(
- text = gameFile.getTitle(),
+ text = gameFile.getTitle(titleDatabase),
style = MaterialTheme.typography.bodySmall,
maxLines = 2,
minLines = 2,
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.kt
index 26da174285..4794f5c1b7 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFile.kt
@@ -12,7 +12,7 @@ class GameFile private constructor(private val pointer: Long) {
external fun getPlatform(): Int
- external fun getTitle(): String
+ external fun getTitle(titleDatabase: TitleDatabase?): String
external fun getDescription(): String
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TitleDatabase.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TitleDatabase.kt
new file mode 100644
index 0000000000..e5089673b8
--- /dev/null
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/TitleDatabase.kt
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.dolphinemu.dolphinemu.model
+
+import androidx.annotation.Keep
+
+@Keep
+class TitleDatabase private constructor(private val pointer: Long) {
+ external fun finalize()
+
+ external fun userTitleMapEquals(other: TitleDatabase?): Boolean
+
+ companion object {
+ @JvmStatic
+ external suspend fun load(): TitleDatabase
+ }
+}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt
index 2ba2c246b3..a5e54aa0de 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheManager.kt
@@ -2,32 +2,36 @@
package org.dolphinemu.dolphinemu.services
-import android.os.Handler
-import android.os.Looper
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asFlow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
import org.dolphinemu.dolphinemu.features.settings.model.ConfigChangedCallback
import org.dolphinemu.dolphinemu.model.GameFile
import org.dolphinemu.dolphinemu.model.GameFileCache
+import org.dolphinemu.dolphinemu.model.TitleDatabase
import org.dolphinemu.dolphinemu.ui.platform.Platform
import org.dolphinemu.dolphinemu.ui.platform.PlatformTab
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
import java.util.Arrays
-import java.util.concurrent.ExecutorService
-import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicReference
/**
- * Loads game list data on a separate thread.
+ * Loads game list data in the background.
*/
object GameFileCacheManager {
private var gameFileCache: GameFileCache? = null
private val gameFiles = MutableLiveData(emptyArray<GameFile>())
+ private var titleDatabase = AtomicReference<TitleDatabase>(null)
private var firstLoadDone = false
- private var runRescanAfterLoad = false
private var recursiveScanEnabled = false
- private val executor: ExecutorService = Executors.newFixedThreadPool(1)
private val loadInProgress = MutableLiveData(false)
private val rescanInProgress = MutableLiveData(false)
@@ -88,6 +92,8 @@ object GameFileCacheManager {
}
}
+ fun getTitleDatabase(): TitleDatabase? = titleDatabase.get()
+
/**
* Returns true if in the process of loading the cache for the first time.
*/
@@ -120,7 +126,9 @@ object GameFileCacheManager {
if (!loadInProgress.value!!) {
loadInProgress.value = true
- AfterDirectoryInitializationRunner().runWithoutLifecycle { executor.execute(::load) }
+ AfterDirectoryInitializationRunner().runWithoutLifecycle {
+ MainScope().launch { load() }
+ }
}
}
@@ -136,7 +144,9 @@ object GameFileCacheManager {
if (!rescanInProgress.value!!) {
rescanInProgress.value = true
- AfterDirectoryInitializationRunner().runWithoutLifecycle { executor.execute(::rescan) }
+ AfterDirectoryInitializationRunner().runWithoutLifecycle {
+ MainScope().launch { rescan() }
+ }
}
}
@@ -144,9 +154,9 @@ object GameFileCacheManager {
fun addOrGet(gamePath: String?): GameFile {
val nonNullGamePath = gamePath!!
- // Common case: The game is in the cache, so just grab it from there. (GameFileCache.addOrGet
+ // Common case: The game is in the cache, so we grab it from there. (GameFileCache.addOrGet
// actually already checks for this case, but we want to avoid calling it if possible
- // because the executor thread may hold a lock on gameFileCache for extended periods of time.)
+ // because rescan() may hold a lock on gameFileCache for extended periods of time.)
val allGames = gameFiles.value!!
for (game in allGames) {
if (game.getPath() == nonNullGamePath) {
@@ -165,28 +175,28 @@ object GameFileCacheManager {
* games are still present in the user's configured folders.
* If this has already been called, calling it again has no effect.
*/
- private fun load() {
+ private suspend fun load() {
if (!firstLoadDone) {
- firstLoadDone = true
- setUpAutomaticRescan()
- gameFileCache!!.load()
- if (gameFileCache!!.getSize() != 0) {
- updateGameFileArray()
+ val titleDatabaseJob = MainScope().async(Dispatchers.IO) {
+ TitleDatabase.load()
}
- }
- if (runRescanAfterLoad) {
- // Without this, there will be a short blip where the loading indicator in the GUI disappears
- // because neither loadInProgress nor rescanInProgress is true
- rescanInProgress.postValue(true)
- }
+ withContext(Dispatchers.IO) {
+ setUpAutomaticRescan()
+
+ gameFileCache!!.load()
+
+ titleDatabase.set(titleDatabaseJob.await())
- loadInProgress.postValue(false)
+ if (gameFileCache!!.getSize() != 0) {
+ updateGameFileArray()
+ }
+ }
- if (runRescanAfterLoad) {
- runRescanAfterLoad = false
- rescan()
+ firstLoadDone = true
}
+
+ loadInProgress.value = false
}
/**
@@ -195,19 +205,35 @@ object GameFileCacheManager {
* If load hasn't been called before this, the execution of this
* will be postponed until after load runs.
*/
- private fun rescan() {
- if (!firstLoadDone) {
- runRescanAfterLoad = true
- } else {
- val gamePaths = GameFileCache.getAllGamePaths()
+ private suspend fun rescan() {
+ while (!firstLoadDone) {
+ // Wait until loadInProgress becomes false
+ loadInProgress.asFlow().first { !it }
+ }
+ val titleDatabaseJob = MainScope().async(Dispatchers.IO) {
+ val newTitleDatabase = TitleDatabase.load()
+ val titleDatabaseChanged =
+ !newTitleDatabase.userTitleMapEquals(titleDatabase.get())
+ if (titleDatabaseChanged) newTitleDatabase else null
+ }
+
+ withContext(Dispatchers.IO) {
+ val gamePaths = GameFileCache.getAllGamePaths()
val changed = gameFileCache!!.update(gamePaths)
if (changed) {
updateGameFileArray()
}
val additionalMetadataChanged = gameFileCache!!.updateAdditionalMetadata()
- if (additionalMetadataChanged) {
+
+ val newTitleDatabase = titleDatabaseJob.await()
+ val titleDatabaseChanged = newTitleDatabase != null
+ if (titleDatabaseChanged) {
+ titleDatabase.set(newTitleDatabase)
+ }
+
+ if (additionalMetadataChanged || titleDatabaseChanged) {
updateGameFileArray()
}
@@ -216,13 +242,14 @@ object GameFileCacheManager {
}
}
- rescanInProgress.postValue(false)
+ rescanInProgress.value = false
}
private fun updateGameFileArray() {
val gameFilesTemp = gameFileCache!!.getAllGames()
+ val titleDatabase = titleDatabase.get()
Arrays.sort(gameFilesTemp) { lhs, rhs ->
- lhs.getTitle().compareTo(rhs.getTitle(), ignoreCase = true)
+ lhs.getTitle(titleDatabase).compareTo(rhs.getTitle(titleDatabase), ignoreCase = true)
}
gameFiles.postValue(gameFilesTemp)
}
@@ -239,10 +266,10 @@ object GameFileCacheManager {
private fun setUpAutomaticRescan() {
recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean
ConfigChangedCallback {
- Handler(Looper.getMainLooper()).post {
- val recursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean
- if (this.recursiveScanEnabled != recursiveScanEnabled) {
- this.recursiveScanEnabled = recursiveScanEnabled
+ MainScope().launch {
+ val newRecursiveScanEnabled = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.boolean
+ if (recursiveScanEnabled != newRecursiveScanEnabled) {
+ recursiveScanEnabled = newRecursiveScanEnabled
startRescan()
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt
index 2d9ad354fb..fd13ff3175 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/SyncProgramsJobService.kt
@@ -138,7 +138,10 @@ class SyncProgramsJobService : JobService() {
TvContractCompat.PreviewPrograms.COLUMN_TYPE,
TvContractCompat.PreviewProgramColumns.TYPE_GAME
)
- values.put(TvContract.Programs.COLUMN_TITLE, game.getTitle())
+ values.put(
+ TvContract.Programs.COLUMN_TITLE,
+ game.getTitle(GameFileCacheManager.getTitleDatabase())
+ )
values.put(TvContract.Programs.COLUMN_SHORT_DESCRIPTION, game.getDescription())
values.put(TvContract.Programs.COLUMN_POSTER_ART_URI, banner.toString())
values.put(