fix(session): no endless loading screen after a game exits back to Steam (#2359)

* fix(session): avoid stale relaunch loading overlay

* fix(session): keep the loading screen while the runtime installs
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-11-09 17:09:27 -05:00
1 parent 1b19837fc6
commit 836340f3a2
6 files changed
+112 -5

No files matched your search

@@ -121,6 +121,7 @@ import com.droiddeck.launcher.session.SessionArtifacts
import com.droiddeck.launcher.session.SessionLogShare
import com.droiddeck.launcher.session.SessionPhase
import com.droiddeck.launcher.session.SessionPaths
import com.droiddeck.launcher.session.SessionLoadingDecision
import com.droiddeck.launcher.wayland.HdrSupport
import com.droiddeck.launcher.session.SessionService
import com.droiddeck.launcher.session.SessionState
@@ -2435,6 +2436,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private fun watchSession() {
val handler = Handler(Looper.getMainLooper())
var ticks = 0
val loadingStartedAt = SystemClock.elapsedRealtime()
val poll = object : Runnable {
override fun run() {
if (!watching) return
@@ -2447,6 +2449,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
finish()
return
}
if (loading.visible && !loading.ended && !SessionState.running && !installingRuntime &&
SessionLoadingDecision.shouldReturnToLauncher(
loadingStartedAt, SystemClock.elapsedRealtime(), SessionState.running, SessionState.phase)) {
finish()
return
}
if (loading.visible && !loading.ended) {
if (!installingRuntime) loading.update(this@SessionActivity, SessionState.logFile)
if (ticks++ % 2 == 0) loading.tick()
@@ -2471,8 +2479,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
val hint = sessionExitHint(status)
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
if (status == 0) SessionState.relaunch?.let { next ->
SessionState.relaunch = null
if (status == 0) SessionState.takeRelaunch()?.let { next ->
next.putExtra(EXTRA_RETURN_HOME, intent.getBooleanExtra(EXTRA_RETURN_HOME, false))
next.putExtra(EXTRA_LINK_TASK, intent.getIntExtra(EXTRA_LINK_TASK, -1))
setIntent(next)
@@ -14,6 +14,7 @@ object SessionEvents {
SessionPaths.take()
val dir = SessionPaths.beginOrCurrent(context, label ?: SessionPaths.label(mode, null))
synchronized(SessionState) {
SessionState.clearRelaunch()
SessionState.phase = SessionPhase.PREPARING
SessionState.mode = mode
SessionState.sessionId = dir.name
@@ -0,0 +1,8 @@
package com.droiddeck.launcher.session
internal object SessionLoadingDecision {
const val TIMEOUT_MS = 10_000L
fun shouldReturnToLauncher(startedAt: Long, now: Long, running: Boolean, phase: SessionPhase): Boolean =
!running && phase in setOf(SessionPhase.IDLE, SessionPhase.FAILED) && now - startedAt >= TIMEOUT_MS
}
@@ -1697,8 +1697,8 @@ class SessionService : Service() {
.putExtra(EXTRA_STEAM_UI, ui)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (url.isNotEmpty()) next.putExtra(EXTRA_STEAM_URL, url)
SessionState.relaunch = next
android.os.Handler(android.os.Looper.getMainLooper()).post { stopSession(0) }
SessionState.queueRelaunch(next)
android.os.Handler(android.os.Looper.getMainLooper()).post { stopSession(0, relaunch = true) }
}
}
watcher.startWatching()
@@ -2013,7 +2013,7 @@ class SessionService : Service() {
}
}
private fun stopSession(status: Int, gen: Int? = null) {
private fun stopSession(status: Int, gen: Int? = null, relaunch: Boolean = false) {
if (gen != null && gen != sessionGen) {
Log.i(TAG, "ignoring a stop from session $gen; session $sessionGen is current")
return
@@ -2024,6 +2024,7 @@ class SessionService : Service() {
cancelQueuedSteamGame()
gameExitWatchToken++
if (!SessionState.running) return
if (relaunch) SessionState.confirmRelaunchStop()
SessionEvents.record("guest.exited", mapOf("status" to status))
SessionEvents.transition(SessionPhase.STOPPING, "session.stopping", mapOf("status" to status))
SessionState.running = false
@@ -73,7 +73,44 @@ object SessionState {
@Volatile
var steamClientArch: String? = null
var steamVr by mutableStateOf(false)
@Volatile
var relaunch: android.content.Intent? = null
@Volatile
private var relaunchSessionId: String? = null
@Volatile
private var relaunchStopRequested = false
fun queueRelaunch(intent: android.content.Intent) {
synchronized(this) {
relaunch = intent
relaunchSessionId = sessionId
relaunchStopRequested = false
}
}
fun confirmRelaunchStop() {
synchronized(this) {
relaunchStopRequested = relaunch != null && relaunchSessionId == sessionId
}
}
fun takeRelaunch(): android.content.Intent? = synchronized(this) {
val next = relaunch
val valid = relaunchStopRequested && relaunchSessionId == sessionId
relaunch = null
relaunchSessionId = null
relaunchStopRequested = false
next.takeIf { valid }
}
fun clearRelaunch() {
synchronized(this) {
relaunch = null
relaunchSessionId = null
relaunchStopRequested = false
}
}
@JvmStatic var hdr = false
@Volatile private var deckPadValue = false
@@ -0,0 +1,53 @@
package com.droiddeck.launcher.session
import android.content.Intent
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
class SessionLoadingDecisionTest {
private var previousSessionId: String? = null
private var previousRelaunch: Intent? = null
@Before
fun setUp() {
previousSessionId = SessionState.sessionId
previousRelaunch = SessionState.relaunch
SessionState.clearRelaunch()
}
@After
fun tearDown() {
SessionState.sessionId = previousSessionId
SessionState.relaunch = previousRelaunch
SessionState.clearRelaunch()
if (previousRelaunch != null) SessionState.queueRelaunch(previousRelaunch!!)
}
@Test
fun loadingTimesOutOnlyWhenNoSessionStarted() {
assertEquals(false, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_099L, false, SessionPhase.IDLE))
assertEquals(true, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_100L, false, SessionPhase.IDLE))
assertEquals(false, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_100L, true, SessionPhase.IDLE))
assertEquals(false, SessionLoadingDecision.shouldReturnToLauncher(100L, 10_100L, false, SessionPhase.PREPARING))
}
@Test
fun relaunchRequiresTheCurrentSessionToHaveRequestedTheStop() {
SessionState.sessionId = "session-a"
val next = Intent().apply { action = "next" }
SessionState.queueRelaunch(next)
assertNull(SessionState.takeRelaunch())
SessionState.queueRelaunch(next)
SessionState.confirmRelaunchStop()
assertEquals(next, SessionState.takeRelaunch())
assertNull(SessionState.takeRelaunch())
SessionState.queueRelaunch(next)
SessionState.confirmRelaunchStop()
SessionState.sessionId = "session-b"
assertNull(SessionState.takeRelaunch())
}
}