Skip to content

Commit 9b8970a

Browse files
committed
sessions: end same-display session when the launcher UI returns to foreground
Returning to Argosy left the play session alive for a 15s UsageStats grace that nothing re-checked: gamepad input was swallowed by the dead-game forward branch, the new task-removed release was blocked by the stale flag, and the bottom screen kept the in-game panel until a second resume. cleanupStaleSession now ends any session on the launcher's own display on every resume and clears SessionStateStore synchronously so the companion flips immediately; only cross-display sessions (swapped roles, per-game display target) survive, which also removes the UsageStats dependency and the swapped-mode resume clearing bug. Input dispatch defers to the emulator only when it runs on a different display. onTaskRemoved checks the in-memory session via new hasLiveSession instead of the prefs flag that lags behind save sync. The companion respawn check no longer honors a persisted session when the process restarted, it is an orphan by definition. requiresEmulatorKill emulators get force-stopped at launch when no session is tracked since the lingering-session kill trigger is gone.
1 parent aa7d55a commit 9b8970a

5 files changed

Lines changed: 38 additions & 21 deletions

File tree

app/src/main/kotlin/com/nendo/argosy/DualScreenManager.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ class DualScreenManager(
128128
com.nendo.argosy.util.SecondaryHomeComponent.setEnabled(appContext, enabled)
129129
}
130130

131+
/** Live in-memory session check; unlike SessionStateStore.hasActiveSession this flips false the moment session teardown begins, not after save sync completes. */
132+
fun hasLiveSession(): Boolean = playSessionTracker.activeSession.value != null
133+
131134
fun teardownCompanion() {
132135
stopStartupGuard()
133136
companionLaunchJob?.cancel()

app/src/main/kotlin/com/nendo/argosy/MainActivity.kt

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ import android.view.Display
4949
import com.nendo.argosy.hardware.SecondaryHomeActivity
5050
import com.nendo.argosy.util.DisplayAffinityHelper
5151
import com.nendo.argosy.util.DisplayRoleResolver
52-
import com.nendo.argosy.util.PermissionHelper
5352
import dagger.hilt.android.AndroidEntryPoint
5453
import com.nendo.argosy.util.SafeCoroutineScope
5554
import kotlinx.coroutines.Dispatchers
@@ -86,7 +85,6 @@ class MainActivity : ComponentActivity() {
8685
@Inject lateinit var ambientLedManager: AmbientLedManager
8786
@Inject lateinit var screenCaptureManager: ScreenCaptureManager
8887
@Inject lateinit var displayAffinityHelper: DisplayAffinityHelper
89-
@Inject lateinit var permissionHelper: PermissionHelper
9088
@Inject lateinit var gameActionsDelegate: GameActionsDelegate
9189
@Inject lateinit var gameLaunchDelegate: GameLaunchDelegate
9290
@Inject lateinit var saveCacheManager: SaveCacheManager
@@ -417,7 +415,7 @@ class MainActivity : ComponentActivity() {
417415
) return true
418416
}
419417

420-
if (dualScreenManager.swappedIsGameActive.value && !isOverlayFocused) {
418+
if (dualScreenManager.swappedIsGameActive.value && !isOverlayFocused && isGameOnOtherDisplay()) {
421419
val emulatorDispatcher = dualScreenManager.emulatorKeyDispatcher
422420
if (emulatorDispatcher != null) {
423421
Log.d(TAG, "dispatchKeyEvent: FORWARDING key=${event.keyCode} to emulator")
@@ -488,7 +486,7 @@ class MainActivity : ComponentActivity() {
488486
triggerAxisKeyEmitter.emit(event).forEach { dispatchKeyEvent(it) }
489487

490488
if (!dualScreenManager.claimInput(event)) return true
491-
if (dualScreenManager.swappedIsGameActive.value && !isOverlayFocused) {
489+
if (dualScreenManager.swappedIsGameActive.value && !isOverlayFocused && isGameOnOtherDisplay()) {
492490
val emulatorDispatcher = dualScreenManager.emulatorMotionDispatcher
493491
if (emulatorDispatcher != null) {
494492
return emulatorDispatcher(event)
@@ -587,6 +585,13 @@ class MainActivity : ComponentActivity() {
587585

588586
// --- Private Helpers ---
589587

588+
/** True only when a session's emulator runs on a different display than this activity; a same-display session cannot have focus while we do, so input must never be deferred to it. */
589+
private fun isGameOnOtherDisplay(): Boolean {
590+
val emulatorDisplay = dualScreenManager.emulatorDisplayId ?: return false
591+
val ownDisplay = window.decorView.display?.displayId ?: return false
592+
return emulatorDisplay != ownDisplay
593+
}
594+
590595
/** Relinks companion input forwarding when input arrives on home but the link is stale (companion marked inactive or overlay focus latched) after a game, sleep/wake, or a foreground app yielding the secondary display. */
591596
private fun reassertCompanionForwarding() {
592597
if (!::dualScreenManager.isInitialized) return
@@ -601,13 +606,6 @@ class MainActivity : ComponentActivity() {
601606
}
602607
}
603608

604-
/**
605-
* Best-effort housekeeping when we come back to the launcher: if a persisted
606-
* session exists but the emulator is no longer in the foreground, clear it so
607-
* stale state doesn't linger. Does not resume or yield to the emulator -- that
608-
* path was too eager and occasionally pulled the user into a phantom session
609-
* on cold start.
610-
*/
611609
/**
612610
* Re-checks localPath for every downloaded game whenever the user returns to
613611
* the launcher. Catches manual deletions performed outside the app (e.g. via
@@ -621,17 +619,25 @@ class MainActivity : ComponentActivity() {
621619
}
622620
}
623621

622+
/**
623+
* The launcher UI returning to the foreground means the session on this display
624+
* is over: end it and restore the companion immediately. Only a session running
625+
* on a different display (swapped roles / per-game display target) survives,
626+
* since the game and the launcher UI legitimately coexist there.
627+
*/
624628
private fun cleanupStaleSession() {
625629
activityScope.launch {
626630
if (!::dualScreenManager.isInitialized) return@launch
627631
if (dualScreenManager.isLaunchingGame) return@launch
628-
if (dualScreenManager.emulatorDisplayId != null) {
629-
val emulatorPkg = sessionStateStore.getEmulatorPackage() ?: return@launch
630-
if (permissionHelper.isPackageInForeground(this@MainActivity, emulatorPkg, 15_000)) return@launch
631-
}
632-
if (preferencesRepository.getPersistedSession() == null) return@launch
632+
val emulatorDisplay = dualScreenManager.emulatorDisplayId
633+
val ownDisplay = window.decorView.display?.displayId
634+
if (emulatorDisplay != null && ownDisplay != null && emulatorDisplay != ownDisplay) return@launch
635+
if (playSessionTracker.activeSession.value == null &&
636+
preferencesRepository.getPersistedSession() == null
637+
) return@launch
633638

634639
dualScreenManager.emulatorDisplayId = null
640+
sessionStateStore.clearSession()
635641
playSessionTracker.endSessionInBackground()
636642
dualScreenManager.broadcastSessionCleared()
637643
if (displayAffinityHelper.hasSecondaryDisplay) {

app/src/main/kotlin/com/nendo/argosy/hardware/CompanionGuardService.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,13 @@ class CompanionGuardService : Service() {
6868
super.onTaskRemoved(rootIntent)
6969
if (rootIntent?.component?.className != MainActivity::class.java.name) return
7070
if (SecondaryHomeComponent.isDefaultHome(this)) return
71-
if (SessionStateStore(applicationContext).hasActiveSession()) return
71+
val dsm = DualScreenManagerHolder.instance
72+
val sessionLive = dsm?.hasLiveSession()
73+
?: SessionStateStore(applicationContext).hasActiveSession()
74+
if (sessionLive) return
7275
Log.i(TAG, "Main task removed while not default home, releasing secondary display")
7376
SecondaryHomeComponent.setEnabled(this, false)
74-
DualScreenManagerHolder.instance?.teardownCompanion()
77+
dsm?.teardownCompanion()
7578
stopSelf()
7679
}
7780

app/src/main/kotlin/com/nendo/argosy/hardware/SecondaryHomeActivity.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,7 @@ class SecondaryHomeActivity :
120120
if (existing != null) {
121121
dsm = existing
122122
initializeCompanion()
123-
} else if (!com.nendo.argosy.util.SecondaryHomeComponent.isDefaultHome(this) &&
124-
!SessionStateStore(applicationContext).hasActiveSession()
125-
) {
123+
} else if (!com.nendo.argosy.util.SecondaryHomeComponent.isDefaultHome(this)) {
126124
android.util.Log.i("SecondaryHome", "Respawned without a running Argosy and not default home, releasing secondary display")
127125
com.nendo.argosy.util.SecondaryHomeComponent.setEnabled(this, false)
128126
CompanionGuardService.stop(this)

app/src/main/kotlin/com/nendo/argosy/ui/screens/common/GameLaunchDelegate.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,13 @@ class GameLaunchDelegate @Inject constructor(
237237

238238
val emulatorPackage = emulatorResolver.getEmulatorPackageForGame(gameId, game.platformId, game.platformSlug)
239239
val emulatorId = emulatorPackage?.let { emulatorResolver.resolveEmulatorId(it) }
240+
if (activeSession == null && emulatorPackage != null && emulatorId != null &&
241+
EmulatorRegistry.getById(emulatorId)?.launchConfig?.requiresEmulatorKill == true
242+
) {
243+
android.util.Log.d("GameLaunchDelegate", "Force-stopping $emulatorPackage before launch (requiresEmulatorKill, no tracked session)")
244+
gameLauncher.forceStopEmulator(emulatorPackage)
245+
delay(EMULATOR_KILL_DELAY_MS)
246+
}
240247
val prefs = preferencesRepository.preferences.first()
241248
val canSync = resolvedVariantId == null && emulatorId != null && SavePathRegistry.canSyncWithSettings(
242249
emulatorId,

0 commit comments

Comments
 (0)