From 3d5ba09eb1e17bc2d72d4a71eb44a7a568ab31e5 Mon Sep 17 00:00:00 2001 From: PabloMK7 Date: Tue, 17 Mar 2026 13:15:33 +0100 Subject: [PATCH] android: Fix launching applications through intent data in vanilla build (#1896) * android: Fix launching applications through intent data in vanilla build * GameHelper.kt: Use Uri.scheme where applicable --------- Co-authored-by: OpenSauce04 --- .../citra_emu/fragments/EmulationFragment.kt | 46 ++++++++++++------ .../org/citra/citra_emu/utils/FileUtil.kt | 4 ++ .../org/citra/citra_emu/utils/GameHelper.kt | 6 ++- src/common/file_util.cpp | 48 +++++++++++++++++++ 4 files changed, 89 insertions(+), 15 deletions(-) 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 091ffd5ee..35899ce7f 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 @@ -18,6 +18,7 @@ import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper +import android.os.ParcelFileDescriptor import android.os.SystemClock import android.text.Editable import android.text.TextWatcher @@ -73,6 +74,7 @@ import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.model.Game +import org.citra.citra_emu.utils.BuildUtil import org.citra.citra_emu.utils.DirectoryInitialization import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState import org.citra.citra_emu.utils.EmulationMenuSettings @@ -108,6 +110,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram private val onPause = Runnable{ togglePause() } private val onShutdown = Runnable{ emulationState.stop() } + // Only used if a game is passed through intent on google play variant + private var gameFd: Int? = null + override fun onAttach(context: Context) { super.onAttach(context) if (context is EmulationActivity) { @@ -125,27 +130,34 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram super.onCreate(savedInstanceState) val intent = requireActivity().intent - val intentUri: Uri? = intent.data + var intentUri: Uri? = intent.data val oldIntentInfo = Pair( intent.getStringExtra("SelectedGame"), intent.getStringExtra("SelectedTitle") ) var intentGame: Game? = null + intentUri = if (intentUri == null && oldIntentInfo.first != null) { + Uri.parse(oldIntentInfo.first) + } else { + intentUri + } if (intentUri != null) { - intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) { - // 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))) { - // isInstalled, addedToLibrary and mediaType do not matter here - GameHelper.getGame(gameUri, isInstalled = false, addedToLibrary = false, mediaType = Game.MediaType.GAME_CARD) - } else { - null + if (!BuildUtil.isGooglePlayBuild) { + // We need to build a special path as the incoming URI may be SAF exclusive + Log.warning("[EmulationFragment] Cannot determine native path of URI \"" + + intentUri.toString() + "\", using file descriptor instead.") + gameFd = requireContext().contentResolver.openFileDescriptor(intentUri, "r")?.detachFd() + intentUri = if (gameFd != null) { + Uri.parse("fd://" + gameFd.toString()) + } else { + null + } } + intentGame = + intentUri?.let { + // isInstalled, addedToLibrary and mediaType do not matter here + GameHelper.getGame(it, isInstalled = false, addedToLibrary = false, mediaType = Game.MediaType.GAME_CARD) + } } val insertedCartridge = preferences.getString("insertedCartridge", "") @@ -163,6 +175,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram return } + Log.info("[EmulationFragment] Starting application " + game.path) + // So this fragment doesn't restart on configuration changes; i.e. rotation. retainInstance = true emulationState = EmulationState(game.path) @@ -528,6 +542,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram override fun onDestroy() { EmulationLifecycleUtil.removeHook(onPause) EmulationLifecycleUtil.removeHook(onShutdown) + if (gameFd != null) { + ParcelFileDescriptor.adoptFd(gameFd!!).close() + gameFd = null + } super.onDestroy() } diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt index 86b82c1dd..29020c99d 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileUtil.kt @@ -222,6 +222,10 @@ object FileUtil { var filename = "" var c: Cursor? = null try { + if (uri.scheme == "fd") { + return "" + } + if (uri.scheme == "file") { BuildUtil.assertNotGooglePlay() val file = File(uri.path!!); 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 e3d871aa2..f711b6224 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 @@ -75,7 +75,11 @@ object GameHelper { if (BuildUtil.isGooglePlayBuild || FileUtil.isNativePath(filePath)) { gameInfo = GameInfo(filePath) } else { - nativePath = "!" + NativeLibrary.getNativePath(uri); + nativePath = if (uri.scheme == "fd") { + uri.toString() + } else { + "!" + NativeLibrary.getNativePath(uri) + }; gameInfo = GameInfo(nativePath) } diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 95b5dda5b..52f406226 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -122,6 +122,16 @@ typedef struct stat file_stat_t; #define FERROR ferror #define FFLUSH std::fflush +#ifdef _MSC_VER +#define DUP_FD _dup +#define FDOPEN _fdopen +#define CLOSE_FD _close +#else +#define DUP_FD dup +#define FDOPEN fdopen +#define CLOSE_FD close +#endif + #endif // This namespace has various generic functions related to files and paths. @@ -1262,6 +1272,44 @@ void IOFile::Swap(IOFile& other) noexcept { bool IOFile::Open() { Close(); + // Any filename with the format fd:// represents a file that + // must be opened by duplicating the provided file_descriptor. This is used + // on Android vanilla builds when the ROM absolute path is not known. + if (filename.starts_with("fd://")) { + +#if !defined(HAVE_LIBRETRO_VFS) + const std::string fd_str = filename.substr(5); + + // Check that fd_str is not empty and contains only digits + if (fd_str.empty() || !std::all_of(fd_str.begin(), fd_str.end(), ::isdigit)) { + m_good = false; + return false; + } + + int fd = std::stoi(fd_str); + + int dup_fd = DUP_FD(fd); + if (dup_fd == -1) { + m_good = false; + return false; + } + + m_file = FDOPEN(dup_fd, openmode.c_str()); + if (!m_file) { + CLOSE_FD(dup_fd); + m_good = false; + return false; + } + + m_good = true; + return true; +#else + // TODO: Add support for libretro vfs when needed. + m_good = false; + return false; +#endif + } + #ifdef _WIN32 // Open with FILE_SHARE_READ, FILE_SHARE_WRITE and FILE_SHARE_DELETE // flags. This mimics linux behaviour as much as possible, which