From 32da5ea0aed6e03dd60b45179737c5ecd25d3afa Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Sun, 8 Mar 2026 13:51:29 +0100 Subject: [PATCH] Read media type and pass it to UninstallProgram --- .../java/org/citra/citra_emu/NativeLibrary.kt | 28 ++++++++++- .../citra/citra_emu/adapters/GameAdapter.kt | 6 +-- .../citra_emu/fragments/EmulationFragment.kt | 6 ++- .../java/org/citra/citra_emu/model/Game.kt | 14 ++++++ .../org/citra/citra_emu/utils/GameHelper.kt | 7 +-- src/android/app/src/main/jni/native.cpp | 50 ++++++++++--------- 6 files changed, 78 insertions(+), 33 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index d20f13d19..18cee9e7b 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -25,6 +25,7 @@ import androidx.fragment.app.DialogFragment import androidx.preference.PreferenceManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.citra.citra_emu.activities.EmulationActivity +import org.citra.citra_emu.model.Game import org.citra.citra_emu.utils.BuildUtil import org.citra.citra_emu.utils.FileUtil import org.citra.citra_emu.utils.Log @@ -132,7 +133,27 @@ object NativeLibrary { * If not set, it auto-detects a location */ external fun setUserDirectory(directory: String) - external fun getInstalledGamePaths(): Array + + data class InstalledGame( + val path: String, + val mediaType: Game.MediaType + ) + fun getInstalledGamePaths(): Array { + val games = getInstalledGamePathsImpl() + + return games.mapNotNull { entry -> + entry?.let { + val sep = it.lastIndexOf('|') + if (sep == -1) return@mapNotNull null + + val path = it.substring(0, sep) + val mediaType = Game.MediaType.fromInt(it.substring(sep + 1).toInt()) + + InstalledGame(path, mediaType!!) + } + }.toTypedArray() + } + private external fun getInstalledGamePathsImpl(): Array // Create the config.ini file. external fun createConfigFile() @@ -230,7 +251,10 @@ object NativeLibrary { external fun playTimeManagerGetPlayTime(titleId: Long): Long external fun playTimeManagerGetCurrentTitleId(): Long - external fun uninstallTitle(titleId: Long): Boolean + private external fun uninstallTitle(titleId: Long, mediaType: Int): Boolean + fun uninstallTitle(titleId: Long, mediaType: Game.MediaType): Boolean { + return uninstallTitle(titleId, mediaType.value) + } private var coreErrorAlertResult = false private val coreErrorAlertLock = Object() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt index c43dba843..a67a89d38 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt @@ -330,9 +330,9 @@ class GameAdapter( popup.setOnMenuItemClickListener { menuItem -> val uninstallAction: () -> Unit = { when (menuItem.itemId) { - R.id.game_context_uninstall -> NativeLibrary.uninstallTitle(titleId) - R.id.game_context_uninstall_dlc -> NativeLibrary.uninstallTitle(dlcTitleId) - R.id.game_context_uninstall_updates -> NativeLibrary.uninstallTitle(updateTitleId) + R.id.game_context_uninstall -> NativeLibrary.uninstallTitle(titleId, game.mediaType) + R.id.game_context_uninstall_dlc -> NativeLibrary.uninstallTitle(dlcTitleId, Game.MediaType.SDMC) + R.id.game_context_uninstall_updates -> NativeLibrary.uninstallTitle(updateTitleId, Game.MediaType.SDMC) } ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true) bottomSheetDialog.dismiss() diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt index fba1a167d..d4ce96a55 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt @@ -133,14 +133,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram var intentGame: Game? = null if (intentUri != null) { intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) { - GameHelper.getGame(intentUri, isInstalled = false, addedToLibrary = false) + // isInstalled, addedToLibrary and mediaType do not matter here + GameHelper.getGame(intentUri, isInstalled = false, addedToLibrary = false, mediaType = Game.MediaType.GAME_CARD) } else { null } } else if (oldIntentInfo.first != null) { val gameUri = Uri.parse(oldIntentInfo.first) intentGame = if (Game.extensions.contains(FileUtil.getExtension(gameUri))) { - GameHelper.getGame(gameUri, isInstalled = false, addedToLibrary = false) + // isInstalled, addedToLibrary and mediaType do not matter here + GameHelper.getGame(gameUri, isInstalled = false, addedToLibrary = false, mediaType = Game.MediaType.GAME_CARD) } else { null } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt index 7a204e3fd..21b2e72f4 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/model/Game.kt @@ -20,6 +20,7 @@ class Game( val description: String = "", val path: String = "", val titleId: Long = 0L, + val mediaType: MediaType = MediaType.GAME_CARD, val company: String = "", val regions: String = "", val isInstalled: Boolean = false, @@ -58,10 +59,23 @@ class Game( result = 31 * result + regions.hashCode() result = 31 * result + path.hashCode() result = 31 * result + titleId.hashCode() + result = 31 * result + mediaType.hashCode() result = 31 * result + company.hashCode() return result } + enum class MediaType(val value: Int) { + NAND(0), + SDMC(1), + GAME_CARD(2); + + companion object { + fun fromInt(value: Int): MediaType? { + return MediaType.entries.find { it.value == value } + } + } + } + companion object { val allExtensions: Set get() = extensions + badExtensions diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt index 032947c1d..6abf5bfdb 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/GameHelper.kt @@ -32,7 +32,7 @@ object GameHelper { addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3) NativeLibrary.getInstalledGamePaths().forEach { - games.add(getGame(Uri.parse(it), isInstalled = true, addedToLibrary = true)) + games.add(getGame(Uri.parse(it.path), isInstalled = true, addedToLibrary = true, it.mediaType)) } // Cache list of games found on disk @@ -62,13 +62,13 @@ object GameHelper { addGamesRecursive(games, FileUtil.listFiles(it.uri), depth - 1) } else { if (Game.allExtensions.contains(FileUtil.getExtension(it.uri))) { - games.add(getGame(it.uri, isInstalled = false, addedToLibrary = true)) + games.add(getGame(it.uri, isInstalled = false, addedToLibrary = true, Game.MediaType.GAME_CARD)) } } } } - fun getGame(uri: Uri, isInstalled: Boolean, addedToLibrary: Boolean): Game { + fun getGame(uri: Uri, isInstalled: Boolean, addedToLibrary: Boolean, mediaType: Game.MediaType): Game { val filePath = uri.toString() var nativePath: String? = null var gameInfo: GameInfo? @@ -95,6 +95,7 @@ object GameHelper { nativePath!! }, gameInfo?.getTitleID() ?: 0, + mediaType, gameInfo?.getCompany() ?: "", if (isEncrypted) { CitraApplication.appContext.getString(R.string.unsupported_encrypted) } else { gameInfo?.getRegions() ?: "" }, isInstalled, diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index 46009d2d2..f84413c25 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -652,34 +652,38 @@ void Java_org_citra_citra_1emu_NativeLibrary_setUserDirectory(JNIEnv* env, FileUtil::SetCurrentDir(GetJString(env, j_directory)); } -jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePaths( +jobjectArray Java_org_citra_citra_1emu_NativeLibrary_getInstalledGamePathsImpl( JNIEnv* env, [[maybe_unused]] jclass clazz) { std::vector games; - const FileUtil::DirectoryEntryCallable ScanDir = - [&games, &ScanDir](u64*, const std::string& directory, const std::string& virtual_name) { - std::string path = directory + virtual_name; - if (FileUtil::IsDirectory(path)) { - path += '/'; - FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir); - } else { - if (!FileUtil::Exists(path)) - return false; - auto loader = Loader::GetLoader(path); - if (loader) { - bool executable{}; - const Loader::ResultStatus result = loader->IsExecutable(executable); - if (Loader::ResultStatus::Success == result && executable) { - games.emplace_back(path); - } + Service::FS::MediaType media_type; + const FileUtil::DirectoryEntryCallable ScanDir = [&games, &ScanDir, &media_type]( + u64*, const std::string& directory, + const std::string& virtual_name) { + std::string path = directory + virtual_name; + if (FileUtil::IsDirectory(path)) { + path += '/'; + FileUtil::ForeachDirectoryEntry(nullptr, path, ScanDir); + } else { + if (!FileUtil::Exists(path)) + return false; + auto loader = Loader::GetLoader(path); + if (loader) { + bool executable{}; + const Loader::ResultStatus result = loader->IsExecutable(executable); + if (Loader::ResultStatus::Success == result && executable) { + games.emplace_back(path + "|" + std::to_string(static_cast(media_type))); } } - return true; - }; + } + return true; + }; + media_type = Service::FS::MediaType::SDMC; ScanDir(nullptr, "", FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "Nintendo " "3DS/00000000000000000000000000000000/" "00000000000000000000000000000000/title/00040000"); + media_type = Service::FS::MediaType::NAND; ScanDir(nullptr, "", FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "00000000000000000000000000000000/title/00040010"); @@ -1108,10 +1112,10 @@ void Java_org_citra_citra_1emu_NativeLibrary_setInsertedCartridge(JNIEnv* env, j } jboolean Java_org_citra_citra_1emu_NativeLibrary_uninstallTitle(JNIEnv* env, jobject obj, - jlong j_titleid) { - const auto titleid = static_cast(env, j_titleid); - // TODO: Don't hard-code to SDMC? (CBA to pass a value from Kotlin at the moment) -OS - const auto result = Service::AM::UninstallProgram(Service::FS::MediaType::SDMC, titleid); + jlong j_titleid, jint j_mediatype) { + const auto titleid = static_cast(j_titleid); + const auto result = + Service::AM::UninstallProgram(static_cast(j_mediatype), titleid); if (result.IsError()) { LOG_ERROR(Frontend, "Failed to uninstall '{}': 0x{:08X}", std::to_string(titleid), result.raw);