Browse Source

[android, intent] Added proper ext content mount and game swap support for intent launch (#3755)

Required so that frontends can launch a game while there is already one running (for CocoonFE usage)
Fix for mounting external content was merged.
This patch also fixes multiple reasons for infinite game "Shutting down..." issue (hope all, who knows...)

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3755
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: xbzk <xbzk@eden-emu.dev>
Co-committed-by: xbzk <xbzk@eden-emu.dev>
pull/3806/head
xbzk 2 days ago
committed by crueter
parent
commit
b4a485e244
No known key found for this signature in database GPG Key ID: 425ACD2D4830EBC6
  1. 204
      src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
  2. 129
      src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt

204
src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt

@ -25,6 +25,11 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.navigation.NavOptions
import org.yuzu.yuzu_emu.fragments.EmulationFragment
import org.yuzu.yuzu_emu.utils.CustomSettingsHandler
import android.util.Rational
import android.view.InputDevice
import android.view.KeyEvent
@ -87,6 +92,28 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
private val emulationViewModel: EmulationViewModel by viewModels()
private var foregroundService: Intent? = null
private val mainHandler = Handler(Looper.getMainLooper())
private var pendingRomSwapIntent: Intent? = null
private var isWaitingForRomSwapStop = false
private var romSwapNativeStopped = false
private var romSwapThreadStopped = false
private var romSwapGeneration = 0
private var hasEmulationSession = processHasEmulationSession
private val romSwapStopTimeoutRunnable = Runnable { onRomSwapStopTimeout() }
private fun onRomSwapStopTimeout() {
if (!isWaitingForRomSwapStop) {
return
}
Log.warning("[EmulationActivity] ROM swap stop timed out; retrying native stop and continuing to wait")
NativeLibrary.stopEmulation()
scheduleRomSwapStopTimeout()
}
private fun scheduleRomSwapStopTimeout() {
mainHandler.removeCallbacks(romSwapStopTimeoutRunnable)
mainHandler.postDelayed(romSwapStopTimeoutRunnable, ROM_SWAP_STOP_TIMEOUT_MS)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(YuzuApplication.applyLanguage(base))
@ -128,9 +155,29 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
binding = ActivityEmulationBinding.inflate(layoutInflater)
setContentView(binding.root)
val launchIntent = Intent(intent)
val shouldDeferLaunchForSwap = hasEmulationSession && isSwapIntent(launchIntent)
if (shouldDeferLaunchForSwap) {
Log.info("[EmulationActivity] onCreate detected existing session; deferring new game setup for swap")
emulationViewModel.setIsEmulationStopping(true)
emulationViewModel.setEmulationStopped(false)
}
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
val initialArgs = if (shouldDeferLaunchForSwap) {
Bundle(intent.extras ?: Bundle()).apply {
processSessionGame?.let { putParcelable("game", it) }
}
} else {
intent.extras
}
navHostFragment.navController.setGraph(R.navigation.emulation_navigation, initialArgs)
if (shouldDeferLaunchForSwap) {
mainHandler.post {
handleSwapIntent(launchIntent)
}
}
isActivityRecreated = savedInstanceState != null
@ -210,6 +257,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
}
override fun onDestroy() {
mainHandler.removeCallbacks(romSwapStopTimeoutRunnable)
super.onDestroy()
inputManager.unregisterInputDeviceListener(this)
stopForegroundService(this)
@ -228,15 +276,121 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleSwapIntent(intent)
nfcReader.onNewIntent(intent)
InputHandler.updateControllerData()
}
private fun isSwapIntent(intent: Intent): Boolean {
return when {
intent.getBooleanExtra(EXTRA_OVERLAY_GAMELESS_EDIT_MODE, false) -> false
intent.action == CustomSettingsHandler.CUSTOM_CONFIG_ACTION -> true
intent.data != null -> true
else -> {
val extras = intent.extras
extras != null &&
BundleCompat.getParcelable(extras, EXTRA_SELECTED_GAME, Game::class.java) != null
}
}
}
// Reset navigation graph with new intent data to recreate EmulationFragment
private fun handleSwapIntent(intent: Intent) {
if (!isSwapIntent(intent)) {
return
}
pendingRomSwapIntent = Intent(intent)
if (!isWaitingForRomSwapStop) {
Log.info("[EmulationActivity] Begin ROM swap: data=${intent.data}")
isWaitingForRomSwapStop = true
romSwapNativeStopped = false
romSwapThreadStopped = false
romSwapGeneration += 1
val thisSwapGeneration = romSwapGeneration
emulationViewModel.setIsEmulationStopping(true)
emulationViewModel.setEmulationStopped(false)
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment
val childFragmentManager = navHostFragment?.childFragmentManager
val stoppingFragmentForSwap =
(childFragmentManager?.primaryNavigationFragment as? EmulationFragment) ?:
childFragmentManager
?.fragments
?.asReversed()
?.firstOrNull {
it is EmulationFragment &&
it.isAdded &&
it.view != null &&
!it.isRemoving
} as? EmulationFragment
val hasSessionForSwap = hasEmulationSession || stoppingFragmentForSwap != null
if (!hasSessionForSwap) {
romSwapNativeStopped = true
romSwapThreadStopped = true
} else {
if (stoppingFragmentForSwap != null) {
stoppingFragmentForSwap.stopForRomSwap()
stoppingFragmentForSwap.notifyWhenEmulationThreadStops {
if (!isWaitingForRomSwapStop || romSwapGeneration != thisSwapGeneration) {
return@notifyWhenEmulationThreadStops
}
romSwapThreadStopped = true
Log.info("[EmulationActivity] ROM swap thread stop acknowledged")
launchPendingRomSwap(force = false)
}
} else {
Log.warning("[EmulationActivity] ROM swap stop target fragment not found; requesting native stop")
romSwapThreadStopped = true
NativeLibrary.stopEmulation()
}
scheduleRomSwapStopTimeout()
}
}
launchPendingRomSwap(force = false)
}
private fun launchPendingRomSwap(force: Boolean) {
if (!isWaitingForRomSwapStop) {
return
}
if (!force && (!romSwapNativeStopped || !romSwapThreadStopped)) {
return
}
val swapIntent = pendingRomSwapIntent ?: return
Log.info("[EmulationActivity] Launching pending ROM swap: data=${swapIntent.data}")
pendingRomSwapIntent = null
isWaitingForRomSwapStop = false
romSwapNativeStopped = false
romSwapThreadStopped = false
mainHandler.removeCallbacks(romSwapStopTimeoutRunnable)
applyGameLaunchIntent(swapIntent)
}
private fun applyGameLaunchIntent(intent: Intent) {
hasEmulationSession = true
processHasEmulationSession = true
emulationViewModel.setIsEmulationStopping(false)
emulationViewModel.setEmulationStopped(false)
setIntent(Intent(intent))
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
navHostFragment.navController.setGraph(R.navigation.emulation_navigation, intent.extras)
nfcReader.onNewIntent(intent)
InputHandler.updateControllerData()
val navController = navHostFragment.navController
val startArgs = intent.extras?.let { Bundle(it) } ?: Bundle()
val navOptions = NavOptions.Builder()
.setPopUpTo(R.id.emulationFragment, true)
.build()
runCatching {
navController.navigate(R.id.emulationFragment, startArgs, navOptions)
}.onFailure {
Log.warning("[EmulationActivity] ROM swap navigate fallback to setGraph: ${it.message}")
navController.setGraph(R.navigation.emulation_navigation, startArgs)
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
@ -608,19 +762,48 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
}
fun onEmulationStarted() {
if (Looper.myLooper() != Looper.getMainLooper()) {
mainHandler.post { onEmulationStarted() }
return
}
hasEmulationSession = true
processHasEmulationSession = true
emulationViewModel.setEmulationStarted(true)
emulationViewModel.setIsEmulationStopping(false)
emulationViewModel.setEmulationStopped(false)
NativeLibrary.playTimeManagerStart()
}
fun onEmulationStopped(status: Int) {
if (status == 0 && emulationViewModel.programChanged.value == -1) {
if (Looper.myLooper() != Looper.getMainLooper()) {
mainHandler.post { onEmulationStopped(status) }
return
}
hasEmulationSession = false
processHasEmulationSession = false
if (isWaitingForRomSwapStop) {
romSwapNativeStopped = true
Log.info("[EmulationActivity] ROM swap native stop acknowledged")
launchPendingRomSwap(force = false)
} else if (status == 0 && emulationViewModel.programChanged.value == -1) {
processSessionGame = null
finish()
} else if (!isWaitingForRomSwapStop) {
processSessionGame = null
}
emulationViewModel.setEmulationStopped(true)
}
fun updateSessionGame(game: Game?) {
processSessionGame = game
}
fun onProgramChanged(programIndex: Int) {
if (Looper.myLooper() != Looper.getMainLooper()) {
mainHandler.post { onProgramChanged(programIndex) }
return
}
emulationViewModel.setProgramChanged(programIndex)
}
@ -644,6 +827,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener, InputManager
companion object {
const val EXTRA_SELECTED_GAME = "SelectedGame"
const val EXTRA_OVERLAY_GAMELESS_EDIT_MODE = "overlayGamelessEditMode"
private const val ROM_SWAP_STOP_TIMEOUT_MS = 5000L
@Volatile
private var processHasEmulationSession = false
@Volatile
private var processSessionGame: Game? = null
fun stopForegroundService(activity: Activity) {
val startIntent = Intent(activity, ForegroundService::class.java)

129
src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt

@ -50,6 +50,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.navArgs
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
@ -135,6 +136,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private var intentGame: Game? = null
private var isCustomSettingsIntent = false
private var isStoppingForRomSwap = false
private var deferGameSetupUntilStopCompletes = false
private var perfStatsRunnable: Runnable? = null
private var socRunnable: Runnable? = null
@ -238,6 +241,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
if (emulationViewModel.isEmulationStopping.value) {
deferGameSetupUntilStopCompletes = true
if (game == null) {
game = args.game ?: intentGame
}
return
}
finishGameSetup()
}
@ -260,6 +271,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
game = gameToUse
emulationActivity?.updateSessionGame(gameToUse)
} catch (e: Exception) {
Log.error("[EmulationFragment] Error during game setup: ${e.message}")
Toast.makeText(
@ -334,7 +346,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
emulationState = EmulationState(game!!.path) {
return@EmulationState driverViewModel.isInteractionAllowed.value
return@EmulationState driverViewModel.isInteractionAllowed.value &&
!isStoppingForRomSwap
}
}
@ -890,8 +903,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
)
GameIconUtils.loadGameIcon(game!!, binding.loadingImage)
binding.loadingTitle.text = game!!.title
game?.let {
GameIconUtils.loadGameIcon(it, binding.loadingImage)
binding.loadingTitle.text = it.title
} ?: run {
binding.loadingTitle.text = ""
}
binding.loadingTitle.isSelected = true
binding.loadingText.isSelected = true
@ -959,6 +976,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
ViewUtils.showView(binding.loadingIndicator)
ViewUtils.hideView(binding.inputContainer)
ViewUtils.hideView(binding.showStatsOverlayText)
} else if (deferGameSetupUntilStopCompletes) {
if (!isAdded) {
return@collect
}
deferGameSetupUntilStopCompletes = false
finishGameSetup()
}
}
emulationViewModel.drawerOpen.collect(viewLifecycleOwner) {
@ -995,24 +1018,22 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
driverViewModel.isInteractionAllowed.collect(viewLifecycleOwner) {
if (it && !NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
startEmulation()
}
}
if (it &&
!isStoppingForRomSwap &&
!NativeLibrary.isRunning() &&
!NativeLibrary.isPaused()
) {
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
driverViewModel.onLaunchGame()
}
updateScreenLayout()
private fun startEmulation(programIndex: Int = 0) {
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
emulationState.run(emulationActivity!!.isActivityRecreated)
}
updateScreenLayout()
emulationState.run(emulationActivity!!.isActivityRecreated, programIndex)
}
driverViewModel.onLaunchGame()
}
override fun onConfigurationChanged(newConfig: Configuration) {
@ -1375,6 +1396,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
super.onDestroyView()
amiiboLoadJob?.cancel()
amiiboLoadJob = null
perfStatsRunnable?.let { perfStatsUpdateHandler.removeCallbacks(it) }
socRunnable?.let { socUpdateHandler.removeCallbacks(it) }
handler.removeCallbacksAndMessages(null)
clearPausedFrame()
_binding?.surfaceInputOverlay?.touchEventListener = null
_binding = null
@ -1382,7 +1406,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
override fun onDetach() {
NativeLibrary.clearEmulationActivity()
if (!hasNewerEmulationFragment()) {
NativeLibrary.clearEmulationActivity()
}
super.onDetach()
}
@ -1840,10 +1866,74 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
emulationState.clearSurface()
if (this::emulationState.isInitialized && !hasNewerEmulationFragment()) {
emulationState.clearSurface()
}
emulationStarted = false
}
private fun hasNewerEmulationFragment(): Boolean {
val activity = emulationActivity ?: return false
return try {
val navHostFragment =
activity.supportFragmentManager.findFragmentById(R.id.fragment_container) as? NavHostFragment
?: return false
val currentFragment = navHostFragment.childFragmentManager.fragments
.filterIsInstance<EmulationFragment>()
.firstOrNull()
currentFragment != null && currentFragment !== this
} catch (_: Exception) {
false
}
}
// xbzk: called from EmulationActivity when a new game is loaded while this fragment is still active,
// to wait for the emulation thread to stop before allowing the ROM swap to proceed
fun notifyWhenEmulationThreadStops(onStopped: () -> Unit) {
if (!this::emulationState.isInitialized) {
onStopped()
return
}
val emuThread = runCatching { emulationState.emulationThread }.getOrNull()
if (emuThread == null || !emuThread.isAlive) {
onStopped()
return
}
Thread({
runCatching { emuThread.join() }
Handler(Looper.getMainLooper()).post {
onStopped()
}
}, "RomSwapWait").start()
}
// xbzk: called from EmulationActivity when a new game is loaded while this
// fragment is still active, to stop the current emulation before swapping the ROM
fun stopForRomSwap() {
if (isStoppingForRomSwap) {
return
}
isStoppingForRomSwap = true
clearPausedFrame()
emulationViewModel.setIsEmulationStopping(true)
_binding?.let {
binding.loadingText.setText(R.string.shutting_down)
ViewUtils.showView(binding.loadingIndicator)
ViewUtils.hideView(binding.inputContainer)
ViewUtils.hideView(binding.showStatsOverlayText)
}
if (this::emulationState.isInitialized) {
emulationState.stop()
if (NativeLibrary.isRunning() || NativeLibrary.isPaused()) {
Log.warning("[EmulationFragment] ROM swap stop fallback: forcing native stop request.")
NativeLibrary.stopEmulation()
}
} else {
NativeLibrary.stopEmulation()
}
NativeConfig.reloadGlobalConfig()
}
private fun showOverlayOptions() {
val anchor = binding.inGameMenu.findViewById<View>(R.id.menu_overlay_controls)
val popup = PopupMenu(requireContext(), anchor)
@ -2134,6 +2224,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
state = State.STOPPED
} else {
Log.warning("[EmulationFragment] Stop called while already stopped.")
NativeLibrary.stopEmulation()
}
}

Loading…
Cancel
Save