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 <opensauce04@gmail.com>
This commit is contained in:
PabloMK7 2026-03-17 13:15:33 +01:00 committed by GitHub
parent ae9972b6be
commit 3d5ba09eb1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 15 deletions

View File

@ -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()
}

View File

@ -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!!);

View File

@ -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)
}

View File

@ -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://<file_descriptor> 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