From 57014eec4dbb54d8255365131469ffe0834bb4e2 Mon Sep 17 00:00:00 2001 From: bug <45903641+inssekt@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:37:03 +0000 Subject: [PATCH] Allow frontends to launch a new game without force closing Dolphin Original message: Hey devs, I'm moth, lead developer of the Cocoon Frontend For our now playing integration, we're trying to maximise compatability for launching & swapping games without the user having to force close out of the activity. Dolphin however does not support this at all. No matter what mix of flags and intents I try it just shows the MainActivity. After a deep dive I found out: EmulationActivity has an ignoreLaunchRequests check that is set to true until the end of NativeLibrary.Run() the launch() method returns if its true and just keeps you in MainActivity, never launching the new ROM This is a problem because Cocoon and other frontends need to use CLEAR_TASK to destroy the existing task and start fresh to load the new ROM. This destroys EmulationActivity & Fragment but the process still survives so NativeLibrary.Run never returns gracefully. This makes it so relaunching a new ROM does not work, even if we clear the activity. Also MainActivity or EmulationActivity doesnt handle onNewIntent() This would be the graceful way to handle it so we can use CLEAR_TOP | SINGLE_TOP without being ignored. The two fixes are: in StartupHandler.HandleInit(), reset the flag before launching so the cleared task's limbo state of ignoreLaunchRequests is reset The more graceful way would be to add override fun onNewIntent(intent: Intent) super.onNewIntent(intent) setIntent(intent) StartupHandler.HandleInit(this) } in your MainActivity This would let frontends redeliver game intents without destroying the entire task After implementation: I added multiple guards to ensure proper synchronicity and ensure emulators and surfaces are finished before starting a new game. Frontends can now take advantage of NEW_TASK | CLEAR_TASK to launch a new Dolphin game. --- .../dolphinemu/fragments/EmulationFragment.kt | 10 ++++- .../dolphinemu/ui/main/MainActivity.kt | 7 ++++ .../dolphinemu/ui/main/TvMainActivity.kt | 6 +++ .../dolphinemu/utils/StartupHandler.java | 40 ++++++++++++++++++- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt index e4698ba43c..e0efc43611 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/fragments/EmulationFragment.kt @@ -119,7 +119,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } override fun onDetach() { - NativeLibrary.clearEmulationActivity() + // Don't clear the reference if a newer EmulationActivity already took over + if (NativeLibrary.getEmulationActivity() === emulationActivity) { + NativeLibrary.clearEmulationActivity() + } super.onDetach() } @@ -154,7 +157,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { override fun surfaceDestroyed(holder: SurfaceHolder) { Log.debug("[EmulationFragment] Surface destroyed.") - NativeLibrary.SurfaceDestroyed() + // Don't tear down the surface if a newer EmulationActivity already took over + if (NativeLibrary.getEmulationActivity() === emulationActivity) { + NativeLibrary.SurfaceDestroyed() + } runWhenSurfaceIsValid = true } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt index 761c6dbfef..d61f1097c8 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.kt @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main +import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.view.Menu @@ -90,6 +91,12 @@ class MainActivity : AppCompatActivity(), MainView, OnRefreshListener, ThemeProv } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + StartupHandler.HandleInit(this) + } + override fun onResume() { ThemeHelper.setCorrectTheme(this) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt index 09bacd201b..2cd8288eb4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.kt @@ -66,6 +66,12 @@ class TvMainActivity : FragmentActivity(), MainView, OnRefreshListener { } } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + StartupHandler.HandleInit(this) + } + override fun onResume() { super.onResume() if (DirectoryInitialization.shouldStart(this)) { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java index db003b82b2..775e8a3330 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java @@ -9,7 +9,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; +import android.util.Log; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.LifecycleOwner; @@ -38,11 +41,44 @@ public final class StartupHandler String[] gamesToLaunch = getGamesFromIntent(parent.getIntent()); if (gamesToLaunch != null && gamesToLaunch.length > 0) { - // Start the emulation activity, send the ISO passed in and finish the main activity - EmulationActivity.launch(parent, gamesToLaunch, false, true); + // If a previous session is still running, stop it and wait for it to finish + // before launching the new game. StopEmulation is async, so launching immediately + // would race with the old emulation thread's cleanup. + if (!NativeLibrary.IsUninitialized()) + { + NativeLibrary.StopEmulation(); + waitForStopThenLaunch(parent, gamesToLaunch); + } + else + { + EmulationActivity.stopIgnoringLaunchRequests(); + EmulationActivity.launch(parent, gamesToLaunch, false, true); + } } } + private static void waitForStopThenLaunch(FragmentActivity parent, String[] gamesToLaunch) + { + Handler handler = new Handler(Looper.getMainLooper()); + final long deadline = System.currentTimeMillis() + 5000; + handler.postDelayed(new Runnable() + { + @Override + public void run() + { + if (NativeLibrary.IsUninitialized() || System.currentTimeMillis() >= deadline) + { + EmulationActivity.stopIgnoringLaunchRequests(); + EmulationActivity.launch(parent, gamesToLaunch, false, true); + } + else + { + handler.postDelayed(this, 100); + } + } + }, 100); + } + private static String[] getGamesFromIntent(Intent intent) { // Priority order when looking for game paths in an intent: