Skip to content

fix: show actionable dialog when Steam session is taken by another device#1306

Merged
utkarshdalal merged 8 commits intoutkarshdalal:masterfrom
ben-pearson:feature/session-conflict
Apr 28, 2026
Merged

fix: show actionable dialog when Steam session is taken by another device#1306
utkarshdalal merged 8 commits intoutkarshdalal:masterfrom
ben-pearson:feature/session-conflict

Conversation

@ben-pearson
Copy link
Copy Markdown
Contributor

@ben-pearson ben-pearson commented Apr 27, 2026

Problem

When a PC starts a Steam game while a game is already running in Wine on the device, Steam sends EResult.LoggedInElsewhere inside LoggedOffCallback. The previous code treated this the same as any other logoff — it emitted ForceCloseApp and silently killed the game with no feedback to the user.

trim.EC957912-3A84-489C-87BA-EA4674055363.MOV

Root cause confirmed via logcat: PlayingSessionStateCallback was assumed to fire in this scenario, but it never does. LoggedInElsewhere inside LoggedOffCallback is the only signal Steam sends.

Changes

trim.61380BA6-1726-4AF5-9224-E388A88977F2.MOV

SteamEvent.kt — adds PlayingBlocked event to signal the conflict to the UI layer.

SteamService.kt

  • onLoggedOff: when LoggedInElsewhere arrives and a game is running (xEnvironment != null), show the conflict dialog instead of force-closing. When no game is running, the existing ForceCloseApp behaviour is preserved.
  • _isHandlingConflict (AtomicBoolean): prevents the dialog being shown multiple times if LoggedInElsewhere fires repeatedly during reconnect attempts.
  • clearPlayingConflict(): resets conflict state on game exit.
  • reconnect() / onDisconnected(): suppress Disconnected/RemotelyDisconnected events while a conflict is being handled, so the reconnect cycle doesn't flash a "Disconnected from Steam" banner behind the dialog.
  • onPlayingSessionState: safety net — also emits PlayingBlocked if PlayingSessionStateCallback fires with isPlayingBlocked=true while in-game.

PluviaApp.kt — calls SteamService.clearPlayingConflict() in shutdownEnvironment() to reset state when the game exits.

XServerScreen.kt — registers a listener for SteamEvent.PlayingBlocked and shows an AlertDialog with:

  • Play Anyway — calls kickPlayingSession(onlyGame=true) to stop the PC's game session
  • Cancel — exits the local game cleanly

onDismissRequest is intentionally empty — the user must make an explicit choice.

strings.xml — adds main_app_running_unknown_game ("another device"), used as the fallback game name in the dialog message when the conflicting game title cannot be determined.

Note

Behaviour varies slightly depending on the conflicting device (PC, Steam Deck, Mac). This has been tested across all three and is improved in each case, but given the variance in how Steam signals session conflicts across platforms, further edge cases may exist.

Test plan

  • Launch a game on PC, then launch a game on device — pre-launch "App Running" dialog appears (existing behaviour, unaffected)
  • Launch a game on device, then start a game on PC — in-game "App Running" dialog appears without a "Disconnected from Steam" banner
  • Press Cancel — game exits cleanly
  • Press Play Anyway — PC game session is kicked, device continues playing
  • Normal game exit with no conflict — no regression to shutdown flow

Summary by cubic

Show an actionable “App Running” dialog when Steam logs the user off because another device started a game, instead of silently closing the local game. The dialog persists across Activity recreation and hides the disconnect banner; users can kick the remote session or exit.

  • Bug Fixes
    • Handle EResult.LoggedInElsewhere in SteamService.onLoggedOff: if a game is running, emit PlayingBlocked (not ForceCloseApp); otherwise keep existing behavior.
    • Show an AlertDialog in XServerScreen on PlayingBlocked with “Play anyway” (call SteamService.clearPlayingConflict() then SteamService.kickPlayingSession(onlyGame=true)) and “Cancel” (clean exit); persist with rememberSaveable and safe-call xServerView.
    • Add _isHandlingConflict to prevent duplicate dialogs; suppress Disconnected/RemotelyDisconnected while handling; reset via SteamService.clearPlayingConflict() on shutdown and before kicking.
    • Also emit PlayingBlocked when PlayingSessionStateCallback.isPlayingBlocked is true; add main_app_running_unknown_game fallback and translations across locales.

Written for commit b9083cf. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • New Features

    • Detects when a game session is blocked by another device and notifies the app.
    • Shows a conflict-resolution dialog to stop the remote session or exit the current session.
    • Adds a fallback label when the remote game name is unknown.
  • Bug Fixes

    • Improved cleanup/reset of Steam-related session/conflict state during app shutdown.

…vice

When a PC starts a Steam game while a game is already running in Wine on
the device, Steam sends LoggedInElsewhere instead of PlayingSessionState.
Handle this in onLoggedOff: if xEnvironment is running, set isPlayingBlocked
and emit PlayingBlocked instead of closing. XServerScreen catches the event
and shows an "App Running" dialog with Play Anyway / Cancel options.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Steam "playing conflict" handling: new SteamEvent.PlayingBlocked; SteamService tracks and suppresses disconnect events during conflict, exposes clearPlayingConflict(); XServerScreen displays a dialog to kick or exit when playing is blocked; PluviaApp.shutdownEnvironment clears the conflict state.

Changes

Cohort / File(s) Summary
Events
app/src/main/java/app/gamenative/events/SteamEvent.kt
Adds data object PlayingBlocked : SteamEvent<Unit> to represent a playing-blocked event.
Service
app/src/main/java/app/gamenative/service/SteamService.kt
Introduces conflict-tracking flags (_isHandlingConflict, _isPlayingBlocked), suppresses some disconnect emissions while handling conflicts, emits PlayingBlocked on relevant Steam states, and adds clearPlayingConflict() companion method.
App lifecycle
app/src/main/java/app/gamenative/PluviaApp.kt
Calls SteamService.clearPlayingConflict() during shutdownEnvironment() to reset Steam playing-conflict state.
UI & Resources
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, app/src/main/res/values/strings.xml
Adds UI handling for PlayingBlocked: shows an AlertDialog with options to kick the remote session (kickPlayingSession(onlyGame = true)) or dismiss/exit; adds fallback string main_app_running_unknown_game.

Sequence Diagram(s)

sequenceDiagram
    participant SteamService as SteamService
    participant EventBus as Event System
    participant XServerUI as XServerScreen UI
    participant User as User

    SteamService->>SteamService: Detect LoggedInElsewhere or playing-blocked state
    SteamService->>SteamService: set _isHandlingConflict = true\nset _isPlayingBlocked = true
    SteamService->>EventBus: Emit SteamEvent.PlayingBlocked
    EventBus->>XServerUI: Deliver PlayingBlocked
    XServerUI->>XServerUI: Show AlertDialog (Kick / Cancel)
    User->>XServerUI: Tap "Kick"
    XServerUI->>SteamService: clearPlayingConflict()
    XServerUI->>SteamService: kickPlayingSession(onlyGame = true)
    alt User taps "Cancel"
        User->>XServerUI: Tap "Cancel"
        XServerUI->>XServerUI: Dismiss dialog and exit flow
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐰 I sniff a Steam tug, a conflict near,
A tiny dialog hops into my ear.
"Kick or leave?" I wiggle my nose,
I press the button — out it goes!
The meadow's quiet; I munch with cheer.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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 title accurately summarizes the main change: showing an actionable dialog when Steam session is taken by another device, directly reflecting the core fix in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering the problem statement, detailed change explanations across multiple files, testing methodology, and user impact.

✏️ 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

@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: 4

🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/PluviaApp.kt (1)

236-238: Consider wrapping clearPlayingConflict() in runCatching for consistency.

The earlier comment in this method explicitly states "per-step catch so one failing teardown doesn't prevent the rest from running", and every teardown call above is wrapped accordingly. While clearPlayingConflict() is presumably a trivial flag reset (and unlikely to throw), wrapping it preserves the defensive pattern and protects clearActiveSuspendState() from any future implementation changes.

♻️ Suggested change
         SteamService.keepAlive = false
-        SteamService.clearPlayingConflict()
+        runCatching { SteamService.clearPlayingConflict() }
+            .onFailure { Timber.e(it, "shutdownEnvironment: clearPlayingConflict") }
         clearActiveSuspendState()
🤖 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 236 - 238, Wrap
the call to SteamService.clearPlayingConflict() in a runCatching block to follow
the existing per-step teardown pattern (so a failure in clearPlayingConflict()
won't prevent clearActiveSuspendState() from running); locate the section where
SteamService.keepAlive is set to false and replace the direct call to
clearPlayingConflict() with a runCatching { SteamService.clearPlayingConflict()
} invocation that logs or ignores the throwable consistently with the other
teardown steps.
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

2260-2261: Dialog always renders "unknown game" — actual conflicting title never plumbed through.

main_app_running_message is invoked with main_app_running_unknown_game as the format argument unconditionally, so users always see the fallback wording regardless of whether Steam knows what's running on the other device. The PR description frames main_app_running_unknown_game as a fallback "when the conflicting game name is unavailable," but SteamEvent.PlayingBlocked is a parameterless object, so there is no path to deliver the real name from onLoggedOff / onPlayingSessionState to this composable.

Consider extending SteamEvent.PlayingBlocked to carry the conflicting app id / display name (or expose it via a StateFlow on SteamService) and only fall back to main_app_running_unknown_game when it is genuinely unknown. Can be deferred to a follow-up if not in scope here.

🤖 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 2260 - 2261, The dialog always shows the fallback string because
SteamEvent.PlayingBlocked is parameterless and main_app_running_message is
always called with main_app_running_unknown_game; fix by plumbing the actual
conflicting game info into the UI: either extend SteamEvent.PlayingBlocked to
carry the conflicting appId/displayName and propagate that from
onPlayingSessionState/onLoggedOff into the state consumed by the XServerScreen
composable, or expose a new StateFlow/String on SteamService with the current
conflicting game's name and use that in the composable to call
main_app_running_message with the real name (only using
main_app_running_unknown_game when the name is null/empty). Ensure references:
SteamEvent.PlayingBlocked, XServerScreen.kt composable where
main_app_running_message is used, onPlayingSessionState/onLoggedOff handlers,
and SteamService StateFlow.
🤖 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/SteamService.kt`:
- Around line 3559-3562: The PlayingBlocked emission path emits
SteamEvent.PlayingBlocked directly (in the PlayingSessionStateCallback handling)
without flipping the conflict sentinel _isHandlingConflict, so it can bypass the
reconnect()/onDisconnected() disconnect-suppression and re-emit repeatedly;
update the block that checks callback.isPlayingBlocked to first atomically set
the conflict flag (use _isHandlingConflict.compareAndSet(false, true) or
equivalent getAndSet logic) and only call
PluviaApp.events.emit(SteamEvent.PlayingBlocked) when that compareAndSet
succeeds, ensuring this path enters the same conflict-handling state machine and
emits at most once.
- Around line 421-424: The _isHandlingConflict flag remains true after a user
chooses "Play Anyway", breaking reconnect and subsequent conflict handling;
update kickPlayingSession's success branch to clear that flag (e.g., call
instance?._isHandlingConflict?.set(false) or getAndSet(false)) so that after
resolving a LoggedInElsewhere you also reset _isHandlingConflict (the flag is
set in onLoggedOff and currently only cleared in clearPlayingConflict), ensuring
reconnect()/onDisconnected() and future LoggedInElsewhere events behave
correctly.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 2262-2271: The confirm button currently fires-and-forgets
SteamService.kickPlayingSession inside scope.launch and clears
showPlayingBlockedDialog immediately, which swallows failures and leaves
_isHandlingConflict true; change the handler in confirmButton to call
kickPlayingSession and await its Boolean result (catching exceptions/timeout),
only set showPlayingBlockedDialog = false and clear _isHandlingConflict when the
call succeeds, and on failure restore/show the dialog or display an error
message (and set _isHandlingConflict = false so a subsequent PlayingBlocked
event can re-show the dialog); update the lambda around
SteamService.kickPlayingSession, showPlayingBlockedDialog, and
_isHandlingConflict accordingly and add error handling/timeout handling for
exceptions.

In `@app/src/main/res/values/strings.xml`:
- Line 1068: The fallback string main_app_running_unknown_game is grammatically
awkward when substituted into main_app_running_message; replace its value
"another device" with a clearer fallback such as "another game" or "an unknown
game" so the message reads naturally (e.g., "...already playing another game");
update the string resource main_app_running_unknown_game accordingly.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/PluviaApp.kt`:
- Around line 236-238: Wrap the call to SteamService.clearPlayingConflict() in a
runCatching block to follow the existing per-step teardown pattern (so a failure
in clearPlayingConflict() won't prevent clearActiveSuspendState() from running);
locate the section where SteamService.keepAlive is set to false and replace the
direct call to clearPlayingConflict() with a runCatching {
SteamService.clearPlayingConflict() } invocation that logs or ignores the
throwable consistently with the other teardown steps.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 2260-2261: The dialog always shows the fallback string because
SteamEvent.PlayingBlocked is parameterless and main_app_running_message is
always called with main_app_running_unknown_game; fix by plumbing the actual
conflicting game info into the UI: either extend SteamEvent.PlayingBlocked to
carry the conflicting appId/displayName and propagate that from
onPlayingSessionState/onLoggedOff into the state consumed by the XServerScreen
composable, or expose a new StateFlow/String on SteamService with the current
conflicting game's name and use that in the composable to call
main_app_running_message with the real name (only using
main_app_running_unknown_game when the name is null/empty). Ensure references:
SteamEvent.PlayingBlocked, XServerScreen.kt composable where
main_app_running_message is used, onPlayingSessionState/onLoggedOff handlers,
and SteamService StateFlow.
🪄 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: c93ce219-621c-41cc-aa40-4a9f81c56532

📥 Commits

Reviewing files that changed from the base of the PR and between 2a236ef and 6a933f2.

📒 Files selected for processing (5)
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/events/SteamEvent.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/res/values/strings.xml

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Comment thread app/src/main/res/values/strings.xml Outdated
@ben-pearson ben-pearson force-pushed the feature/session-conflict branch from 6a933f2 to 644f73b Compare April 27, 2026 15:59
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.

5 issues found across 5 files

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/res/values/strings.xml">

<violation number="1" location="app/src/main/res/values/strings.xml:1068">
P2: Fallback value for unknown game title uses a device phrase, causing incorrect conflict dialog text ("already playing another device").</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:421">
P1: `_isHandlingConflict` is never reset after a successful kick. When the user taps "Play Anyway" and `kickPlayingSession` succeeds, the flag stays `true` for the rest of the game session. This silently suppresses all subsequent `Disconnected`/`RemotelyDisconnected` events (the user won't see disconnect banners for real outages) and prevents the conflict dialog from reappearing if another `LoggedInElsewhere` arrives. Reset `_isHandlingConflict` to `false` inside `kickPlayingSession` on the success branch.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/service/SteamService.kt:3540">
P1: Conflict-handling flag is set but not cleared on unblock, causing later real disconnect events to stay suppressed for the session.</violation>

<violation number="3" location="app/src/main/java/app/gamenative/service/SteamService.kt:3559">
P2: This safety-net path emits `PlayingBlocked` without setting `_isHandlingConflict`, unlike the `onLoggedOff` path. This means disconnect suppression in `reconnect()`/`onDisconnected()` won't activate, so the dialog can appear behind a disconnect banner. Additionally, repeated `PlayingSessionStateCallback` calls with `isPlayingBlocked=true` will re-emit the event with no deduplication. Gate on `_isHandlingConflict.compareAndSet(false, true)` to match the invariants established in `onLoggedOff`.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:2264">
P2: "Play Anyway" dismisses the dialog before awaiting the kick result. `kickPlayingSession` returns a `Boolean` indicating success, but it's launched fire-and-forget and the result is discarded. If the kick times out or fails, the dialog is already gone and the user is stuck in a blocked session with no feedback or way to retry. Await the result and re-show the dialog (or display an error) on failure.</violation>
</file>

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

Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt
Comment thread app/src/main/res/values/strings.xml Outdated
Comment thread app/src/main/java/app/gamenative/service/SteamService.kt Outdated
Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.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.

♻️ Duplicate comments (2)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

2263-2267: ⚠️ Potential issue | 🟠 Major

Handle “Play Anyway” failures before dismissing the dialog.

At Line 2264, the dialog is closed before the result of kickPlayingSession is known. If the kick fails/times out, the user can be left blocked with no in-flow recovery prompt.

♻️ Suggested fix
             confirmButton = {
                 TextButton(onClick = {
-                    showPlayingBlockedDialog = false
                     scope.launch {
-                        SteamService.kickPlayingSession(onlyGame = true)
+                        val kicked = SteamService.kickPlayingSession(onlyGame = true)
+                        if (kicked) {
+                            showPlayingBlockedDialog = false
+                        }
                     }
                 }) {
                     Text(text = stringResource(R.string.main_play_anyway))
                 }
             },
🤖 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 2263 - 2267, The dialog is being dismissed before the result of
SteamService.kickPlayingSession is known; change the onClick handler on the
button (where showPlayingBlockedDialog is toggled and scope.launch calls
SteamService.kickPlayingSession) so it launches the coroutine, awaits the
kickPlayingSession result, and only set showPlayingBlockedDialog = false on
success; on failure/timeouts catch the error and keep the dialog open while
showing an inline error state or retry option (or a Snackbar) so the user can
recover; use the existing scope.launch, wrap the call to
SteamService.kickPlayingSession in try/catch (and optionally with a timeout) and
update UI state based on the outcome instead of immediately closing the dialog.
app/src/main/java/app/gamenative/service/SteamService.kt (1)

3556-3562: ⚠️ Potential issue | 🟠 Major

Drive _isHandlingConflict from PlayingSessionStateCallback, not just onLoggedOff.

This callback currently emits PlayingBlocked on every true, but it never enters the conflict state machine when this is the first signal, and it never exits it when Steam later reports false. That leaves two bugs: duplicate dialog emissions on repeated blocked callbacks, and _isHandlingConflict staying true after the conflict is resolved, which suppresses later real disconnect banners and blocks future conflict dialogs for the rest of the session. MainViewModel only learns about connection loss from those suppressed disconnect events.

Suggested fix
 private fun onPlayingSessionState(callback: PlayingSessionStateCallback) {
     Timber.d("onPlayingSessionState called with isPlayingBlocked = " + callback.isPlayingBlocked)
     _isPlayingBlocked.value = callback.isPlayingBlocked
-    if (callback.isPlayingBlocked) {
-        val event = SteamEvent.PlayingBlocked
-        PluviaApp.events.emit(event)
-    }
+    if (callback.isPlayingBlocked) {
+        if (_isHandlingConflict.compareAndSet(false, true)) {
+            PluviaApp.events.emit(SteamEvent.PlayingBlocked)
+        }
+    } else {
+        _isHandlingConflict.set(false)
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/service/SteamService.kt` around lines 3556 -
3562, The onPlayingSessionState handler must drive the conflict state from
PlayingSessionStateCallback instead of only toggling in onLoggedOff: update the
_isHandlingConflict state to match callback.isPlayingBlocked (use
PlayingSessionStateCallback.isPlayingBlocked) and only emit
PluviaApp.events.emit(SteamEvent.PlayingBlocked) when transitioning from
not-blocked to blocked to avoid duplicate dialogs; when
callback.isPlayingBlocked becomes false, clear _isHandlingConflict so the
conflict machine exits and future disconnect events (e.g., onLoggedOff) are not
suppressed. Ensure you reference onPlayingSessionState,
PlayingSessionStateCallback, _isPlayingBlocked, _isHandlingConflict, and
PluviaApp.events.emit(SteamEvent.PlayingBlocked) when making the change.
🤖 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/service/SteamService.kt`:
- Around line 3556-3562: The onPlayingSessionState handler must drive the
conflict state from PlayingSessionStateCallback instead of only toggling in
onLoggedOff: update the _isHandlingConflict state to match
callback.isPlayingBlocked (use PlayingSessionStateCallback.isPlayingBlocked) and
only emit PluviaApp.events.emit(SteamEvent.PlayingBlocked) when transitioning
from not-blocked to blocked to avoid duplicate dialogs; when
callback.isPlayingBlocked becomes false, clear _isHandlingConflict so the
conflict machine exits and future disconnect events (e.g., onLoggedOff) are not
suppressed. Ensure you reference onPlayingSessionState,
PlayingSessionStateCallback, _isPlayingBlocked, _isHandlingConflict, and
PluviaApp.events.emit(SteamEvent.PlayingBlocked) when making the change.

In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 2263-2267: The dialog is being dismissed before the result of
SteamService.kickPlayingSession is known; change the onClick handler on the
button (where showPlayingBlockedDialog is toggled and scope.launch calls
SteamService.kickPlayingSession) so it launches the coroutine, awaits the
kickPlayingSession result, and only set showPlayingBlockedDialog = false on
success; on failure/timeouts catch the error and keep the dialog open while
showing an inline error state or retry option (or a Snackbar) so the user can
recover; use the existing scope.launch, wrap the call to
SteamService.kickPlayingSession in try/catch (and optionally with a timeout) and
update UI state based on the outcome instead of immediately closing the dialog.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bf693dc-c69c-4180-9f40-874393eaf3ee

📥 Commits

Reviewing files that changed from the base of the PR and between 6a933f2 and 644f73b.

📒 Files selected for processing (5)
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/events/SteamEvent.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/app/gamenative/events/SteamEvent.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

♻️ Duplicate comments (1)
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt (1)

2263-2268: ⚠️ Potential issue | 🟠 Major

Only clear the conflict after kickPlayingSession() succeeds.

The dialog is dismissed before the kick completes, and the Boolean result is ignored. On timeout/exception, the user can stay blocked while the prompt disappears and the conflict state gets cleared anyway.

♻️ Proposed fix
             confirmButton = {
                 TextButton(onClick = {
-                    showPlayingBlockedDialog = false
                     scope.launch {
-                        SteamService.kickPlayingSession(onlyGame = true)
-                        SteamService.clearPlayingConflict()
+                        val kicked = SteamService.kickPlayingSession(onlyGame = true)
+                        if (kicked) {
+                            showPlayingBlockedDialog = false
+                            SteamService.clearPlayingConflict()
+                        }
                     }
                 }) {
                     Text(text = stringResource(R.string.main_play_anyway))
                 }
             },
🤖 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 2263 - 2268, The dialog is being dismissed before kickPlayingSession
completes and its Boolean result is ignored; change the onClick handler so it
waits for scope.launch to complete and only clears the conflict if
SteamService.kickPlayingSession(onlyGame = true) returns true, leaving
showPlayingBlockedDialog true on failure or exception; catch exceptions/timeouts
around the call to SteamService.kickPlayingSession in the launched coroutine and
only call SteamService.clearPlayingConflict() on successful completion, and
update showPlayingBlockedDialog based on the success result.
🤖 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`:
- Line 416: Replace the transient Compose state for the Steam conflict dialog so
it survives Activity recreation: change the declaration of
showPlayingBlockedDialog (in XServerScreen.kt) from using remember {
mutableStateOf(false) } to a saveable state (e.g., rememberSaveable {
mutableStateOf(false) }) so the dialog remains visible across configuration
changes/process recreation while SteamService emits PlayingBlocked; also ensure
any code that handles clearPlayingConflict() still sets showPlayingBlockedDialog
to false when the conflict is cleared.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt`:
- Around line 2263-2268: The dialog is being dismissed before kickPlayingSession
completes and its Boolean result is ignored; change the onClick handler so it
waits for scope.launch to complete and only clears the conflict if
SteamService.kickPlayingSession(onlyGame = true) returns true, leaving
showPlayingBlockedDialog true on failure or exception; catch exceptions/timeouts
around the call to SteamService.kickPlayingSession in the launched coroutine and
only call SteamService.clearPlayingConflict() on successful completion, and
update showPlayingBlockedDialog based on the success result.
🪄 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: cb3297ac-2a87-4901-b934-3d01641185e8

📥 Commits

Reviewing files that changed from the base of the PR and between 644f73b and db6915c.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/res/values/strings.xml
✅ Files skipped from review due to trivial changes (1)
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/main/java/app/gamenative/service/SteamService.kt

Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt Outdated
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 2274-2277: The Cancel handler currently force-unwraps xServerView
(xServerView!!) which can be null after Activity recreation; change the handler
to avoid the force-unwrap by safely resolving xServerView and its winHandler
before calling exit: capture a safe local reference (e.g. val server =
xServerView?.getxServer()?.winHandler) or guard with if (xServerView != null) {
exit(...) } and if null, perform a safe no-op or fallback (e.g. just
navigateBack/onExit) so exit(...) is only invoked when winHandler is non-null;
update the TextButton onClick to use this null-safe logic and still set
showPlayingBlockedDialog = false.
🪄 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: e4db0a64-687f-431b-ba69-594ddd8e46c8

📥 Commits

Reviewing files that changed from the base of the PR and between db6915c and 1d15369.

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

Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.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.

2 issues found across 1 file (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/ui/screen/xserver/XServerScreen.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:416">
P1: Changing `showPlayingBlockedDialog` to `rememberSaveable` means this dialog state now survives Activity recreation (e.g., screen rotation), but `xServerView` is stored with plain `remember` and will be null after recreation. If the user taps Cancel before `xServerView` is rebound, the `xServerView!!` non-null assertion in the dismiss button handler will throw an NPE. Use safe-call (`xServerView?.getxServer()?.winHandler`) instead, or revert to `remember` if the dialog doesn't need to survive configuration changes.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt:2265">
P2: Conflict-handling state is cleared too early, re-enabling suppressed Steam conflict/disconnect events before session kick finishes.</violation>
</file>

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

Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Comment thread app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Comment on lines +2257 to +2282
if (showPlayingBlockedDialog) {
androidx.compose.material3.AlertDialog(
onDismissRequest = {},
title = { Text(text = stringResource(R.string.main_app_running_title)) },
text = { Text(text = stringResource(R.string.main_app_running_message, context.getString(R.string.main_app_running_unknown_game))) },
confirmButton = {
TextButton(onClick = {
showPlayingBlockedDialog = false
SteamService.clearPlayingConflict()
scope.launch {
SteamService.kickPlayingSession(onlyGame = true)
}
}) {
Text(text = stringResource(R.string.main_play_anyway))
}
},
dismissButton = {
TextButton(onClick = {
showPlayingBlockedDialog = false
exit(xServerView?.getxServer()?.winHandler, frameRating, currentAppInfo, container, appId, onExit, navigateBack)
}) {
Text(text = stringResource(R.string.cancel))
}
},
)
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Curious why you decided to put the dialog here instead of PluviaMain with the others? Is it required to be here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the Cancel handler needs to call exit() which handles Wine process teardown via winHandler - that context only exists in XServerScreen, is there a better way to do this? I was trying to change as little as possible (reducing risk)

<string name="main_app_running_message">You are logged in on another device already playing %s. \nYou can still play this game, but that will disconnect the other session from Steam.</string>
<string name="main_app_running_other_device">You are logged in on another device (%1$s) already playing %2$s (%3$s), and that save is not yet in the cloud. \nYou can still play this game, but that will disconnect the other session from Steam and may create a save conflict when that session progress is synced</string>
<string name="main_play_anyway">Play anyway</string>
<string name="main_app_running_unknown_game">a game</string>
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

translations pls

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added (AI generated translations) hope they are correct :-D 🏴󠁧󠁢󠁥󠁮󠁧󠁿

@utkarshdalal
Copy link
Copy Markdown
Owner

couple of comments but otherwise looks good

@utkarshdalal utkarshdalal merged commit f1bd38e into utkarshdalal:master Apr 28, 2026
3 checks passed
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.

2 participants