summaryrefslogtreecommitdiff
path: root/Source/Android
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
parent112e1a436ae8c81ccd6d002460094172e72a600f (diff)
parent49ebb1cfea589e66134039d4ecca911e0e14af43 (diff)
Merge pull request #14794 from JosJuice/android-title-database
Android: Use TitleDatabase for game list
Diffstat (limited to 'Source/Android')
-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
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.cpp26
-rw-r--r--Source/Android/jni/AndroidCommon/IDCache.h4
-rw-r--r--Source/Android/jni/CMakeLists.txt2
-rw-r--r--Source/Android/jni/GameList/GameFile.cpp17
-rw-r--r--Source/Android/jni/GameList/TitleDatabase.cpp37
-rw-r--r--Source/Android/jni/GameList/TitleDatabase.h13
14 files changed, 193 insertions, 52 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(
diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp
index 58cec02308..8d774dbbd9 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.cpp
+++ b/Source/Android/jni/AndroidCommon/IDCache.cpp
@@ -151,6 +151,10 @@ static jclass s_audio_utils_class;
static jmethodID s_audio_utils_get_sample_rate;
static jmethodID s_audio_utils_get_frames_per_buffer;
+static jclass s_title_database_class;
+static jfieldID s_title_database_pointer;
+static jmethodID s_title_database_constructor;
+
namespace IDCache
{
JNIEnv* GetEnvForThread()
@@ -721,6 +725,21 @@ jmethodID GetAudioUtilsGetFramesPerBuffer()
return s_audio_utils_get_frames_per_buffer;
}
+jclass GetTitleDatabaseClass()
+{
+ return s_title_database_class;
+}
+
+jfieldID GetTitleDatabasePointer()
+{
+ return s_title_database_pointer;
+}
+
+jmethodID GetTitleDatabaseConstructor()
+{
+ return s_title_database_constructor;
+}
+
} // namespace IDCache
extern "C" {
@@ -1015,6 +1034,13 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
env->GetStaticMethodID(audio_utils_class, "getFramesPerBuffer", "()I");
env->DeleteLocalRef(audio_utils_class);
+ const jclass title_database_class =
+ env->FindClass("org/dolphinemu/dolphinemu/model/TitleDatabase");
+ s_title_database_class = reinterpret_cast<jclass>(env->NewGlobalRef(title_database_class));
+ s_title_database_pointer = env->GetFieldID(title_database_class, "pointer", "J");
+ s_title_database_constructor = env->GetMethodID(title_database_class, "<init>", "(J)V");
+ env->DeleteLocalRef(title_database_class);
+
return JNI_VERSION;
}
diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h
index 43c4f43585..1483b3bbd3 100644
--- a/Source/Android/jni/AndroidCommon/IDCache.h
+++ b/Source/Android/jni/AndroidCommon/IDCache.h
@@ -150,4 +150,8 @@ jclass GetAudioUtilsClass();
jmethodID GetAudioUtilsGetSampleRate();
jmethodID GetAudioUtilsGetFramesPerBuffer();
+jclass GetTitleDatabaseClass();
+jfieldID GetTitleDatabasePointer();
+jmethodID GetTitleDatabaseConstructor();
+
} // namespace IDCache
diff --git a/Source/Android/jni/CMakeLists.txt b/Source/Android/jni/CMakeLists.txt
index a7ee0b6b3a..59e1e1138e 100644
--- a/Source/Android/jni/CMakeLists.txt
+++ b/Source/Android/jni/CMakeLists.txt
@@ -13,6 +13,8 @@ add_library(main SHARED
GameList/GameFile.cpp
GameList/GameFile.h
GameList/GameFileCache.cpp
+ GameList/TitleDatabase.cpp
+ GameList/TitleDatabase.h
GpuDriver.cpp
HotkeyScheduler.cpp
HotkeyScheduler.h
diff --git a/Source/Android/jni/GameList/GameFile.cpp b/Source/Android/jni/GameList/GameFile.cpp
index dc922d701c..e13f5c810a 100644
--- a/Source/Android/jni/GameList/GameFile.cpp
+++ b/Source/Android/jni/GameList/GameFile.cpp
@@ -16,6 +16,7 @@
#include "UICommon/GameFile.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#include "jni/AndroidCommon/IDCache.h"
+#include "jni/GameList/TitleDatabase.h"
static std::shared_ptr<const UICommon::GameFile>* GetPointer(JNIEnv* env, jobject obj)
{
@@ -52,11 +53,19 @@ JNIEXPORT jint JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getPlatform
return static_cast<jint>(GetRef(env, obj)->GetPlatform());
}
-JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTitle(JNIEnv* env,
- jobject obj)
+JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getTitle(
+ JNIEnv* env, jobject obj, jobject j_title_database)
{
- return ToJString(env,
- GetRef(env, obj)->GetName(UICommon::GameFile::Variant::LongAndPossiblyCustom));
+ const Core::TitleDatabase* title_database = TitleDatabaseFromJava(env, j_title_database);
+ if (title_database)
+ {
+ return ToJString(env, GetRef(env, obj)->GetName(*title_database));
+ }
+ else
+ {
+ return ToJString(env,
+ GetRef(env, obj)->GetName(UICommon::GameFile::Variant::LongAndPossiblyCustom));
+ }
}
JNIEXPORT jstring JNICALL Java_org_dolphinemu_dolphinemu_model_GameFile_getDescription(JNIEnv* env,
diff --git a/Source/Android/jni/GameList/TitleDatabase.cpp b/Source/Android/jni/GameList/TitleDatabase.cpp
new file mode 100644
index 0000000000..c4aca48a25
--- /dev/null
+++ b/Source/Android/jni/GameList/TitleDatabase.cpp
@@ -0,0 +1,37 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "jni/GameList/TitleDatabase.h"
+
+#include <jni.h>
+
+#include "Core/TitleDatabase.h"
+#include "jni/AndroidCommon/IDCache.h"
+
+Core::TitleDatabase* TitleDatabaseFromJava(JNIEnv* env, jobject obj)
+{
+ return reinterpret_cast<Core::TitleDatabase*>(
+ env->GetLongField(obj, IDCache::GetTitleDatabasePointer()));
+}
+extern "C" {
+
+JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_model_TitleDatabase_finalize(JNIEnv* env,
+ jobject obj)
+{
+ delete TitleDatabaseFromJava(env, obj);
+}
+
+JNIEXPORT jboolean JNICALL Java_org_dolphinemu_dolphinemu_model_TitleDatabase_userTitleMapEquals(
+ JNIEnv* env, jobject obj, jobject other)
+{
+ return other != nullptr && TitleDatabaseFromJava(env, obj)->GetUserTitleMap() ==
+ TitleDatabaseFromJava(env, other)->GetUserTitleMap();
+}
+
+JNIEXPORT jobject JNICALL Java_org_dolphinemu_dolphinemu_model_TitleDatabase_load(JNIEnv* env,
+ jclass)
+{
+ return env->NewObject(IDCache::GetTitleDatabaseClass(), IDCache::GetTitleDatabaseConstructor(),
+ reinterpret_cast<jlong>(new Core::TitleDatabase));
+}
+}
diff --git a/Source/Android/jni/GameList/TitleDatabase.h b/Source/Android/jni/GameList/TitleDatabase.h
new file mode 100644
index 0000000000..fcf8f9bf32
--- /dev/null
+++ b/Source/Android/jni/GameList/TitleDatabase.h
@@ -0,0 +1,13 @@
+// Copyright 2026 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <jni.h>
+
+namespace Core
+{
+class TitleDatabase;
+}
+
+Core::TitleDatabase* TitleDatabaseFromJava(JNIEnv* env, jobject obj);