@ -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 ( !is WaitingForRomSwapStop ) {
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 ( !is SwapIntent ( intent ) ) {
return
}
pendingRomSwapIntent = Intent ( intent )
if ( !is WaitingForRomSwapStop ) {
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 ( !is WaitingForRomSwapStop || 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 ( !is WaitingForRomSwapStop ) {
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 ( !is WaitingForRomSwapStop ) {
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 )