Skip to content

Steam overlay invites - #1783

Merged
utkarshdalal merged 3 commits into
masterfrom
steam-overlay-invites
Jul 30, 2026
Merged

Steam overlay invites#1783
utkarshdalal merged 3 commits into
masterfrom
steam-overlay-invites

Conversation

@utkarshdalal

@utkarshdalal utkarshdalal commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Description

Added Steam overlay with bionic steam for inviting players to games

Recording

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

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.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Adds Steam overlay invites for bionic Steam so you can invite friends, join them, or accept invites without leaving the game. The Quick Menu adds an Invite tab and opens it automatically when a game asks for the Steam invite dialog.

  • New Features

    • New Quick Menu “Invite” tab for bionic Steam: browse friends, send invites, or join friends already in this game.
    • Auto-opens the Invite tab on game request; the game isn’t paused in this case.
    • Incoming Steam invites show as an in-game banner with Join/Dismiss; accepts both lobby and rich‑presence invites and hands off the join to the running game.
    • Added SteamOverlayClient for the host libsteambootstrap socket; GameInviteHandler/GameInviteCallback plus GameInviteOverlay/GameInviteNotificationManager to surface and act on invites.
    • Export SteamOS=1 for bionic launches so games enable overlay-gated invite UI; expanded Quick Menu tab range; updated libsteambootstrap.so; added localized strings.
  • Migration

    • No setup required. Feature activates only for bionic Steam launches.
    • Behavior note: when the game opens the invite dialog, the app keeps the game running while the menu is up.

Written for commit e4f4a3d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a Steam Invite tab to the quick menu for viewing friends, sending invitations, and joining available game sessions.
    • Added in-game invite notifications with Join and Dismiss actions.
    • Added automatic handling of game-triggered invite requests and overlay prompts.
    • Expanded invite tab support and translated interface text across multiple languages.
  • Bug Fixes

    • Improved persistence for quick menu tab selections.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds Steam invite communication and callback handling, friend and invite state, QuickMenu and overlay invite flows, SteamOS bootstrap forwarding, expanded tab persistence, and localized invite UI strings.

Changes

Steam invite integration

Layer / File(s) Summary
Invite transport and state
app/src/main/java/app/gamenative/service/SteamOverlayClient.kt, app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt, app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java
Adds socket commands for Steam availability, friends, invites, joins, and overlay requests; tracks invite state; forwards SteamOS=1 to the native bootstrap.
Steam callback event pipeline
app/src/main/java/app/gamenative/service/callback/*, app/src/main/java/app/gamenative/service/handler/*, app/src/main/java/app/gamenative/service/SteamService.kt, app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt
Parses and dispatches game-invite packets, subscribes SteamService to callbacks, and stores the latest pending invite.
Invite UI and menu integration
app/src/main/java/app/gamenative/ui/component/*, app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt, app/src/main/java/app/gamenative/ui/PluviaMain.kt, app/src/main/java/app/gamenative/PrefManager.kt
Adds the Steam Invite QuickMenu tab and overlay, handles request-driven opening and joining, updates menu lifecycle behavior, expands saved-tab validation, and adds localized resources across supported languages.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: joshuatam, nightwalker743, phobos665

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.63% 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 is concise and accurately reflects the main change: Steam overlay invites.
Description check ✅ Passed The description follows the template and covers the change, type, and checklist; only the recording is still a placeholder.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch steam-overlay-invites

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.

@utkarshdalal
utkarshdalal force-pushed the steam-overlay-invites branch from d1fea5a to e4f4a3d Compare July 30, 2026 02:40

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt (2)

82-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Overlay text is hardcoded, unlike the rest of the invite feature.

The PR adds localized steam_invite_* strings used elsewhere (e.g. QuickMenu.kt's invite tab), but this overlay's prompt/button text ("invited you to play", "Join", "Joining…", "Dismiss") is hardcoded English, so it won't be localized for non-English users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt` around
lines 82 - 119, Replace the hardcoded prompt and button labels in
GameInviteOverlay with the localized steam_invite_* string resources already
used by the invite feature, including separate resources for Join, Joining, and
Dismiss. Resolve them through the existing Compose string-resource pattern while
preserving the current busy-state behavior and invite actions.

44-68: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exit animation breaks because content reads live (nulled) state.

invite is set to null by clear() before/while the exit transition plays, so current = invite ?: return@AnimatedVisibility bails out immediately — the slide/fade-out never actually animates anything, it just disappears.

🎬 Proposed fix: remember the last non-null invite for the exit frame
     val invite by GameInviteNotificationManager.pending.collectAsState()

+    var lastInvite by remember { mutableStateOf(invite) }
+    LaunchedEffect(invite) {
+        if (invite != null) lastInvite = invite
+    }
+
     var inviterName by remember { mutableStateOf<String?>(null) }
     ) {
-        val current = invite ?: return@AnimatedVisibility
+        val current = lastInvite ?: return@AnimatedVisibility
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt` around
lines 44 - 68, Preserve the last non-null invite for rendering during the exit
transition instead of reading the live nullable invite directly. Update the
state and AnimatedVisibility content around
GameInviteNotificationManager.pending and the current invite so clear() can set
invite to null while the existing overlay content remains available until the
exit animation completes, then discard the remembered invite.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/service/SteamOverlayClient.kt`:
- Around line 148-166: Update the response-reading loop in SteamOverlayClient so
EOF before the "." terminator is treated as an unavailable host rather than a
successful partial result. Preserve normal "."-terminated LIST responses and
single-record behavior, but return null when readLine() reaches EOF while
processing a multi-record LIST.
- Around line 139-144: Update the LocalSocket connection flow in the overlay
client to call getPeerCredentials() immediately after connect and validate the
peer UID against the expected bootstrap process UID. Reject and close the socket
before any command write or response handling when the UID does not match, while
preserving the existing behavior for authorized peers.

In `@app/src/main/res/values-it/strings.xml`:
- Line 2162: Replace the singular steam_invite_header string resource with a
plurals resource defining “amico” for one and “amici” for other, and update its
retrieval to use the quantity-aware resource API with the friend count.

In `@app/src/main/res/values-ro/strings.xml`:
- Line 2174: Replace the singular steam_invite_header string resource with a
plurals resource containing correct one and other forms for the invite count,
and update the code retrieving steam_invite_header to use getQuantityString with
the count so singular and plural Romanian grammar render correctly.

In `@app/src/main/res/values-ru/strings.xml`:
- Line 2107: Update the Russian translation for the steam_invite_joined string
to explicitly confirm that the user joined the game, using wording equivalent to
“Вы присоединились к игре” and matching the source meaning.

In `@HANDOFF_EXTERNAL_PERF.md`:
- Around line 30-35: The device-state notes in HANDOFF_EXTERNAL_PERF.md are
contradictory between the current-state block and the e1141a36 deployment note.
Consolidate them into one canonical device-state section, updating the current
live file, hash/size, .so.orig backup status, and deployed hash to match the
actual device; remove or reconcile the conflicting duplicate state description.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt`:
- Around line 82-119: Replace the hardcoded prompt and button labels in
GameInviteOverlay with the localized steam_invite_* string resources already
used by the invite feature, including separate resources for Join, Joining, and
Dismiss. Resolve them through the existing Compose string-resource pattern while
preserving the current busy-state behavior and invite actions.
- Around line 44-68: Preserve the last non-null invite for rendering during the
exit transition instead of reading the live nullable invite directly. Update the
state and AnimatedVisibility content around
GameInviteNotificationManager.pending and the current invite so clear() can set
invite to null while the existing overlay content remains available until the
exit animation completes, then discard the remembered invite.
🪄 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 Plus

Run ID: cb3865cd-3386-4233-bda3-9866bf473310

📥 Commits

Reviewing files that changed from the base of the PR and between d61318b and d1fea5a.

⛔ Files ignored due to path filters (1)
  • app/src/main/jniLibs/arm64-v8a/libsteambootstrap.so is excluded by !**/*.so
📒 Files selected for processing (28)
  • HANDOFF_EXTERNAL_PERF.md
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/service/SteamOverlayClient.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/callback/GameInviteCallback.kt
  • app/src/main/java/app/gamenative/service/handler/GameInviteHandler.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
  • app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt
  • app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml

Comment on lines +139 to +144
LocalSocket().use { socket ->
socket.connect(LocalSocketAddress(SOCKET_NAME, LocalSocketAddress.Namespace.ABSTRACT))
socket.soTimeout = TIMEOUT_MS

socket.outputStream.write("$command\n".toByteArray())
socket.outputStream.flush()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Android LocalSocket abstract namespace socket security same UID access

💡 Result:

In Android, Unix domain sockets in the abstract namespace do not have filesystem-based access controls (DAC/MAC) applied to them [1][2][3]. Because these sockets exist independently of the filesystem, traditional Linux file permissions cannot be used to restrict access, making the abstract namespace inherently less secure than the filesystem namespace [1][2]. By default, an abstract socket is accessible by any process on the system [2]. The Android kernel and framework do not automatically enforce a UID (User ID) check when a process attempts to connect to an abstract socket [2]. If a server process intends to restrict access to its abstract socket, it must implement its own security logic to verify the identity of the connecting client [1][2][4]. Developers commonly secure abstract sockets by performing an explicit UID check after the connection is established [2][4]. This is typically achieved by: 1. Retrieving the peer credentials of the connected socket [5]. 2. Verifying that the UID of the client matches the expected, trusted UID (e.g., root, system, or a specific app UID) [2]. In Android native code, this can often be facilitated by utilities such as socket_peer_is_trusted(int fd), which checks if the peer is running as a trusted user like root or shell [6]. Without such manual implementation of access control, any application with the ability to connect to the socket—which is generally permitted by the default SELinux policy for abstract sockets—can interact with the service [2]. Consequently, relying solely on the Android operating system to restrict abstract socket access to the same UID is insufficient; explicit verification in the server code is required for secure inter-process communication [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate SteamOverlayClient.kt =="
fd -a 'SteamOverlayClient.kt$' . || true

echo "== file outline =="
FILE="$(fd 'SteamOverlayClient.kt$' . | head -n1)"
if [ -n "${FILE:-}" ]; then
  wc -l "$FILE"
  ast-grep outline "$FILE" || true
  echo "== relevant lines =="
  cat -n "$FILE" | sed -n '1,260p'
fi

echo "== local socket/listening references =="
rg -n "gamenative-steam-overlay|SocketServer|UnixSocket|Abstract|LocalSocket|getPeerCredentials|SOCKET_NAME|SteamOverlayClient" -S .

Repository: utkarshdalal/GameNative

Length of output: 23059


🌐 Web query:

Android LocalSocket getPeerCredentials current app same UID abstract socket

💡 Result:

In Android, the LocalSocket.getPeerCredentials method is used to retrieve the Process ID (PID), User ID (UID), and Group ID (GID) of the process on the other end of a connected Unix domain socket [1][2][3]. When you call this method, the underlying implementation in the Android framework invokes the native getsockopt system call with the SO_PEERCRED option [4][5]. Because this information is provided directly by the Linux kernel, it is considered authoritative and cannot be spoofed by non-privileged user-space processes [6]. Regarding your specific scenario: - Same UID: If you are connecting an abstract Unix domain socket [7] between two components within the same application (or different applications sharing the same UID), getPeerCredentials will return the UID associated with that application [1][4]. - Verification: You can programmatically verify if the peer is the same as the current process by comparing the UID returned by getPeerCredentials.getUid with the UID of your own process, which can be retrieved using android.os.Process.myUid. - Abstract Sockets: The behavior of getPeerCredentials does not change based on whether the socket is in the abstract namespace or the filesystem namespace; it consistently reports the credentials of the connected peer [6][7]. This mechanism is commonly used for security checks to ensure that a local service only accepts connections from authorized clients (e.g., verifying that the caller is the same UID or a trusted system UID) [8][6].

Citations:


🌐 Web query:

"LocalSocket" "getPeerCredentials" "abstract namespace" UID

💡 Result:

In Android, the LocalSocket API provides a mechanism to interact with Unix domain sockets, including those in the Linux abstract namespace [1][2]. You can retrieve the credentials of a connected peer using the getPeerCredentials method [3][4]. When this method is called on a connected LocalSocket, it returns a Credentials object containing information about the peer process, which typically includes the process's User ID (UID), Group ID (GID), and Process ID (PID) [5]. This information is derived from the underlying Linux kernel's SO_PEERCRED socket option, which is available for AF_UNIX stream sockets [6]. Key points regarding the abstract namespace and UID retrieval: - Abstract Namespace: Sockets in the abstract namespace are identified by a null byte at the beginning of their address string (sun_path = '\0') and exist independently of the filesystem [6][7]. Because they are not constrained by filesystem permissions, any process can theoretically connect to an abstract socket if the name is known [8]. - Security: Due to the lack of filesystem-based access control, retrieving and validating the peer's UID via getPeerCredentials is a critical step for implementing authentication in Android IPC when using abstract sockets [5]. Developers should treat the UID/GID as the primary basis for verifying the identity of the connecting process [5]. - Implementation: The getPeerCredentials method acts as a wrapper around native functionality that queries the kernel for the credentials associated with the socket's file descriptor [9]. The method is only valid on sockets that are already connected [3][2]. If you are implementing a server, you must call it after accepting a connection from a client to identify who is connecting to your service [5].

Citations:


Verify the peer UID before trusting overlay socket replies.

Abstract-namespace LocalSockets don’t carry filesystem-based access controls, and getPeerCredentials() only applies after connect()/accept(). Right after connecting, reject any peer whose UID is not the expected bootstrap process UID before reading/writing overlay commands or trusting friend list/OK responses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamOverlayClient.kt` around lines
139 - 144, Update the LocalSocket connection flow in the overlay client to call
getPeerCredentials() immediately after connect and validate the peer UID against
the expected bootstrap process UID. Reject and close the socket before any
command write or response handling when the UID does not match, while preserving
the existing behavior for authorized peers.

Comment on lines +148 to +166
while (true) {
val line = reader.readLine() ?: break
when {
line == "." -> return@withContext out
line.startsWith("ERR") -> {
Timber.w("SteamOverlayClient: '$command' -> $line")
return@withContext null
}
else -> {
out.add(line)
// Single-record replies have no terminator; don't block on more.
if (out.size == 1 && !command.startsWith("LIST")) {
return@withContext out
}
}
}
}
out
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Premature disconnect during LIST is silently treated as a complete, successful reply.

reader.readLine() ?: break falls through to return@withContext out. If the host closes the connection mid-stream (crash, restart) before sending the . terminator, whatever friend lines were collected so far are returned exactly like a normal successful reply — callers can't distinguish a truncated list from a complete one, and SteamInviteState.hostUnavailable won't be set since loaded isn't empty. The function's own doc comment says it "Returns null if the host is unreachable" — an EOF mid-LIST should hit that path too.

🐛 Proposed fix
                 val reader = BufferedReader(InputStreamReader(socket.inputStream))
                 val out = mutableListOf<String>()
                 while (true) {
-                    val line = reader.readLine() ?: break
+                    val line = reader.readLine()
+                        ?: return@withContext if (command.startsWith("LIST")) null else out
                     when {
                         line == "." -> return@withContext out
                         line.startsWith("ERR") -> {
                             Timber.w("SteamOverlayClient: '$command' -> $line")
                             return@withContext null
                         }
                         else -> {
                             out.add(line)
                             // Single-record replies have no terminator; don't block on more.
                             if (out.size == 1 && !command.startsWith("LIST")) {
                                 return@withContext out
                             }
                         }
                     }
                 }
-                out
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (true) {
val line = reader.readLine() ?: break
when {
line == "." -> return@withContext out
line.startsWith("ERR") -> {
Timber.w("SteamOverlayClient: '$command' -> $line")
return@withContext null
}
else -> {
out.add(line)
// Single-record replies have no terminator; don't block on more.
if (out.size == 1 && !command.startsWith("LIST")) {
return@withContext out
}
}
}
}
out
}
while (true) {
val line = reader.readLine()
?: return@withContext if (command.startsWith("LIST")) null else out
when {
line == "." -> return@withContext out
line.startsWith("ERR") -> {
Timber.w("SteamOverlayClient: '$command' -> $line")
return@withContext null
}
else -> {
out.add(line)
// Single-record replies have no terminator; don't block on more.
if (out.size == 1 && !command.startsWith("LIST")) {
return@withContext out
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamOverlayClient.kt` around lines
148 - 166, Update the response-reading loop in SteamOverlayClient so EOF before
the "." terminator is treated as an unavailable host rather than a successful
partial result. Preserve normal "."-terminated LIST responses and single-record
behavior, but return null when readLine() reaches EOF while processing a
multi-record LIST.

<string name="bfg_model_standard">Standard</string>
<string name="bfg_model_clear">Clear</string>
<string name="steam_invite_tab_title">Invita</string>
<string name="steam_invite_header">%1$d amici</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a plural resource for the friend count.

With one friend, this renders as 1 amici. Define steam_invite_header as a <plurals> resource with amico for one and amici for other, then retrieve it with the quantity-aware API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-it/strings.xml` at line 2162, Replace the singular
steam_invite_header string resource with a plurals resource defining “amico” for
one and “amici” for other, and update its retrieval to use the quantity-aware
resource API with the friend count.

<string name="bfg_model_standard">Standard</string>
<string name="bfg_model_clear">Clear</string>
<string name="steam_invite_tab_title">Invită</string>
<string name="steam_invite_header">%1$d prieteni</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a plural resource for the invite count.

%1$d prieteni renders incorrectly for 1 (1 prieteni). Define singular and plural forms with <plurals> and retrieve the value using getQuantityString.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-ro/strings.xml` at line 2174, Replace the singular
steam_invite_header string resource with a plurals resource containing correct
one and other forms for the invite count, and update the code retrieving
steam_invite_header to use getQuantityString with the count so singular and
plural Romanian grammar render correctly.

<string name="steam_invite_failed">Не удалось пригласить — ваша игра не в сессии, к которой можно присоединиться</string>
<string name="steam_join_failed">Не удалось присоединиться — сессия могла закрыться</string>
<string name="steam_invite_sent">Приглашение отправлено</string>
<string name="steam_invite_joined">В вашей игре</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the joined-game confirmation.

В вашей игре means “In your game,” which does not confirm that the user joined. Use wording such as Вы присоединились к игре (or Вы присоединились к вашей игре if the source means “Joined your game”).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/res/values-ru/strings.xml` at line 2107, Update the Russian
translation for the steam_invite_joined string to explicitly confirm that the
user joined the game, using wording equivalent to “Вы присоединились к игре” and
matching the source meaning.

Comment thread HANDOFF_EXTERNAL_PERF.md Outdated
Comment on lines +30 to +35
DEVICE STATE AFTER THIS BUG: stock pre-feature shim (41528 bytes, from git f7615aba8)
restored at imagefs/usr/lib/libredirect-bionic-wx.so — wine view truthful, boots slow.
Experimental builds parked in /data/local/tmp/ffp_*.so. The .so.orig backup no longer
exists (v29 reinstall recreated imagefs). App assets on the DEVICE build still contain
the buggy gated shims, but nothing re-copies them unless the file goes missing or
LATEST_VERSION bumps.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file existence =="
git ls-files HANDOFF_EXTERNAL_PERF.md || true

echo "== relevant lines =="
if [ -f HANDOFF_EXTERNAL_PERF.md ]; then
  nl -ba HANDOFF_EXTERNAL_PERF.md | sed -n '20,55p'
fi

echo "== related snippets in docs =="
rg -n "so\.orig|LATEST_VERSION|e1141a36|f7615aba8|41528|libredirect-bionic-wx" . --glob '!.*node_modules*' --glob '!**/.git/**' 2>/dev/null || true

echo "== git status/stat =="
git status --short -- HANDOFF_EXTERNAL_PERF.md || true
git diff --stat -- HANDOFF_EXTERNAL_PERF.md || true
git diff -- HANDOFF_EXTERNAL_PERF.md | sed -n '1,220p' || true

Repository: utkarshdalal/GameNative

Length of output: 4695


Fix the unreconciled device-state notes before merging.

Lines 30-35 record the live device state as stock glib/shim with no .so.orig, while the note around lines 42-45 records e1141a36 being deployed with the backup present. This contradictory handoff can send engineers to the wrong device image and restore context. Preserve a single canonical device-state section with the current live file, its hash/size, backup status, and deployed hash.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@HANDOFF_EXTERNAL_PERF.md` around lines 30 - 35, The device-state notes in
HANDOFF_EXTERNAL_PERF.md are contradictory between the current-state block and
the e1141a36 deployment note. Consolidate them into one canonical device-state
section, updating the current live file, hash/size, .so.orig backup status, and
deployed hash to match the actual device; remove or reconcile the conflicting
duplicate state description.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

13 issues found and verified against the latest diff

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:2036">
P2: steam_invite_header will render "1 friends" due to missing pluralization. Use `<plurals name="steam_invite_header"><item quantity="one">%1$d friend</item><item quantity="other">%1$d friends</item></plurals>` instead of a flat `<string>` resource.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/PrefManager.kt:348">
P3: The hardcoded upper bound `6` duplicates the QuickMenuTab constant set without a shared source of truth. If tabs are renumbered or added in QuickMenu.kt, PrefManager's bound can silently fall out of sync. Consider exposing a `MAX_TAB` constant (e.g., as a public val in an internal object or directly on PrefManager) and referencing it from both sides.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt:30">
P2: A second invite received while the first invite is joining is silently discarded: `show` replaces pending invite A with B, then A's completion clears B. Associate clearing with the accepted invite (or otherwise serialize/queue replacement) so a newer prompt survives the earlier request.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt:76">
P2: After the last friend disappears, the open invite tab keeps that stale row and still lets it send an invite. Reconcile an empty successful list too; distinguish a failed `LIST` response in `SteamOverlayClient` if preserving data on transport failure is required.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt:76">
P3: When the Steam host becomes unavailable while the invite tab is open, the UI doesn't reflect it. `refreshQuietly()` returns early if the friend list is empty, without recalculating `hostUnavailable`. The header stays on the friend count instead of showing "unavailable". Consider updating `hostUnavailable` even on the early-return path, matching the logic in `refresh()`: `hostUnavailable = loaded.isEmpty() && !SteamOverlayClient.isAvailable()`. If the list is empty because there are genuinely no online friends (host still healthy), `isAvailable()` returning true keeps `hostUnavailable` false, so the check is safe.</violation>

<violation number="3" location="app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt:81">
P2: The polling path cannot recover from an initially unavailable host or display friends that appear while the tab stays open, because it maps only the old IDs. Preserve existing ordering but append newly loaded friends and clear `hostUnavailable` after a successful reply.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/component/QuickMenu.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/component/QuickMenu.kt:400">
P2: An invite request received while an already-open menu is displayed is treated as if the menu were closed, because this coroutine captures initial `isVisible`. Restart it when visibility changes so an open menu neither switches tabs unexpectedly nor leaves the game paused after an invite request.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/component/QuickMenu.kt:838">
P2: The invite tab shows a brief loading flicker when the game triggers an invite request. `consumeGameInviteRequest()` already loads friends before opening the tab, but `SteamInviteQuickMenuTab`'s `LaunchedEffect(Unit)` then calls `refresh()` again, setting `isLoading` back to true and causing a visual flash. Consider either skipping the initial `refresh()` in the tab's LaunchedEffect when opened via game request, or not calling `refresh()` inside `consumeGameInviteRequest()` since the tab's LaunchedEffect will load data anyway.</violation>

<violation number="3" location="app/src/main/java/app/gamenative/ui/component/QuickMenu.kt:860">
P2: Large Steam friend lists eagerly create and refresh every row, which can stall or exhaust the menu on accounts with many friends. A `LazyColumn` with keyed items would retain only visible rows during polling.</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt:88">
P2: The game invite overlay embeds four user-facing strings directly as Kotlin string literals ("invited you to play", "Joining…", "Join", "Dismiss") instead of referencing `stringResource(R.string.*)`. The rest of the invite feature already defines related resource strings in `strings.xml` (`steam_invite_joinable`, `steam_invite_sent`, `steam_join_failed`, etc.), so pulling these into resources as well keeps the feature consistent and localizable.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamOverlayClient.kt:63">
P3: `self()` returns the host's known lobby ID as part of `SelfInfo.connectString` because the host includes a lobby field between the Steam ID and connect string. Any caller using this API gets an invalid join token; parsing the second separator (while preserving spaces in the token) would keep the client protocol aligned with the host.</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:4022">
P2: Steam invite join tokens are written to INFO logs, exposing game-specific connection credentials and allowing externally supplied line breaks to forge log entries. Logging only the inviter ID (or a redacted/hash form of the payload) would preserve diagnostics without recording the join token.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/service/SteamService.kt:4025">
P2: Incoming invites are surfaced even when there is no bionic Steam host to handle them, so users can see a Join prompt outside a bionic game and the action will fail. Gating this notification on the active bionic host/game state (or routing non-bionic invites through the normal Steam flow) would avoid presenting an action that cannot work.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

<string name="lsfg_performance_mode_desc">Reduces quality for higher throughput</string>
<!-- Bionic FG (AI) Frame Generation -->
<string name="steam_invite_tab_title">Invite</string>
<string name="steam_invite_header">%1$d friends</string>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: steam_invite_header will render "1 friends" due to missing pluralization. Use <plurals name="steam_invite_header"><item quantity="one">%1$d friend</item><item quantity="other">%1$d friends</item></plurals> instead of a flat <string> resource.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/res/values/strings.xml, line 2036:

<comment>steam_invite_header will render "1 friends" due to missing pluralization. Use `<plurals name="steam_invite_header"><item quantity="one">%1$d friend</item><item quantity="other">%1$d friends</item></plurals>` instead of a flat `<string>` resource.</comment>

<file context>
@@ -2032,6 +2032,16 @@
     <string name="lsfg_performance_mode_desc">Reduces quality for higher throughput</string>
     <!-- Bionic FG (AI) Frame Generation -->
+    <string name="steam_invite_tab_title">Invite</string>
+    <string name="steam_invite_header">%1$d friends</string>
+    <string name="steam_invite_joinable">In this game — tap to join</string>
+    <string name="steam_invite_unavailable">Steam client not ready</string>
</file context>

val pending: StateFlow<GameInvite?> = _pending.asStateFlow()

fun show(fromSteamId: Long, connectString: String) {
_pending.value = GameInvite(fromSteamId, connectString)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: A second invite received while the first invite is joining is silently discarded: show replaces pending invite A with B, then A's completion clears B. Associate clearing with the accepted invite (or otherwise serialize/queue replacement) so a newer prompt survives the earlier request.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt, line 30:

<comment>A second invite received while the first invite is joining is silently discarded: `show` replaces pending invite A with B, then A's completion clears B. Associate clearing with the accepted invite (or otherwise serialize/queue replacement) so a newer prompt survives the earlier request.</comment>

<file context>
@@ -0,0 +1,36 @@
+    val pending: StateFlow<GameInvite?> = _pending.asStateFlow()
+
+    fun show(fromSteamId: Long, connectString: String) {
+        _pending.value = GameInvite(fromSteamId, connectString)
+    }
+
</file context>

/** Refreshes without the loading flicker, for the poll that runs while the tab is open. */
suspend fun refreshQuietly() {
val loaded = SteamOverlayClient.listFriends()
if (loaded.isEmpty()) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: After the last friend disappears, the open invite tab keeps that stale row and still lets it send an invite. Reconcile an empty successful list too; distinguish a failed LIST response in SteamOverlayClient if preserving data on transport failure is required.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt, line 76:

<comment>After the last friend disappears, the open invite tab keeps that stale row and still lets it send an invite. Reconcile an empty successful list too; distinguish a failed `LIST` response in `SteamOverlayClient` if preserving data on transport failure is required.</comment>

<file context>
@@ -0,0 +1,119 @@
+    /** Refreshes without the loading flicker, for the poll that runs while the tab is open. */
+    suspend fun refreshQuietly() {
+        val loaded = SteamOverlayClient.listFriends()
+        if (loaded.isEmpty()) return
+
+        // Keep the existing order rather than re-sorting: rows moving under a controller
</file context>

// Keep the existing order rather than re-sorting: rows moving under a controller
// cursor mid-navigation is worse than a joined friend staying where they were.
val byId = loaded.associateBy { it.steamId }
friends = friends.mapNotNull { byId[it.steamId] }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The polling path cannot recover from an initially unavailable host or display friends that appear while the tab stays open, because it maps only the old IDs. Preserve existing ordering but append newly loaded friends and clear hostUnavailable after a successful reply.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt, line 81:

<comment>The polling path cannot recover from an initially unavailable host or display friends that appear while the tab stays open, because it maps only the old IDs. Preserve existing ordering but append newly loaded friends and clear `hostUnavailable` after a successful reply.</comment>

<file context>
@@ -0,0 +1,119 @@
+        // Keep the existing order rather than re-sorting: rows moving under a controller
+        // cursor mid-navigation is worse than a joined friend staying where they were.
+        val byId = loaded.associateBy { it.steamId }
+        friends = friends.mapNotNull { byId[it.steamId] }
+
+        // Once they're actually in, the "sent" note has served its purpose.
</file context>

subtitle = state.lastError?.let { stringResource(it) },
)

state.friends.forEachIndexed { index, friend ->

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Large Steam friend lists eagerly create and refresh every row, which can stall or exhaust the menu on accounts with many friends. A LazyColumn with keyed items would retain only visible rows during polling.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/component/QuickMenu.kt, line 860:

<comment>Large Steam friend lists eagerly create and refresh every row, which can stall or exhaust the menu on accounts with many friends. A `LazyColumn` with keyed items would retain only visible rows during polling.</comment>

<file context>
@@ -769,6 +822,66 @@ fun QuickMenu(
+            subtitle = state.lastError?.let { stringResource(it) },
+        )
+
+        state.friends.forEachIndexed { index, friend ->
+            QuickMenuDetailRow(
+                title = friend.name,
</file context>

* overlay's job -- this only surfaces the prompt.
*/
private fun onGameInvite(callback: GameInviteCallback) {
Timber.i("onGameInvite: from=${callback.inviterSteamId} connect=${callback.connectString}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Steam invite join tokens are written to INFO logs, exposing game-specific connection credentials and allowing externally supplied line breaks to forge log entries. Logging only the inviter ID (or a redacted/hash form of the payload) would preserve diagnostics without recording the join token.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamService.kt, line 4022:

<comment>Steam invite join tokens are written to INFO logs, exposing game-specific connection credentials and allowing externally supplied line breaks to forge log entries. Logging only the inviter ID (or a redacted/hash form of the payload) would preserve diagnostics without recording the join token.</comment>

<file context>
@@ -4007,6 +4013,18 @@ class SteamService : Service(), IChallengeUrlChanged {
+     * overlay's job -- this only surfaces the prompt.
+     */
+    private fun onGameInvite(callback: GameInviteCallback) {
+        Timber.i("onGameInvite: from=${callback.inviterSteamId} connect=${callback.connectString}")
+        if (callback.connectString.isEmpty()) return
+
</file context>
Suggested change
Timber.i("onGameInvite: from=${callback.inviterSteamId} connect=${callback.connectString}")
+ Timber.i("onGameInvite: from=${callback.inviterSteamId}")

Timber.i("onGameInvite: from=${callback.inviterSteamId} connect=${callback.connectString}")
if (callback.connectString.isEmpty()) return

GameInviteNotificationManager.show(callback.inviterSteamId, callback.connectString)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Incoming invites are surfaced even when there is no bionic Steam host to handle them, so users can see a Join prompt outside a bionic game and the action will fail. Gating this notification on the active bionic host/game state (or routing non-bionic invites through the normal Steam flow) would avoid presenting an action that cannot work.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamService.kt, line 4025:

<comment>Incoming invites are surfaced even when there is no bionic Steam host to handle them, so users can see a Join prompt outside a bionic game and the action will fail. Gating this notification on the active bionic host/game state (or routing non-bionic invites through the normal Steam flow) would avoid presenting an action that cannot work.</comment>

<file context>
@@ -4007,6 +4013,18 @@ class SteamService : Service(), IChallengeUrlChanged {
+        Timber.i("onGameInvite: from=${callback.inviterSteamId} connect=${callback.connectString}")
+        if (callback.connectString.isEmpty()) return
+
+        GameInviteNotificationManager.show(callback.inviterSteamId, callback.connectString)
+    }
+
</file context>

get() = getPref(QUICK_MENU_LAST_TAB, 0)
set(value) {
setPref(QUICK_MENU_LAST_TAB, value.coerceIn(0, 2))
setPref(QUICK_MENU_LAST_TAB, value.coerceIn(0, 6))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: The hardcoded upper bound 6 duplicates the QuickMenuTab constant set without a shared source of truth. If tabs are renumbered or added in QuickMenu.kt, PrefManager's bound can silently fall out of sync. Consider exposing a MAX_TAB constant (e.g., as a public val in an internal object or directly on PrefManager) and referencing it from both sides.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/PrefManager.kt, line 348:

<comment>The hardcoded upper bound `6` duplicates the QuickMenuTab constant set without a shared source of truth. If tabs are renumbered or added in QuickMenu.kt, PrefManager's bound can silently fall out of sync. Consider exposing a `MAX_TAB` constant (e.g., as a public val in an internal object or directly on PrefManager) and referencing it from both sides.</comment>

<file context>
@@ -345,7 +345,7 @@ object PrefManager {
         get() = getPref(QUICK_MENU_LAST_TAB, 0)
         set(value) {
-            setPref(QUICK_MENU_LAST_TAB, value.coerceIn(0, 2))
+            setPref(QUICK_MENU_LAST_TAB, value.coerceIn(0, 6))
         }
 
</file context>

Comment on lines +76 to +77
if (loaded.isEmpty()) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: When the Steam host becomes unavailable while the invite tab is open, the UI doesn't reflect it. refreshQuietly() returns early if the friend list is empty, without recalculating hostUnavailable. The header stays on the friend count instead of showing "unavailable". Consider updating hostUnavailable even on the early-return path, matching the logic in refresh(): hostUnavailable = loaded.isEmpty() && !SteamOverlayClient.isAvailable(). If the list is empty because there are genuinely no online friends (host still healthy), isAvailable() returning true keeps hostUnavailable false, so the check is safe.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt, line 76:

<comment>When the Steam host becomes unavailable while the invite tab is open, the UI doesn't reflect it. `refreshQuietly()` returns early if the friend list is empty, without recalculating `hostUnavailable`. The header stays on the friend count instead of showing "unavailable". Consider updating `hostUnavailable` even on the early-return path, matching the logic in `refresh()`: `hostUnavailable = loaded.isEmpty() && !SteamOverlayClient.isAvailable()`. If the list is empty because there are genuinely no online friends (host still healthy), `isAvailable()` returning true keeps `hostUnavailable` false, so the check is safe.</comment>

<file context>
@@ -0,0 +1,119 @@
+    /** Refreshes without the loading flicker, for the poll that runs while the tab is open. */
+    suspend fun refreshQuietly() {
+        val loaded = SteamOverlayClient.listFriends()
+        if (loaded.isEmpty()) return
+
+        // Keep the existing order rather than re-sorting: rows moving under a controller
</file context>
Suggested change
if (loaded.isEmpty()) return
val loaded = SteamOverlayClient.listFriends()
hostUnavailable = loaded.isEmpty() && !SteamOverlayClient.isAvailable()
if (loaded.isEmpty()) return

// "S <steamid> <connect string, may contain spaces or be empty>"
val rest = line.removePrefix("S ")
val steamId = rest.substringBefore(' ').toLongOrNull() ?: return null
val connect = rest.substringAfter(' ', "").trim()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: self() returns the host's known lobby ID as part of SelfInfo.connectString because the host includes a lobby field between the Steam ID and connect string. Any caller using this API gets an invalid join token; parsing the second separator (while preserving spaces in the token) would keep the client protocol aligned with the host.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/service/SteamOverlayClient.kt, line 63:

<comment>`self()` returns the host's known lobby ID as part of `SelfInfo.connectString` because the host includes a lobby field between the Steam ID and connect string. Any caller using this API gets an invalid join token; parsing the second separator (while preserving spaces in the token) would keep the client protocol aligned with the host.</comment>

<file context>
@@ -0,0 +1,172 @@
+        // "S <steamid> <connect string, may contain spaces or be empty>"
+        val rest = line.removePrefix("S ")
+        val steamId = rest.substringBefore(' ').toLongOrNull() ?: return null
+        val connect = rest.substringAfter(' ', "").trim()
+        return SelfInfo(steamId, connect)
+    }
</file context>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
app/src/main/java/app/gamenative/service/SteamOverlayClient.kt (2)

139-144: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Still missing peer UID verification on the overlay socket.

Abstract-namespace LocalSockets carry no filesystem-based access control; any process able to connect can inject PONG/OK/friend-list responses since there's no check against getPeerCredentials() here. This was flagged in a prior review and remains unresolved in this diff.

🔒 Proposed fix
             LocalSocket().use { socket ->
                 socket.connect(LocalSocketAddress(SOCKET_NAME, LocalSocketAddress.Namespace.ABSTRACT))
                 socket.soTimeout = TIMEOUT_MS
+
+                val peerUid = socket.peerCredentials?.uid
+                if (peerUid != android.os.Process.myUid()) {
+                    Timber.w("SteamOverlayClient: rejecting untrusted peer uid=$peerUid")
+                    return@withContext null
+                }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamOverlayClient.kt` around lines
139 - 144, Update the LocalSocket handling in the overlay client to retrieve and
validate getPeerCredentials() immediately after connect, accepting only the
expected peer UID before writing commands or processing responses. Reject and
close the socket when the UID is not trusted, while preserving the existing
command and timeout flow for valid peers.

146-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Truncated LIST reply is still indistinguishable from a complete one.

reader.readLine() ?: break on host EOF falls through to return@withContext out, so a crash/restart of the host mid-LIST returns a partial friend list as if it were successful, and hostUnavailable won't be set since loaded isn't empty. This mirrors a prior review comment that remains unaddressed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/service/SteamOverlayClient.kt` around lines
146 - 166, Update the response-reading loop in SteamOverlayClient so EOF during
LIST commands is treated as a failed or unavailable host response rather than
returning the partial out list. Preserve the existing single-record behavior and
successful "." terminator path, while ensuring truncated LIST replies propagate
the failure so hostUnavailable is set and partial data is not accepted.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt (1)

88-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hardcoded English strings bypass the localization work done elsewhere in this PR.

"invited you to play", "Join"/"Joining…", and "Dismiss" are literal strings, while this same PR adds steam_invite_tab_title, steam_invite_header, etc. as localized resources for the rest of the invite feature. These three strings will not be translated.

Also applies to: 113-113, 119-119

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt` at line
88, Replace the hardcoded invite overlay strings in the relevant composable with
localized string resources: the invitation message, the Join/Joining… button
labels, and the Dismiss action. Reuse the invite-related resource keys added by
this PR or add appropriately named resources if missing, including translations
for each supported locale.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt`:
- Around line 93-114: Handle failures in both invite actions: in
app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt#L93-L114,
wrap the SteamOverlayClient accept call in try/catch/finally, surface thrown or
false-result failures to the user, always reset busy, and only clear the invite
after successful handling; in
app/src/main/java/app/gamenative/ui/component/QuickMenu.kt#L873-L878, wrap
state.join(friend)/state.invite(friend.steamId) in try/catch so exceptions are
handled rather than escaping the row coroutine.
- Around line 60-69: Update the AnimatedVisibility content in GameInviteOverlay
so it renders a captured non-null invite snapshot during exit rather than
re-reading the live invite state. Preserve the existing enter/exit transitions
and ensure dismiss, join, and auto-clear events keep the card visible until the
exit animation completes.

In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt`:
- Around line 399-411: Update the LaunchedEffect polling loop for inviteMenu so
consumeGameInviteRequest() is called regardless of isVisible, ensuring game
overlay requests are drained while the Quick Menu is open. Preserve the existing
navigation and state updates only when a request is consumed and avoid changing
the polling interval.

In `@app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt`:
- Around line 24-36: Clear pending invites during Steam session teardown by
calling GameInviteNotificationManager.clear() from SteamService.clearUserData()
and/or performLogOffDuties(), ensuring stale invites cannot survive logout or
account switching. Keep GameInviteNotificationManager.show() and its existing
state flow behavior unchanged.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/service/SteamOverlayClient.kt`:
- Around line 139-144: Update the LocalSocket handling in the overlay client to
retrieve and validate getPeerCredentials() immediately after connect, accepting
only the expected peer UID before writing commands or processing responses.
Reject and close the socket when the UID is not trusted, while preserving the
existing command and timeout flow for valid peers.
- Around line 146-166: Update the response-reading loop in SteamOverlayClient so
EOF during LIST commands is treated as a failed or unavailable host response
rather than returning the partial out list. Preserve the existing single-record
behavior and successful "." terminator path, while ensuring truncated LIST
replies propagate the failure so hostUnavailable is set and partial data is not
accepted.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt`:
- Line 88: Replace the hardcoded invite overlay strings in the relevant
composable with localized string resources: the invitation message, the
Join/Joining… button labels, and the Dismiss action. Reuse the invite-related
resource keys added by this PR or add appropriately named resources if missing,
including translations for each supported locale.
🪄 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 Plus

Run ID: d68727e0-e1ed-4a0c-b1c7-ced880117175

📥 Commits

Reviewing files that changed from the base of the PR and between d1fea5a and e4f4a3d.

⛔ Files ignored due to path filters (1)
  • app/src/main/jniLibs/arm64-v8a/libsteambootstrap.so is excluded by !**/*.so
📒 Files selected for processing (27)
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/service/SteamOverlayClient.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/callback/GameInviteCallback.kt
  • app/src/main/java/app/gamenative/service/handler/GameInviteHandler.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
  • app/src/main/java/app/gamenative/ui/component/SteamInviteState.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt
  • app/src/main/java/com/winlator/xenvironment/components/BionicProgramLauncherComponent.java
  • app/src/main/res/values-da/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-fr/strings.xml
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-pl/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-uk/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (11)
  • app/src/main/res/values-it/strings.xml
  • app/src/main/res/values-de/strings.xml
  • app/src/main/res/values-ko/strings.xml
  • app/src/main/res/values-ja/strings.xml
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values-zh-rCN/strings.xml
  • app/src/main/res/values-zh-rTW/strings.xml
  • app/src/main/res/values-pt-rBR/strings.xml
  • app/src/main/res/values-ro/strings.xml
  • app/src/main/res/values-ru/strings.xml
  • app/src/main/res/values-pl/strings.xml

Comment on lines +60 to +69
AnimatedVisibility(
visible = invite != null,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp),
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
) {
val current = invite ?: return@AnimatedVisibility

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exit animation never plays: content reads live state instead of a captured snapshot.

val current = invite ?: return@AnimatedVisibility reads the live invite value inside the animated content. The instant invite becomes null (Dismiss/Join/auto-clear), this collapses to nothing immediately, so the configured slideOutHorizontally + fadeOut exit transition never actually renders the card while animating out.

♻️ Suggested fix
     val invite by GameInviteNotificationManager.pending.collectAsState()
+    var lastInvite by remember { mutableStateOf<GameInvite?>(null) }
+    LaunchedEffect(invite) {
+        if (invite != null) lastInvite = invite
+    }
 
     var inviterName by remember { mutableStateOf<String?>(null) }
     ...
     AnimatedVisibility(
         visible = invite != null,
         ...
     ) {
-        val current = invite ?: return@AnimatedVisibility
+        val current = lastInvite ?: return@AnimatedVisibility
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AnimatedVisibility(
visible = invite != null,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp),
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
) {
val current = invite ?: return@AnimatedVisibility
val invite by GameInviteNotificationManager.pending.collectAsState()
var lastInvite by remember { mutableStateOf<GameInvite?>(null) }
LaunchedEffect(invite) {
if (invite != null) lastInvite = invite
}
var inviterName by remember { mutableStateOf<String?>(null) }
AnimatedVisibility(
visible = invite != null,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(16.dp),
enter = slideInHorizontally(initialOffsetX = { it }) + fadeIn(),
exit = slideOutHorizontally(targetOffsetX = { it }) + fadeOut(),
) {
val current = lastInvite ?: return@AnimatedVisibility
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt` around
lines 60 - 69, Update the AnimatedVisibility content in GameInviteOverlay so it
renders a captured non-null invite snapshot during exit rather than re-reading
the live invite state. Preserve the existing enter/exit transitions and ensure
dismiss, join, and auto-clear events keep the card visible until the exit
animation completes.

Comment on lines +93 to +114
TextButton(
enabled = !busy,
onClick = {
busy = true
scope.launch {
val lobby = current.lobbyId
val ok = if (lobby != null) {
SteamOverlayClient.acceptInvite(lobby, current.fromSteamId)
} else {
SteamOverlayClient.acceptRichPresenceJoin(
current.fromSteamId,
current.connectString,
)
}
Timber.i("GameInviteOverlay: accept -> $ok (lobby=$lobby)")
busy = false
GameInviteNotificationManager.clear()
}
},
) {
Text(if (busy) "Joining…" else "Join")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing exception handling around new invite/join coroutines in both invite UI surfaces.

Both the overlay's "Join" action and the Quick Menu's per-friend "activate" (join/invite) action launch a Steam-overlay-related suspend call via scope.launch with no try/catch. An exception thrown by SteamOverlayClient/SteamInviteState propagates uncaught from these coroutine scopes; in GameInviteOverlay.kt this additionally leaves the busy flag stuck at true forever (button permanently shows "Joining…") since the reset line is never reached.

  • app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt#L93-L114: wrap the acceptInvite/acceptRichPresenceJoin call in try/catch/finally so busy always resets and a failure (thrown or ok == false) surfaces a message to the user instead of silently clearing the invite.
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt#L873-L878: wrap state.join(friend)/state.invite(friend.steamId) in try/catch so an exception doesn't propagate uncaught from the row's coroutine.
📍 Affects 2 files
  • app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt#L93-L114 (this comment)
  • app/src/main/java/app/gamenative/ui/component/QuickMenu.kt#L873-L878
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt` around
lines 93 - 114, Handle failures in both invite actions: in
app/src/main/java/app/gamenative/ui/component/GameInviteOverlay.kt#L93-L114,
wrap the SteamOverlayClient accept call in try/catch/finally, surface thrown or
false-result failures to the user, always reset busy, and only clear the invite
after successful handling; in
app/src/main/java/app/gamenative/ui/component/QuickMenu.kt#L873-L878, wrap
state.join(friend)/state.invite(friend.steamId) in try/catch so exceptions are
handled rather than escaping the row coroutine.

Comment on lines +399 to +411
if (inviteMenu != null) {
LaunchedEffect(inviteMenu) {
while (true) {
if (!isVisible && inviteMenu.consumeGameInviteRequest()) {
selectedTab = QuickMenuTab.INVITE
PrefManager.quickMenuLastTab = selectedTab
SteamInviteState.openedForGameRequest = true
onRequestOpen()
}
delay(1000)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect SteamOverlayClient's overlay-request transport/queue semantics.
fd -e kt SteamOverlayClient
rg -n -A15 'fun pollOverlayRequest' app/src/main/java/app/gamenative/service/SteamOverlayClient.kt

Repository: utkarshdalal/GameNative

Length of output: 1122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant QuickMenu and SteamOverlayClient definitions/usages around the concern.
printf 'Files:\n'
fd -e kt 'SteamOverlayClient|QuickMenu' .

printf '\nOverlay request definitions/usages:\n'
rg -n -C 10 'OverlayRequest|consumeGameInviteRequest|POLL|A ' app/src/main/java/app/gamenative

printf '\nQuickMenu visibility/poll section:\n'
sed -n '360,425p' app/src/main/java/app/gamenative/ui/component/QuickMenu.kt

printf '\nSteamOverlayClient relevant section:\n'
sed -n '1,130p' app/src/main/java/app/gamenative/service/SteamOverlayClient.kt

Repository: utkarshdalal/GameNative

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'SteamOverlayClient.kt outline:\n'
ast-grep outline app/src/main/java/app/gamenative/service/SteamOverlayClient.kt --view expanded || true

printf '\nSteamOverlayClient.kt first 180 lines:\n'
sed -n '1,180p' app/src/main/java/app/gamenative/service/SteamOverlayClient.kt

printf '\nConsumeGameInviteRequest definitions/usages (focused):\n'
rg -n -C 4 'consumeGameInviteRequest|OverlayRequest|pollOverlayRequest|POLL' app/src/main/java/app/gamenative --glob '*.kt' | sed -n '1,240p'

printf '\nQuickMenu.kt relevant sections:\n'
sed -n '350,425p' app/src/main/java/app/gamenative/ui/component/QuickMenu.kt
sed -n '855,885p' app/src/main/java/app/gamenative/ui/component/QuickMenu.kt

Repository: utkarshdalal/GameNative

Length of output: 21935


Continue polling game overlay requests while the Quick Menu is visible.

inviteMenu.consumeGameInviteRequest() drains POLL requests, but this loop skips them whenever isVisible is true. If the game requests its in-game invite dialog while the menu is already open, the request is left unconsumed until the menu is closed, which can leave the game-side button stuck or behave inconsistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/component/QuickMenu.kt` around lines 399
- 411, Update the LaunchedEffect polling loop for inviteMenu so
consumeGameInviteRequest() is called regardless of isVisible, ensuring game
overlay requests are drained while the Quick Menu is open. Preserve the existing
navigation and state updates only when a request is consumed and avoid changing
the polling interval.

Comment on lines +24 to +36
object GameInviteNotificationManager {

private val _pending = MutableStateFlow<GameInvite?>(null)
val pending: StateFlow<GameInvite?> = _pending.asStateFlow()

fun show(fromSteamId: Long, connectString: String) {
_pending.value = GameInvite(fromSteamId, connectString)
}

fun clear() {
_pending.value = null
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Pending invite isn't cleared on logout/account switch.

GameInviteNotificationManager is a process-wide singleton with no lifecycle hook tied to the Steam session. If an invite arrives and is left unanswered, then the user logs out and a different account logs in on the same device, the stale invite (with the old account's fromSteamId/connectString) would still surface and could be acted on under the new session.

🛡️ Suggested fix
 fun clear() {
     _pending.value = null
 }

And call GameInviteNotificationManager.clear() from SteamService.clearUserData() / performLogOffDuties().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/app/gamenative/ui/util/GameInviteNotificationManager.kt`
around lines 24 - 36, Clear pending invites during Steam session teardown by
calling GameInviteNotificationManager.clear() from SteamService.clearUserData()
and/or performLogOffDuties(), ensuring stale invites cannot survive logout or
account switching. Keep GameInviteNotificationManager.show() and its existing
state flow behavior unchanged.

@utkarshdalal
utkarshdalal merged commit 155ad56 into master Jul 30, 2026
2 of 3 checks passed
@utkarshdalal
utkarshdalal deleted the steam-overlay-invites branch July 30, 2026 03:08
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.

1 participant