Skip to content

fix: clear stale container state on task swipe and app restart#1136

Merged
utkarshdalal merged 2 commits intoutkarshdalal:masterfrom
jeremybernstein:jb/fix-task-swipe-container-stuck
Apr 13, 2026
Merged

fix: clear stale container state on task swipe and app restart#1136
utkarshdalal merged 2 commits intoutkarshdalal:masterfrom
jeremybernstein:jb/fix-task-swipe-container-stuck

Conversation

@jeremybernstein
Copy link
Copy Markdown
Contributor

@jeremybernstein jeremybernstein commented Apr 7, 2026

Description

Fixes app getting stuck on the container screen after task-swiping during a game.

onDestroy was calling super.onDestroy() before emitting ActivityDestroyed, so Compose DisposableEffect listeners (including exit()) were already unregistered and never fired. Even when re-ordered, a race between listener teardown and event emission can still leave stale state.

  • Reorder onDestroy so ActivityDestroyed fires before lifecycle teardown, giving exit() a chance to run
  • Add fallback cleanup in onDestroy when exit() doesn't fire (listener race / task swipe)
  • Clear stale keepAlive flag in onCreate when no container is actually running (crash recovery)

Closes #1135

Recording

n/a — no visible UI changes

Test plan

  • Launch a game, swipe app from recents, reopen — should not be stuck on container screen
  • Launch a game, exit normally — should still work as before
  • Force-kill app during game, reopen — should recover cleanly

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • I have NOT attached a recording of the change (it's backend).
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented lingering background keep-alive at startup and ensured activity destruction reliably triggers coordinated cleanup so resources are released promptly.
    • Background services will now stop when the app task is removed if no active work remains, reducing orphaned background activity.
  • Refactor

    • Consolidated environment shutdown into a single, resilient teardown used by screen exits and activity-destroy flows for more predictable cleanup.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b5939f82-e313-478b-a79c-6ee2cbf0a785

📥 Commits

Reviewing files that changed from the base of the PR and between c887b7a and 1989857.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/epic/EpicService.kt
  • app/src/main/java/app/gamenative/service/gog/GOGService.kt

📝 Walkthrough

Walkthrough

Added a centralized teardown API PluviaApp.shutdownEnvironment() and wired it into lifecycle and screen exit flows: MainActivity now pre-shuts a stale environment at startup and conditionally during destruction, XServerScreen delegates environment cleanup to the new API, and multiple services now handle task-removal by stopping themselves when no active work remains.

Changes

Cohort / File(s) Summary
Activity lifecycle
app/src/main/java/app/gamenative/MainActivity.kt
Added startup guard: if SteamService.keepAlive is true while PluviaApp.xEnvironment is null, call PluviaApp.shutdownEnvironment(). Reordered onDestroy() to emit AndroidEvent.ActivityDestroyed before super.onDestroy() when not changing configurations and conditionally call shutdownEnvironment() if SteamService.keepAlive remains true.
Centralized teardown
app/src/main/java/app/gamenative/PluviaApp.kt
Added companion fun shutdownEnvironment() that snapshots xEnvironment, then (safely via runCatching) stops watchers/handlers, clears Steam cached achievements, releases pointer capture, stops env components, nulls app environment/input/touchpad references, sets SteamService.keepAlive = false, and clears suspend state.
Screen cleanup callers
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Removed inline environment teardown from private exit(...) (dropped environment param). exit now attempts winHandler?.stop() and delegates remaining environment shutdown to PluviaApp.shutdownEnvironment() while preserving session metadata writes, container save, background trash cleanup, and navigation/onExit branching.
Services: task removal handling
app/src/main/java/app/gamenative/service/.../SteamService.kt, .../epic/EpicService.kt, .../gog/GOGService.kt
Each service now overrides onTaskRemoved(rootIntent: Intent?) to call stopSelf() only when hasActiveOperations() is false; otherwise they log that work remains and keep running. GOGService.hasActiveOperations() now also considers hasActiveDownload().

Sequence Diagram(s)

sequenceDiagram
  participant MainActivity as MainActivity
  participant SteamService as SteamService
  participant PluviaApp as PluviaApp
  participant XServerScreen as XServerScreen

  MainActivity->>SteamService: read keepAlive
  alt keepAlive && PluviaApp.xEnvironment == null
    MainActivity->>PluviaApp: shutdownEnvironment()
    PluviaApp->>PluviaApp: stop watchers, stop env components
    PluviaApp->>SteamService: clearCachedAchievements()
    PluviaApp->>SteamService: set keepAlive = false
    PluviaApp->>PluviaApp: null refs, clear suspend state
  end

  XServerScreen->>XServerScreen: exit() -> winHandler?.stop()
  XServerScreen->>PluviaApp: shutdownEnvironment()

  MainActivity->>MainActivity: emit AndroidEvent.ActivityDestroyed (before super)
  alt SteamService.keepAlive still true
    MainActivity->>PluviaApp: shutdownEnvironment()
  end
  MainActivity->>MainActivity: super.onDestroy()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

I hopped through logs at dawn's light beam,
A stubborn keep-alive broke the dream.
I nudged the env, swept caches clean,
Closed sleepy tasks and cleared the scene. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately captures the main fix: clearing stale container state when the app is task-swiped or restarted.
Description check ✅ Passed The PR description covers the root cause, the changes made, and the test plan. However, the checklist indicates 'I have NOT attached a recording' when the template requires one, though this is acknowledged as backend-only changes.
Linked Issues check ✅ Passed The PR directly addresses issue #1135 by preventing stale keepAlive flags, ensuring exit() cleanup runs on destruction, and adding startup recovery logic in onCreate to detect when no container is active.
Out of Scope Changes check ✅ Passed All changes are within scope: MainActivity guards against stale keepAlive, PluviaApp.shutdownEnvironment() provides coordinated teardown, XServerScreen.exit() delegates to it, and Services implement onTaskRemoved to stop gracefully.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 1 file

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/MainActivity.kt`:
- Around line 279-287: The partial fallback in MainActivity (the block that
checks SteamService.keepAlive and calls env.stopEnvironmentComponents(), clears
PluviaApp.xEnvironment and SteamService.keepAlive, and calls
PluviaApp.clearActiveSuspendState()) must instead delegate to the same full
teardown routine used by the XServerScreen exit path; update the MainActivity
fallback to invoke the centralized cleanup function used by XServerScreen.exit()
(or refactor the XServerScreen exit logic into a shared
PluviaApp.performFullTeardown()/shutdownEnvironment() and call that) so
achievement watcher shutdown, cached-achievement reset, input/touchpad singleton
cleanup, and the existing stopEnvironmentComponents/keepAlive/xEnvironment
clearing all run in both paths.
- Around line 275-290: The ActivityDestroyed event is emitted unconditionally in
MainActivity.onDestroy which triggers terminal shutdown logic (exit()) during
configuration-change recreations; wrap the
PluviaApp.events.emit(AndroidEvent.ActivityDestroyed) call in a guard that
checks isChangingConfigurations (or isFinishing()) and only emit when the
activity is not being recreated (e.g., if (!isChangingConfigurations) {
PluviaApp.events.emit(AndroidEvent.ActivityDestroyed) }) so temporary rotations
don't trigger XServerScreen's listener and shutdown logic in exit().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b07f152e-4244-4ac2-a1f1-c794b095fb54

📥 Commits

Reviewing files that changed from the base of the PR and between f5200c5 and d66ab5a.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/MainActivity.kt

@jeremybernstein jeremybernstein force-pushed the jb/fix-task-swipe-container-stuck branch from d66ab5a to ecdd1a9 Compare April 7, 2026 21:13
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/MainActivity.kt`:
- Around line 273-284: The new onDestroy path can race with the async
environment bootstrap in XServerScreen.kt that assigns PluviaApp.xEnvironment;
cancel or gate that setup work so it cannot assign or start the environment
after the Activity has been destroyed. Specifically, add cancellation or a
session/state check to the background bootstrap job in XServerScreen.kt (the
task that constructs/publishes PluviaApp.xEnvironment) so it either aborts if
onDestroy has run or isCancelled, or obtains a short-lived session token that
onDestroy invalidates before calling
PluviaApp.events.emit/PluviaApp.shutdownEnvironment; ensure the bootstrap checks
that token or !isDestroyed (or job.isActive) right before setting
PluviaApp.xEnvironment and starting services so a resurrected environment cannot
appear after onDestroy/shutdownEnvironment runs.
- Around line 147-150: The stale-session check currently uses
SteamService.keepAlive && PluviaApp.xEnvironment == null which also matches
non-XServer flows; change the guard to only clear the stale keepAlive when it
truly represents a game/XServer session by also checking the game-session
marker: call PluviaApp.hasValidSuspendPolicyState() and only invoke
PluviaApp.shutdownEnvironment() when SteamService.keepAlive is true,
PluviaApp.xEnvironment is null, and PluviaApp.hasValidSuspendPolicyState() is
false (i.e., gate the shutdown on the absence of a valid suspend policy state).

In `@app/src/main/java/app/gamenative/PluviaApp.kt`:
- Around line 204-225: shutdownEnvironment() must perform a complete,
exception-safe teardown: ensure the WinHandler.stop() cleanup and the null/flag
resets always run even if env?.stopEnvironmentComponents() throws. Modify
shutdownEnvironment() to call env?.stopEnvironmentComponents() inside a
try/catch (log the exception) and then in a finally block call
WinHandler.stop(), set xEnvironment = null, SteamService.keepAlive = false,
clearActiveSuspendState(), and null out
inputControlsView/inputControlsManager/touchpadView/achievementWatcher; also
move touchpadView?.releasePointerCapture() into the safe teardown flow. Ensure
XServerScreen.exit() uses this helper so both paths are identical.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23707692-13cd-4d71-a1ab-a93472b79435

📥 Commits

Reviewing files that changed from the base of the PR and between d66ab5a and ecdd1a9.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

@jeremybernstein jeremybernstein force-pushed the jb/fix-task-swipe-container-stuck branch from ecdd1a9 to 809edb2 Compare April 7, 2026 21:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/PluviaApp.kt (1)

204-229: ⚠️ Potential issue | 🟠 Major

Finish making shutdownEnvironment() the full teardown path.

This is much closer now, but XServerScreen.exit() still has to stop WinHandler separately on Line 3395, so this helper is not actually the single source of truth yet. The single try also short-circuits env?.stopEnvironmentComponents() if achievementWatcher?.stop(), SteamService.clearCachedAchievements(), or touchpadView?.releasePointerCapture() throws first. Since XEnvironment.stopEnvironmentComponents() does not stop WinHandler, callers that rely on shutdownEnvironment() alone can still leave the handler/socket alive or miss the actual environment stop.

Suggested shape
+import com.winlator.xenvironment.components.XServerComponent
+
 fun shutdownEnvironment() {
     val env = xEnvironment
+    val winHandler = env?.getComponent(XServerComponent::class.java)?.xServer?.winHandler
     Timber.i("shutdownEnvironment: env=%s", env != null)

-    try {
-        achievementWatcher?.stop()
-        achievementWatcher = null
-        SteamService.clearCachedAchievements()
-        touchpadView?.releasePointerCapture()
-        env?.stopEnvironmentComponents()
-    } catch (e: Exception) {
-        Timber.e(e, "shutdownEnvironment: error during component teardown")
-    } finally {
-        xEnvironment = null
-        inputControlsView = null
-        inputControlsManager = null
-        touchpadView = null
-        achievementWatcher = null
-        SteamService.keepAlive = false
-        clearActiveSuspendState()
-    }
+    runCatching { achievementWatcher?.stop() }
+        .onFailure { Timber.e(it, "shutdownEnvironment: error stopping achievement watcher") }
+    achievementWatcher = null
+
+    runCatching { SteamService.clearCachedAchievements() }
+        .onFailure { Timber.e(it, "shutdownEnvironment: error clearing cached achievements") }
+    runCatching { touchpadView?.releasePointerCapture() }
+        .onFailure { Timber.e(it, "shutdownEnvironment: error releasing pointer capture") }
+    runCatching { winHandler?.stop() }
+        .onFailure { Timber.e(it, "shutdownEnvironment: error stopping WinHandler") }
+    runCatching { env?.stopEnvironmentComponents() }
+        .onFailure { Timber.e(it, "shutdownEnvironment: error during component teardown") }
+
+    xEnvironment = null
+    inputControlsView = null
+    inputControlsManager = null
+    touchpadView = null
+    achievementWatcher = null
+    SteamService.keepAlive = false
+    clearActiveSuspendState()
 }

After that, XServerScreen.exit() can delegate completely instead of keeping a separate handler stop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/PluviaApp.kt` around lines 204 - 229,
shutdownEnvironment currently short-circuits if early teardown steps throw and
doesn't stop WinHandler/socket, so make it the single teardown path by ensuring
all components run even if one fails and by stopping the WinHandler there; wrap
each step (achievementWatcher?.stop(), SteamService.clearCachedAchievements(),
touchpadView?.releasePointerCapture(), env?.stopEnvironmentComponents(), and an
explicit WinHandler.stop()/socket close) in their own try/catch or otherwise
catch exceptions per-step so later cleanup still runs, call the WinHandler
stop/socket close from shutdownEnvironment (or have
XEnvironment.stopEnvironmentComponents include WinHandler shutdown) and then
remove the separate WinHandler stop from XServerScreen.exit so callers can
delegate solely to shutdownEnvironment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@app/src/main/java/app/gamenative/PluviaApp.kt`:
- Around line 204-229: shutdownEnvironment currently short-circuits if early
teardown steps throw and doesn't stop WinHandler/socket, so make it the single
teardown path by ensuring all components run even if one fails and by stopping
the WinHandler there; wrap each step (achievementWatcher?.stop(),
SteamService.clearCachedAchievements(), touchpadView?.releasePointerCapture(),
env?.stopEnvironmentComponents(), and an explicit WinHandler.stop()/socket
close) in their own try/catch or otherwise catch exceptions per-step so later
cleanup still runs, call the WinHandler stop/socket close from
shutdownEnvironment (or have XEnvironment.stopEnvironmentComponents include
WinHandler shutdown) and then remove the separate WinHandler stop from
XServerScreen.exit so callers can delegate solely to shutdownEnvironment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 44199d61-7e79-4184-a845-82f9d8b849bc

📥 Commits

Reviewing files that changed from the base of the PR and between ecdd1a9 and 809edb2.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

This is much closer now, but XServerScreen.exit() still has to stop WinHandler separately on Line 3395, so this helper is not actually the single source of truth yet. The single try also short-circuits env?.stopEnvironmentComponents() if achievementWatcher?.stop(), SteamService.clearCachedAchievements(), or touchpadView?.releasePointerCapture() throws first. Since XEnvironment.stopEnvironmentComponents() does not stop WinHandler, callers that rely on shutdownEnvironment() alone can still leave the handler/socket alive or miss the actual environment stop.

@coderabbitai: I disagree that we need to stop WinHandler in the case of a swipe. The OS will reclaim the socket on its own. It's important for the exit() case, but seems like unjustified extra complexity here.

The single try/catch finding makes sense to me, will be momentarily addressed in a force-push.

@jeremybernstein jeremybernstein force-pushed the jb/fix-task-swipe-container-stuck branch from 809edb2 to 760adaf Compare April 8, 2026 06:26
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

Tip

For best results, initiate chat on the files or code changes.

@jeremybernstein That reasoning is sound — on a task-swipe the process is on its way out and the OS will reclaim the socket, so pushing winHandler?.stop() into shutdownEnvironment() would be unnecessary complexity for that path. The explicit call before PluviaApp.shutdownEnvironment() inside XServerScreen.exit() covers the graceful-exit case where it actually matters. I'll note this for future reviews.

Thanks for confirming on the try/catch point — looking forward to the force-push.


✏️ Learnings added
Learnt from: jeremybernstein
URL: https://github.com/utkarshdalal/GameNative/pull/1136

Timestamp: 2026-04-08T06:26:20.785Z
Learning: In `app/src/main/java/app/gamenative/PluviaApp.kt`, `shutdownEnvironment()` intentionally does **not** call `winHandler?.stop()`. On a task-swipe / `onDestroy` path the OS reclaims the socket naturally, so stopping `WinHandler` there is unnecessary. The explicit `winHandler?.stop()` call before `PluviaApp.shutdownEnvironment()` in `XServerScreen.exit()` covers the graceful-exit case where it actually matters. Do not flag the absence of a `WinHandler` stop inside `shutdownEnvironment()` in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 1136
File: app/src/main/java/app/gamenative/MainActivity.kt:273-284
Timestamp: 2026-04-07T21:36:12.480Z
Learning: In the GameNative repository (`utkarshdalal/GameNative`), `SteamService.keepAlive` is set to `true` in only one place: `ContainerConfigDialog.kt:1032` (inside a folder-picker flow). During the normal XServerScreen game-launch bootstrap (`XServerScreen.kt` lines ~1529-1680), `keepAlive` is `false` throughout. The bootstrap uses a raw Java `Executors.newSingleThreadExecutor().submit {}` (not a coroutine/LaunchedEffect), so Compose teardown does not cancel it. However, since `keepAlive` is `false` during bootstrap, a task-swipe in that window does not produce a stale `keepAlive=true` on the next launch, and therefore does not cause the "stuck on container screen" bug. Do not flag onDestroy's `if (SteamService.keepAlive)` guard as failing to cover the bootstrap-window race in future reviews.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 1136
File: app/src/main/java/app/gamenative/MainActivity.kt:147-150
Timestamp: 2026-04-07T21:37:25.757Z
Learning: In `app/src/main/java/app/gamenative/MainActivity.kt`, the `onCreate` stale-session check `if (SteamService.keepAlive && PluviaApp.xEnvironment == null)` is intentionally kept as-is. The only non-game flow that sets `keepAlive = true` with `xEnvironment == null` is the folder-picker in `ContainerConfigDialog.kt` (line ~1032); if the app is killed in that state, resetting `keepAlive = false` on the next `onCreate` is harmless. Do not flag this guard as incorrectly matching non-XServer flows in future reviews.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 918
File: app/src/main/java/app/gamenative/service/SteamService.kt:3091-3102
Timestamp: 2026-03-19T10:03:51.598Z
Learning: In `app/src/main/java/app/gamenative/service/SteamService.kt`, `connectToSteam()` uses a `retryAttemptAtStart` snapshot to guard the 5s watchdog: after the delay, the watchdog only marks the endpoint bad and disconnects if `!isConnected && retryAttempt == retryAttemptAtStart`. Since `onConnected` resets `retryAttempt = 0` and `onDisconnected` increments `retryAttempt++`, any callback activity during the 5s window changes `retryAttempt`, causing the watchdog to back off. This is the intentional, correct design. Do not flag the `delay(5000)` + `retryAttempt` guard pattern as a race condition in future reviews.

Learnt from: lvonasek
Repo: utkarshdalal/GameNative PR: 1118
File: app/src/main/java/com/winlator/renderer/GLRenderer.java:297-317
Timestamp: 2026-04-06T19:18:32.654Z
Learning: In `app/src/main/java/com/winlator/renderer/GLRenderer.java`, `preWindows()` and `postWindows()` are **intentionally** called while holding the `XServer.Lockable.DRAWABLE_MANAGER` lock inside `renderWindows()`. Subclass overrides (e.g., in `XrRenderer`) are expected to access drawable data that is protected by this lock. Do not flag this as a deadlock risk in future reviews.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 0
File: :0-0
Timestamp: 2026-03-18T16:30:26.610Z
Learning: In `app/src/main/java/app/gamenative/ui/PluviaMain.kt`, the three sites that call `MainActivity.setPendingLaunchRequest`, toggle `shownPendingLaunchSnackbar`, and conditionally call `SnackbarManager.show` (cold-start pending branch, ExternalGameLaunch pending branch, and non-terminal SteamDisconnected branch) are intentionally kept separate — the surrounding logic differs enough that extracting a shared helper would add more complexity than it removes. Do not suggest refactoring these into a helper in future reviews.

Learnt from: jeremybernstein
Repo: utkarshdalal/GameNative PR: 1001
File: app/src/main/java/app/gamenative/ui/screen/xserver/PhysicalControllerHandler.kt:30-31
Timestamp: 2026-03-23T21:41:08.751Z
Learning: In `app/src/main/java/app/gamenative/ui/screen/xserver/PhysicalControllerHandler.kt`, `activeAxisBindings` is a plain `mutableSetOf<Int>()` and is safe without synchronization because all accesses occur on the main thread: Android dispatches MotionEvents (and thus `onGenericMotionEvent` → `processJoystickInput`) on the main thread, and `setProfile` and `cleanup` are called from Compose, which also runs on the main thread. Do not flag this as a thread-safety issue in future reviews.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

3360-3407: LGTM! Clean centralization of teardown logic.

The refactoring correctly delegates cleanup to PluviaApp.shutdownEnvironment() while preserving the explicit winHandler?.stop() call for graceful exits. The comment at line 3395 clearly documents the design rationale.

One minor observation: the environment: XEnvironment? parameter (line 3362) is now unused since shutdownEnvironment() accesses PluviaApp.xEnvironment directly. Consider removing it to clean up the API surface, though this is optional since maintaining the current signature avoids changing all 5 call sites.

Based on learnings: shutdownEnvironment() intentionally does not call winHandler?.stop() — on task-swipe the OS reclaims the socket naturally, and the explicit stop before shutdownEnvironment() covers the graceful-exit case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt` around
lines 3360 - 3407, The parameter environment: XEnvironment? on the exit(...)
function is now unused because shutdown is done via
PluviaApp.shutdownEnvironment(); remove the environment parameter from the exit
function signature (function name: exit) and update all call sites (there are 5)
to stop passing that argument, also remove any now-unused imports or references
to environment inside the file; ensure method references or lambda types that
depended on the old signature are updated accordingly so compilation succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 3360-3407: The parameter environment: XEnvironment? on the
exit(...) function is now unused because shutdown is done via
PluviaApp.shutdownEnvironment(); remove the environment parameter from the exit
function signature (function name: exit) and update all call sites (there are 5)
to stop passing that argument, also remove any now-unused imports or references
to environment inside the file; ensure method references or lambda types that
depended on the old signature are updated accordingly so compilation succeeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54491248-12b8-4f1d-8276-a9802639e54e

📥 Commits

Reviewing files that changed from the base of the PR and between 809edb2 and 760adaf.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 3393-3395: The unguarded call to winHandler?.stop() in the exit
path can throw and prevent PluviaApp.shutdownEnvironment() from running; wrap
the winHandler stop call in a try/catch inside the same exit/teardown routine
(catch Throwable/Exception) and ensure the catch only logs the error (including
the exception) so that PluviaApp.shutdownEnvironment() is always invoked;
reference the winHandler?.stop() invocation and the
PluviaApp.shutdownEnvironment() call to locate where to add the try/catch and
logging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b89e9634-8787-489b-9a54-48e62e2254f6

📥 Commits

Reviewing files that changed from the base of the PR and between 760adaf and b247853.

📒 Files selected for processing (1)
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

@jeremybernstein jeremybernstein force-pushed the jb/fix-task-swipe-container-stuck branch from b247853 to 61b8a9d Compare April 8, 2026 07:43
@jeremybernstein
Copy link
Copy Markdown
Contributor Author

I've confirmed this working on my phone.

@utkarshdalal
Copy link
Copy Markdown
Owner

Can we minimize the scope of this? Looks like the only necessary changes are in MainActivity, can we just do that and leave the rest alone?
Or is it because when we swipe away we want to do all the stuff the exit() function was doing too?

@utkarshdalal
Copy link
Copy Markdown
Owner

also there are conflicts

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

jeremybernstein commented Apr 12, 2026

Or is it because when we swipe away we want to do all the stuff the exit() function was doing too?

☝️ exactly, because we're shutting down. In theory, those resources will be reclaimed by the OS on process death, but if the process survives (e.g. service keeps it alive) they leak/bleed into the next container launch. In fact, that's what happens on my phone: I swipe the game away and the audio keeps playing (and other services remain) until I launch the container again, at which point a) the audio/services stop, and the b) the container doesn't launch. 😄

I think the complete shutdown is the correct solution here. Fixing the conflicts.

when the user swipes the app from the task switcher while a container
is running, keepAlive stays true but xEnvironment is gone. on next
launch the app is stuck thinking a container is running.

extract shutdownEnvironment() from XServerScreen.exit() so the same
full teardown runs in both the normal exit path and the crash recovery
path. each step is wrapped in runCatching so one failure doesn't
prevent the rest.

- onCreate: if keepAlive is set but xEnvironment is null, run
  shutdownEnvironment() to clear stale state
- onDestroy: emit ActivityDestroyed before super (so exit() listeners
  still fire), then force shutdownEnvironment() if keepAlive persists
- exit(): delegates teardown to shutdownEnvironment(), keeps only
  winHandler.stop() and trash cleanup (container-specific)
@jeremybernstein jeremybernstein force-pushed the jb/fix-task-swipe-container-stuck branch from 61b8a9d to c9ae6c2 Compare April 12, 2026 19:07
@jeremybernstein
Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 12, 2026

✅ Actions performed

Reviews resumed.

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

I just noticed that the GN notification doesn't go away when you swipe the app, but now it does (2nd commit just pushed).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/service/gog/GOGService.kt`:
- Around line 698-706: onTaskRemoved currently calls stopSelf() based solely on
hasActiveOperations(), which doesn't account for active downloads; update the
onTaskRemoved method to check for active downloads before stopping the service
(combine hasActiveOperations() with the service's download state, e.g. a
method/field like isDownloading(), downloadManager.hasActiveDownloads(), or
activeDownloads count in GOGService) and only call stopSelf() when neither
operations nor downloads are active; keep the existing Timber logs and adjust
the condition to use the combined check.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bdabad7f-3517-4e49-abcc-a12949aef093

📥 Commits

Reviewing files that changed from the base of the PR and between c9ae6c2 and c887b7a.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/epic/EpicService.kt
  • app/src/main/java/app/gamenative/service/gog/GOGService.kt

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/src/main/java/app/gamenative/service/gog/GOGService.kt">

<violation number="1" location="app/src/main/java/app/gamenative/service/gog/GOGService.kt:700">
P1: `onTaskRemoved` may stop the service during active downloads because it checks only sync state via `hasActiveOperations()`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Steam/GOG/Epic services had no onTaskRemoved — foreground notification
persisted after swipe because nothing told the service to stop itself.
@jeremybernstein jeremybernstein force-pushed the jb/fix-task-swipe-container-stuck branch from c887b7a to 1989857 Compare April 12, 2026 20:22
@jeremybernstein
Copy link
Copy Markdown
Contributor Author

arguably the notification stuff should be its own PR, but it's obviously closely related, so avoiding the overhead.

@xXJSONDeruloXx
Copy link
Copy Markdown
Contributor

xXJSONDeruloXx commented Apr 13, 2026

@utkarshdalal @jeremybernstein if worried about touching sensitive portions of the code, do you think this would be a less invasive bandaid that solves the end user hang for now, then we can solve for the core teardown issues more methodically? Currently I think this is our biggest bug and I would even say a hot fix release might be warranted given how often its coming up

#1195
https://github.com/utkarshdalal/GameNative/pull/1195/changes

@jeremybernstein
Copy link
Copy Markdown
Contributor Author

@utkarshdalal @jeremybernstein if worried about touching sensitive portions of the code, do you think this would be a less invasive bandaid that solves the end user hang for now, then we can solve for the core teardown issues more methodically? Currently I think this is our biggest bug and I would even say a hot fix release might be warranted given how often its coming up

#1195 https://github.com/utkarshdalal/GameNative/pull/1195/changes

Thanks @xXJSONDeruloXx ! I tried a variety of combinations to see what helped. If we don't teardown on swipe, the wine process continues to run (so f.e. I have a game with a title music loop, if I swipe it away, the title music continues to play and won't stop without a Force Quit).

I appreciate the desire for minimal fixes, and that's always my goal and design priority when tackling issues in this codebase. Sadly, sometimes you need to change code to fix code. The main change here is a refactor of existing shutdown code into a reusable function so it can be called from elsewhere, and then calling it from elsewhere, when shutting down the software -- that doesn't seem controversial to me. As a bonus, it solves the problem.

The new second commit ensures that the notification badge disappears. It can be eliminated if the priority is minimal LoC, but then you'll have a notification badge hanging around.

@utkarshdalal utkarshdalal merged commit 6879259 into utkarshdalal:master Apr 13, 2026
2 checks passed
@utkarshdalal
Copy link
Copy Markdown
Owner

i've merged but we'll need to kill AmazonService too, no?

linkq pushed a commit to linkq/GameNative that referenced this pull request Apr 13, 2026
…shdalal#1136)

* fix: clear stale container state on task swipe and app restart

when the user swipes the app from the task switcher while a container
is running, keepAlive stays true but xEnvironment is gone. on next
launch the app is stuck thinking a container is running.

extract shutdownEnvironment() from XServerScreen.exit() so the same
full teardown runs in both the normal exit path and the crash recovery
path. each step is wrapped in runCatching so one failure doesn't
prevent the rest.

- onCreate: if keepAlive is set but xEnvironment is null, run
  shutdownEnvironment() to clear stale state
- onDestroy: emit ActivityDestroyed before super (so exit() listeners
  still fire), then force shutdownEnvironment() if keepAlive persists
- exit(): delegates teardown to shutdownEnvironment(), keeps only
  winHandler.stop() and trash cleanup (container-specific)

* fix: stop all foreground services on task swipe when idle

Steam/GOG/Epic services had no onTaskRemoved — foreground notification
persisted after swipe because nothing told the service to stop itself.
@jeremybernstein
Copy link
Copy Markdown
Contributor Author

i've merged but we'll need to kill AmazonService too, no?

Amazon already had onTaskRemoved() in place, nothing else to do!

@jeremybernstein jeremybernstein deleted the jb/fix-task-swipe-container-stuck branch April 13, 2026 15:16
@coderabbitai coderabbitai bot mentioned this pull request Apr 14, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App stuck on container screen after task swipe during game

3 participants