Skip to content

refactor: deduplicate network connectivity checks#648

Open
jeremybernstein wants to merge 5 commits intoutkarshdalal:masterfrom
jeremybernstein:jb/dedup-network-check
Open

refactor: deduplicate network connectivity checks#648
jeremybernstein wants to merge 5 commits intoutkarshdalal:masterfrom
jeremybernstein:jb/dedup-network-check

Conversation

@jeremybernstein
Copy link
Contributor

@jeremybernstein jeremybernstein commented Feb 25, 2026

Closes #647

Summary

  • Unify WiFi/internet tracking into a new NetworkMonitor singleton (process-wide, never unregistered) — removes duplicate one-shot ConnectivityManager query from LibraryAppScreen and state tracking from SteamService
  • SteamService keeps its callback only for download-pausing side effect
  • Switch from AtomicBoolean to StateFlow so install button updates in real-time when network changes
  • Skip "Connecting to remote servers..." 10s timeout immediately when offline

Test plan

  • Install a game with WiFi on — button shows "Install"
  • Toggle airplane mode — button updates to "Need Internet" without navigating away
  • Toggle "Download on WiFi only" + switch to mobile data — button shows "WiFi-Only Enabled"
  • Start a download, lose WiFi — download pauses with notification
  • Launch app with no internet — skips connecting screen immediately

Summary by CodeRabbit

  • Refactor

    • Connectivity is now tracked centrally and reactively, making online/offline status consistent across the app.
  • New Features

    • Downloads respect Wi‑Fi-only settings and pause with a localized "Download paused – waiting for Wi‑Fi/LAN" notice.
    • New user-facing "Not connected to Wi‑Fi/LAN" message and improved overall offline handling.
  • Behavior

    • Connection attempts abort sooner when no internet is available, reducing wasted waiting.

move WiFi/Ethernet tracking from SteamService into MainActivity's
existing NetworkCallback. SteamService keeps its callback only for
download-pausing side effect. LibraryAppScreen reads both states
from MainActivity instead of doing a one-shot ConnectivityManager query.
switch connectivity state from AtomicBoolean to StateFlow so
LibraryAppScreen recomposes when network changes — install button
updates in real-time without navigating away and back
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Introduce a process-wide NetworkMonitor singleton exposing StateFlows for internet and Wi‑Fi connectivity; remove MainActivity-local network callback/state and update consumers (SteamService, LibraryAppScreen, Coil interceptor) to read NetworkMonitor; initialize NetworkMonitor in PluviaApp and add two download-related string resources.

Changes

Cohort / File(s) Summary
New central monitor
app/src/main/java/app/gamenative/NetworkMonitor.kt
Add NetworkMonitor singleton with hasInternet: StateFlow<Boolean>, isWifiConnected: StateFlow<Boolean>, and init(context: Context); registers ConnectivityManager callback and maintains per-network capabilities.
Startup initialization
app/src/main/java/app/gamenative/PluviaApp.kt
Call NetworkMonitor.init(this) from application onCreate.
MainActivity cleanup
app/src/main/java/app/gamenative/MainActivity.kt
Remove local networkCallback, _hasInternet/getter, registration/unregistration, and related imports; replace internal references with NetworkMonitor usage.
Service download gating
app/src/main/java/app/gamenative/service/SteamService.kt
Replace per-instance Wi‑Fi tracking with NetworkMonitor.isWifiConnected; add checkWifiOrNotify() and use centralized check to gate/pause downloads and show localized notifications.
UI reactive consumption
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
Replace one‑shot ConnectivityManager queries with collectAsState() from NetworkMonitor.hasInternet and NetworkMonitor.isWifiConnected for reactive UI gating.
Coil / image loading
app/src/main/java/.../CoilInterceptor
References to former MainActivity hasInternet replaced with NetworkMonitor.hasInternet.value to control offline image handling.
Strings
app/src/main/res/values/strings.xml
Add download_no_wifi and download_paused_wifi for user-facing download notifications.

Sequence Diagram(s)

sequenceDiagram
    participant App as PluviaApp
    participant NM as NetworkMonitor
    participant Steam as SteamService
    participant UI as LibraryAppScreen
    participant Coil as CoilInterceptor

    App->>NM: init(context)
    NM->>NM: register ConnectivityManager callback\ntrack per-network caps, compute StateFlows
    NM--)Steam: expose isWifiConnected StateFlow / .value
    NM--)UI: expose hasInternet & isWifiConnected StateFlows
    NM--)Coil: expose hasInternet (.value) for interceptor checks
    Steam->>Steam: checkWifiOrNotify() reads NM state -> allow or pause downloads
    UI->>UI: collect StateFlows -> recompute UI gating
    Coil->>Coil: read hasInternet -> alter image loading/offline behavior
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • phobos665

Poem

🐇 I hopped through sockets, caps, and nets,
One monitor now minding the assets.
Apps listen close, downloads take care,
Images load when routes are fair —
A carrot cheer for tidy states! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: deduplicate network connectivity checks' accurately and concisely describes the main objective of the changeset—consolidating duplicate network monitoring logic across multiple components into a centralized NetworkMonitor.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #647: centralizes network state via NetworkMonitor StateFlows, eliminates LibraryAppScreen's one-shot ConnectivityManager query, keeps SteamService callback only for side effects, and migrates from AtomicBoolean to StateFlow for reactive UI updates.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the stated objectives: introducing NetworkMonitor singleton, removing duplicate checks, consolidating connectivity state, updating dependent components, and adding localized strings for notifications. No unrelated modifications detected.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)

2615-2623: ⚠️ Potential issue | 🟡 Minor

Avoid pausing on every onLost event without current-state guard.

Line 2615 pauses all downloads whenever any tracked network is lost. If another Wi‑Fi/LAN network is still active, this causes unnecessary pauses.

Suggested fix
-                if (PrefManager.downloadOnWifiOnly) {
+                if (PrefManager.downloadOnWifiOnly && !isWifiConnected) {
                     for ((appId, info) in downloadJobs.entries.toList()) {
                         Timber.d("Cancelling job")
                         info.cancel()
                         PluviaApp.events.emit(AndroidEvent.DownloadPausedDueToConnectivity(appId))
                         removeDownloadJob(appId)
                     }
                     notificationHelper.notify("Download paused – waiting for Wi-Fi/LAN")
                 }
🤖 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 2615 -
2623, The current onLost handler unconditionally pauses all downloads when any
tracked network is lost; change it to first check the actual current
connectivity state and only pause/remove jobs if there is no longer any active
Wi‑Fi/LAN connection. In practice, add a guard before iterating downloadJobs
(e.g., call your ConnectivityManager helper or implement an isOnWifiOrLan()
check) and only run the loop that calls info.cancel(), emits
AndroidEvent.DownloadPausedDueToConnectivity(appId) and removeDownloadJob(appId)
when that check returns false; keep the PrefManager.downloadOnWifiOnly branch
but make it conditional on the updated connectivity check so transient losses of
one network won’t pause downloads if another Wi‑Fi/LAN is still available.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/service/SteamService.kt (1)

1012-1015: Deduplicate the Wi‑Fi-only gate logic into one helper.

The same guard+notification block appears twice. Extracting it reduces drift risk and keeps behavior consistent.

Refactor sketch
+        private fun enforceWifiOnlyGate(): Boolean {
+            if (PrefManager.downloadOnWifiOnly && !isWifiConnected) {
+                instance?.notificationHelper?.notify("Not connected to Wi‑Fi/LAN")
+                return false
+            }
+            return true
+        }

-            if (PrefManager.downloadOnWifiOnly && !isWifiConnected) {
-                instance?.notificationHelper?.notify("Not connected to Wi‑Fi/LAN")
-                return null
-            }
+            if (!enforceWifiOnlyGate()) return null
...
-            if (PrefManager.downloadOnWifiOnly && !isWifiConnected) {
-                instance?.notificationHelper?.notify("Not connected to Wi‑Fi/LAN")
-                return null
-            }
+            if (!enforceWifiOnlyGate()) return null

Also applies to: 1364-1367

🤖 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 1012 -
1015, Create a single helper in SteamService (e.g., checkWifiOrNotify or
enforceWifiOnly) that encapsulates the PrefManager.downloadOnWifiOnly &&
!isWifiConnected check and calls instance?.notificationHelper?.notify("Not
connected to Wi‑Fi/LAN") when blocked; have it return a Boolean (true = ok to
proceed, false = blocked). Replace both duplicated guard blocks that currently
use PrefManager.downloadOnWifiOnly, isWifiConnected, instance and
notificationHelper.notify with a single call like if (!checkWifiOrNotify())
return null so behavior and return semantics remain identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/app/gamenative/MainActivity.kt`:
- Around line 91-93: The code currently sets _isWifiConnected from the raw
wifiNetworks set, which can include unvalidated/captive Wi‑Fi; change the logic
to derive Wi‑/LAN connectivity from the validated networks instead. After
computing validated (the list/collection of validated networks), compute whether
any of those validated entries satisfy isWifiOrEthernet(caps) and set
_isWifiConnected based on that (e.g., replace wifiNetworks.isNotEmpty() with a
check over validated using isWifiOrEthernet), while keeping the existing
wifiNetworks updates if you still need the full transport set.

---

Outside diff comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 2615-2623: The current onLost handler unconditionally pauses all
downloads when any tracked network is lost; change it to first check the actual
current connectivity state and only pause/remove jobs if there is no longer any
active Wi‑Fi/LAN connection. In practice, add a guard before iterating
downloadJobs (e.g., call your ConnectivityManager helper or implement an
isOnWifiOrLan() check) and only run the loop that calls info.cancel(), emits
AndroidEvent.DownloadPausedDueToConnectivity(appId) and removeDownloadJob(appId)
when that check returns false; keep the PrefManager.downloadOnWifiOnly branch
but make it conditional on the updated connectivity check so transient losses of
one network won’t pause downloads if another Wi‑Fi/LAN is still available.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 1012-1015: Create a single helper in SteamService (e.g.,
checkWifiOrNotify or enforceWifiOnly) that encapsulates the
PrefManager.downloadOnWifiOnly && !isWifiConnected check and calls
instance?.notificationHelper?.notify("Not connected to Wi‑Fi/LAN") when blocked;
have it return a Boolean (true = ok to proceed, false = blocked). Replace both
duplicated guard blocks that currently use PrefManager.downloadOnWifiOnly,
isWifiConnected, instance and notificationHelper.notify with a single call like
if (!checkWifiOrNotify()) return null so behavior and return semantics remain
identical.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 761d93b and 05727ba.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt

- derive isWifiConnected from validated networks only (excludes captive portals)
- SteamService onLost checks actual state before pausing (multi-network safe)
- extract checkWifiOrNotify() helper for duplicated WiFi guard
- move notification strings to strings.xml for localization
Copy link
Contributor

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

Choose a reason for hiding this comment

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

1 issue found across 3 files

Prompt for AI agents (all issues)

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


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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:294">
P2: SteamService now relies on MainActivity.isWifiConnected for Wi‑Fi gating, but that StateFlow only updates while MainActivity is alive. If the activity is not running, the flow stays at its default/stale value and downloads can be incorrectly blocked or allowed in the background.</violation>
</file>

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

Copy link
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/res/values/strings.xml (1)

753-754: Move new strings to their own section and align the prefix.

Both strings are SteamService notification messages (per the PR description: "Move notification strings to strings.xml for localization"), yet they are placed inside <!-- LibraryAppScreen: UI Labels --> whose every other entry carries the library_ prefix. Keeping notification strings in the wrong section makes the file harder to navigate and may mislead translators.

♻️ Suggested reorganisation
     <string name="library_file_count">File %1$d of %2$d</string>
-    <string name="download_no_wifi">Not connected to Wi\u2011Fi/LAN</string>
-    <string name="download_paused_wifi">Download paused – waiting for Wi-Fi/LAN</string>

     <!-- PluviaMain: Update & App Info -->

Add a dedicated section further down (or near the <!-- Login & Connection --> block):

+    <!-- SteamService: Notifications -->
+    <string name="download_no_wifi">Not connected to Wi-Fi/LAN</string>
+    <string name="download_paused_wifi">Download paused – waiting for Wi-Fi/LAN</string>
+
     <!-- PluviaMain: Update & App Info -->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/res/values/strings.xml` around lines 753 - 754, Move the two
SteamService notification strings out of the "LibraryAppScreen: UI Labels" block
and into a new dedicated notification section (e.g., near the existing "Login &
Connection" block); specifically relocate the <string name="download_no_wifi">
and <string name="download_paused_wifi"> entries into that new section and align
their naming/prefix with other notification keys if your convention requires it
(ensure the new section comment clearly indicates SteamService/notification
strings so translators can find them).
app/src/main/java/app/gamenative/service/SteamService.kt (1)

1019-1021: checkWifiOrNotify() called twice in the same download dispatch path

downloadApp(appId, dlcAppIds, isUpdateOrVerify) (line 1019) calls checkWifiOrNotify() at line 1020, and if it passes, delegates to the full-parameter overload downloadApp(appId, downloadableDepots, …) which calls checkWifiOrNotify() again at line 1368. In a race where connectivity drops between the two calls, the user receives a duplicate notification. Removing the redundant guard at line 1020 (or line 1368 if this call site is the only entry point) removes the ambiguity.

♻️ Proposed fix — remove the redundant guard at the shallower overload
 fun downloadApp(appId: Int, dlcAppIds: List<Int>, isUpdateOrVerify: Boolean): DownloadInfo? {
-    if (!checkWifiOrNotify()) return null
     return getAppInfoOf(appId)?.let { appInfo ->
🤖 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 1019 -
1021, The shallow overload downloadApp(appId: Int, dlcAppIds: List<Int>,
isUpdateOrVerify: Boolean) currently calls checkWifiOrNotify() and then
delegates to the deeper overload which also calls checkWifiOrNotify(), causing
duplicate notifications; remove the redundant guard and early return from the
shallow overload (keep the call to getAppInfoOf and the delegation to the
full-parameter downloadApp), so that the deeper downloadApp(appId,
downloadableDepots, ...) remains the single place that performs the connectivity
check via checkWifiOrNotify().
🤖 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 2614-2626: SteamService.onLost currently reads the shared
StateFlow isWifiConnected which can be stale due to unordered NetworkCallback
delivery; instead, inside the onLost override query ConnectivityManager directly
to determine whether any Wi‑Fi/LAN transports remain before pausing downloads:
obtain the ConnectivityManager, iterate its current networks
(connectivityManager.allNetworks or getAllNetworks()) and for each check
connectivityManager.getNetworkCapabilities(network) for TRANSPORT_WIFI or
TRANSPORT_ETHERNET; compute a live wifiConnected boolean and then, if
PrefManager.downloadOnWifiOnly && !wifiConnected, cancel downloadJobs (call
info.cancel,
PluviaApp.events.emit(AndroidEvent.DownloadPausedDueToConnectivity(appId)),
removeDownloadJob(appId)) and call
notificationHelper.notify(getString(R.string.download_paused_wifi)) as currently
done.
- Around line 296-303: The checkWifiOrNotify() method currently suppresses the
user notification when instance is null; update this method to explicitly handle
the null branch by logging a warning (e.g., via Log.w or your app logger) before
returning false so the failure is observable; keep the existing behavior of
calling instance.notificationHelper.notify(...) when instance is non-null, and
reference checkWifiOrNotify(), instance, notificationHelper.notify,
PrefManager.downloadOnWifiOnly and isWifiConnected when making the change.
- Line 294: MainActivity.isWifiConnected is stale when MainActivity unregisters
its NetworkCallback, so update SteamService to own/observe network state instead
of relying on MainActivity: add onAvailable and/or onCapabilitiesChanged
handlers to SteamService's NetworkCallback (in addition to the existing onLost)
to maintain an internal wifiConnected flag and change the isWifiConnected getter
to read that flag (or use the shared NetworkMonitor singleton referenced in
comments) so downloads respect current connectivity even when MainActivity is
destroyed; also remove the redundant call to checkWifiOrNotify in the nested
downloadApp overload (leave a single check prior to starting downloads) to avoid
duplicate notifications.

In `@app/src/main/res/values/strings.xml`:
- Line 753: The string resource download_no_wifi contains a non‑breaking hyphen
(U+2011) in "Wi\u2011Fi/LAN"; replace that character with a standard
hyphen-minus '-' so the value becomes "Not connected to Wi-Fi/LAN" to match
other Wi‑Fi strings (e.g., those used elsewhere) and avoid invisible character
issues.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 1019-1021: The shallow overload downloadApp(appId: Int, dlcAppIds:
List<Int>, isUpdateOrVerify: Boolean) currently calls checkWifiOrNotify() and
then delegates to the deeper overload which also calls checkWifiOrNotify(),
causing duplicate notifications; remove the redundant guard and early return
from the shallow overload (keep the call to getAppInfoOf and the delegation to
the full-parameter downloadApp), so that the deeper downloadApp(appId,
downloadableDepots, ...) remains the single place that performs the connectivity
check via checkWifiOrNotify().

In `@app/src/main/res/values/strings.xml`:
- Around line 753-754: Move the two SteamService notification strings out of the
"LibraryAppScreen: UI Labels" block and into a new dedicated notification
section (e.g., near the existing "Login & Connection" block); specifically
relocate the <string name="download_no_wifi"> and <string
name="download_paused_wifi"> entries into that new section and align their
naming/prefix with other notification keys if your convention requires it
(ensure the new section comment clearly indicates SteamService/notification
strings so translators can find them).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 05727ba and 32b2282.

📒 Files selected for processing (3)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • 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/MainActivity.kt


internal var instance: SteamService? = null

val isWifiConnected: Boolean get() = MainActivity.isWifiConnected.value
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for MainActivity.isWifiConnected declaration and initialization
rg -n "isWifiConnected" app/src/main/java/app/gamenative/MainActivity.kt -B5 -A5

Repository: utkarshdalal/GameNative

Length of output: 1224


🏁 Script executed:

# Search for SteamService START_STICKY declaration
rg -n "START_STICKY" --type=kt

Repository: utkarshdalal/GameNative

Length of output: 93


🏁 Script executed:

# Search for StateFlow declarations in MainActivity
rg -n "MutableStateFlow|StateFlow" app/src/main/java/app/gamenative/MainActivity.kt -B2 -A2

Repository: utkarshdalal/GameNative

Length of output: 757


🏁 Script executed:

# Check the callback registration in SteamService
rg -n "registerNetworkCallback\|onNetworkAvailable\|onLost" app/src/main/java/app/gamenative/service/SteamService.kt -B3 -A3

Repository: utkarshdalal/GameNative

Length of output: 49


🏁 Script executed:

# Search for registerNetworkCallback and callback registration
rg "registerNetworkCallback" --type=java --type=kt

Repository: utkarshdalal/GameNative

Length of output: 93


🏁 Script executed:

# Search for START_STICKY in service declarations
rg "START_STICKY" --type=java --type=kt

Repository: utkarshdalal/GameNative

Length of output: 93


🏁 Script executed:

# Check MainActivity onCreate and callback registration
cat -n app/src/main/java/app/gamenative/MainActivity.java | head -150

Repository: utkarshdalal/GameNative

Length of output: 149


🏁 Script executed:

# Check if MainActivity has a companion object and how the callback is registered
rg "NetworkCallback\|registerNetworkCallback" app/src/main/java/app/gamenative/MainActivity.java -B5 -A10

Repository: utkarshdalal/GameNative

Length of output: 157


🏁 Script executed:

# Search for registerNetworkCallback and callback setup
rg "registerNetworkCallback|NetworkCallback" -B3 -A5

Repository: utkarshdalal/GameNative

Length of output: 7252


🏁 Script executed:

# Search for START_STICKY
rg "START_STICKY"

Repository: utkarshdalal/GameNative

Length of output: 846


🏁 Script executed:

# Check MainActivity.kt for callback registration details
head -200 app/src/main/java/app/gamenative/MainActivity.kt

Repository: utkarshdalal/GameNative

Length of output: 8257


🏁 Script executed:

# Check SteamService companion properties around line 294
sed -n '290,310p' app/src/main/java/app/gamenative/service/SteamService.kt

Repository: utkarshdalal/GameNative

Length of output: 889


🏁 Script executed:

# Check the full context of isWifiConnected in SteamService
rg -n "val isWifiConnected" app/src/main/java/app/gamenative/service/SteamService.kt -B2 -A2

Repository: utkarshdalal/GameNative

Length of output: 306


🏁 Script executed:

# Check where isWifiConnected is used in SteamService to understand the flow
rg -n "isWifiConnected" app/src/main/java/app/gamenative/service/SteamService.kt

Repository: utkarshdalal/GameNative

Length of output: 303


🏁 Script executed:

# Check if SteamService's networkCallback has onAvailable or onCapabilitiesChanged
sed -n '2610,2650p' app/src/main/java/app/gamenative/service/SteamService.kt

Repository: utkarshdalal/GameNative

Length of output: 1901


🏁 Script executed:

# Also check around line 2617 where the callback is used
rg -n "onAvailable\|onCapabilitiesChanged" app/src/main/java/app/gamenative/service/SteamService.kt

Repository: utkarshdalal/GameNative

Length of output: 49


MainActivity's callback unregistration leaves SteamService with stale Wi-Fi state

When MainActivity is destroyed, its onDestroy() unregisters the NetworkCallback, but MainActivity.isWifiConnected (a static MutableStateFlow) retains its last value. If the flow becomes false while MainActivity is still alive, then MainActivity is destroyed, and Wi-Fi becomes available while MainActivity remains dead, SteamService will permanently read stale false from MainActivity.isWifiConnected. Since SteamService's own callback only implements onLost() (no recovery handler for network availability), downloads with downloadOnWifiOnly enabled will be permanently blocked until MainActivity returns and its callback resumes monitoring.

The TODO comment in MainActivity acknowledges the underlying design issue. Consider having SteamService maintain its own Wi-Fi state via onAvailable() and onCapabilitiesChanged() handlers in its callback, or use a shared NetworkMonitor singleton as suggested in the codebase.

Additionally, checkWifiOrNotify() is called redundantly at both lines 1020 and 1368 in the nested downloadApp overloads—consider removing the second call or restructuring to avoid duplicate checks and notification risk.

🤖 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` at line 294,
MainActivity.isWifiConnected is stale when MainActivity unregisters its
NetworkCallback, so update SteamService to own/observe network state instead of
relying on MainActivity: add onAvailable and/or onCapabilitiesChanged handlers
to SteamService's NetworkCallback (in addition to the existing onLost) to
maintain an internal wifiConnected flag and change the isWifiConnected getter to
read that flag (or use the shared NetworkMonitor singleton referenced in
comments) so downloads respect current connectivity even when MainActivity is
destroyed; also remove the redundant call to checkWifiOrNotify in the nested
downloadApp overload (leave a single check prior to starting downloads) to avoid
duplicate notifications.

Copy link
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 3 files (changes from recent commits).

Prompt for AI agents (all issues)

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


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

<violation number="1" location="app/src/main/java/app/gamenative/service/SteamService.kt:298">
P2: Wi‑Fi loss handling in SteamService now depends on MainActivity.isWifiConnected, which stops updating when MainActivity is destroyed. This can leave the Wi‑Fi state stale while the foreground service keeps running, causing Wi‑Fi‑only downloads to continue or stay blocked incorrectly. Consider deriving connectivity state inside the service (or a process‑wide NetworkMonitor) instead of Activity‑scoped state.</violation>
</file>

<file name="app/src/main/res/values/strings.xml">

<violation number="1" location="app/src/main/res/values/strings.xml:753">
P3: Inconsistent hyphen character: `download_no_wifi` uses `\u2011` (non-breaking hyphen) for "Wi‑Fi" while `download_paused_wifi` and all other Wi-Fi strings in the file use a plain hyphen-minus (`-`). This invisible difference can confuse translation tools and cause visual inconsistency. Use a plain hyphen for consistency.</violation>
</file>

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

Copy link
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/service/SteamService.kt (1)

2619-2630: ⚠️ Potential issue | 🟠 Major

TOCTOU gap in onLost — reading stale NetworkMonitor state.

SteamService registers its own NetworkCallback (filtered to TRANSPORT_WIFI | TRANSPORT_ETHERNET) while NetworkMonitor independently registers a separate callback (filtered to NET_CAPABILITY_INTERNET). Android does not guarantee delivery order between two independently registered callbacks for the same underlying network loss event. If SteamService.onLost fires before NetworkMonitor processes the same event and clears _isWifiConnected, then isWifiConnected (line 2621) still returns true, the guard !isWifiConnected is false, and active downloads are not paused even though Wi-Fi is gone.

The previously suggested fix — querying ConnectivityManager directly inside onLost for live state instead of reading the StateFlow — still applies:

🛡️ Proposed fix — live ConnectivityManager query in onLost
 override fun onLost(network: Network) {
-    // only pause if no WiFi/LAN remains (avoids false pause on multi-network)
-    if (PrefManager.downloadOnWifiOnly && !isWifiConnected) {
+    val hasRemainingWifi = connectivityManager.allNetworks.any { n ->
+        val caps = connectivityManager.getNetworkCapabilities(n) ?: return@any false
+        (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+            caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) &&
+            caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+    }
+    if (PrefManager.downloadOnWifiOnly && !hasRemainingWifi) {
🤖 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 2619 -
2630, SteamService.onLost currently reads the possibly-stale StateFlow
isWifiConnected (used with PrefManager.downloadOnWifiOnly) which can miss a
network-loss if NetworkMonitor hasn't updated yet; instead, inside onLost query
ConnectivityManager.getNetworkCapabilities/getActiveNetwork (or use
ConnectivityManager#getNetworkInfo fallback) to determine live Wi‑Fi/LAN
connectivity and only cancel downloadJobs (iterate downloadJobs, call
info.cancel(),
PluviaApp.events.emit(AndroidEvent.DownloadPausedDueToConnectivity(appId)),
removeDownloadJob(appId)) and call notificationHelper.notify(...) when that live
check shows no Wi‑Fi/LAN remains; keep the same behavior but replace the
isWifiConnected check with this immediate ConnectivityManager query to avoid the
TOCTOU race.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/NetworkMonitor.kt (1)

25-63: Add an idempotency guard to init().

networkCaps is a local variable captured by the registered callback. If init() is ever called a second time (accidental double-call, test setup, etc.) a second callback is registered against a brand-new, empty map while the first callback keeps its own map. Both callbacks write to the same _hasInternet / _isWifiConnected flows, but with diverging views of the network inventory — they will race and produce inconsistent state. The KDoc says "call once", but there is no enforcement.

🛡️ Proposed fix — atomic init guard
 object NetworkMonitor {

+    `@Volatile` private var initialized = false
+
     private val _hasInternet = MutableStateFlow(false)
     val hasInternet: StateFlow<Boolean> = _hasInternet.asStateFlow()

     private val _isWifiConnected = MutableStateFlow(false)
     val isWifiConnected: StateFlow<Boolean> = _isWifiConnected.asStateFlow()

     fun init(context: Context) {
+        if (initialized) return
+        initialized = true
         val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/app/gamenative/NetworkMonitor.kt` around lines 25 - 63, Add
an idempotency guard to prevent registering multiple callbacks: introduce a
private AtomicBoolean (e.g., initialized) at the class level and in init() do a
compareAndSet(false, true) and return immediately if it fails; keep networkCaps
as-is (local to the single callback) and proceed to call
cm.registerNetworkCallback only when initialization succeeds so you never
register a second callback that would capture a separate networkCaps map and
race on _hasInternet/_isWifiConnected.
🤖 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/NetworkMonitor.kt`:
- Around line 19-23: The public StateFlows _hasInternet/hasInternet and
_isWifiConnected/isWifiConnected start false and can cause a false-negative on
cold start; in the NetworkMonitor initialization (where you
registerNetworkCallback) perform an immediate synchronous seed read using
ConnectivityManager.activeNetwork and NetworkCapabilities to set _hasInternet
and _isWifiConnected before registering the callback (or document the
snapshot-read caveat on the public properties). Update the code that sets
initial state (e.g., in the NetworkMonitor init/constructor or a
seedInitialNetworkState method) to compute
networkCapabilities.hasCapability(INTERNET) and whether TRANSPORT_WIFI is
present and assign those values to _hasInternet and _isWifiConnected so early
synchronous reads reflect the current connectivity.

---

Duplicate comments:
In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 2619-2630: SteamService.onLost currently reads the possibly-stale
StateFlow isWifiConnected (used with PrefManager.downloadOnWifiOnly) which can
miss a network-loss if NetworkMonitor hasn't updated yet; instead, inside onLost
query ConnectivityManager.getNetworkCapabilities/getActiveNetwork (or use
ConnectivityManager#getNetworkInfo fallback) to determine live Wi‑Fi/LAN
connectivity and only cancel downloadJobs (iterate downloadJobs, call
info.cancel(),
PluviaApp.events.emit(AndroidEvent.DownloadPausedDueToConnectivity(appId)),
removeDownloadJob(appId)) and call notificationHelper.notify(...) when that live
check shows no Wi‑Fi/LAN remains; keep the same behavior but replace the
isWifiConnected check with this immediate ConnectivityManager query to avoid the
TOCTOU race.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/NetworkMonitor.kt`:
- Around line 25-63: Add an idempotency guard to prevent registering multiple
callbacks: introduce a private AtomicBoolean (e.g., initialized) at the class
level and in init() do a compareAndSet(false, true) and return immediately if it
fails; keep networkCaps as-is (local to the single callback) and proceed to call
cm.registerNetworkCallback only when initialization succeeds so you never
register a second callback that would capture a separate networkCaps map and
race on _hasInternet/_isWifiConnected.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32b2282 and 4189ded.

📒 Files selected for processing (6)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/NetworkMonitor.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • app/src/main/res/values/strings.xml

process-wide network state lives in NetworkMonitor, initialized once
in PluviaApp.onCreate. fixes stale state when activity is destroyed
while SteamService still runs. also addresses review feedback:
validate WiFi state (captive portal), guard onLost for multi-network,
extract checkWifiOrNotify helper, localize notification strings,
seed initial state, add idempotency guard.
use NetworkMonitor to detect no-internet on launch and skip
the 10s connection timeout, going straight to offline mode
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

2619-2622: ⚠️ Potential issue | 🟠 Major

Use live network query in onLost to avoid stale Wi‑Fi state.

Line 2621 reads a shared flow snapshot that can still be stale when callback delivery order differs between registrations, so downloads may fail to pause on actual Wi‑Fi loss.

🛠️ Suggested fix
         networkCallback = object : ConnectivityManager.NetworkCallback() {
             override fun onLost(network: Network) {
-                // only pause if no WiFi/LAN remains (avoids false pause on multi-network)
-                if (PrefManager.downloadOnWifiOnly && !isWifiConnected) {
+                // Query live state here to avoid callback-order races.
+                val hasRemainingValidatedWifi = connectivityManager.allNetworks.any { n ->
+                    val caps = connectivityManager.getNetworkCapabilities(n) ?: return@any false
+                    (caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
+                        caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) &&
+                        caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+                }
+                if (PrefManager.downloadOnWifiOnly && !hasRemainingValidatedWifi) {
                     for ((appId, info) in downloadJobs.entries.toList()) {
                         Timber.d("Cancelling job")
                         info.cancel()
                         PluviaApp.events.emit(AndroidEvent.DownloadPausedDueToConnectivity(appId))
                         removeDownloadJob(appId)
                     }
                     notificationHelper.notify(getString(R.string.download_paused_wifi))
                 }
             }
         }
#!/bin/bash
# Verify whether onLost still depends on shared snapshot state instead of live ConnectivityManager query.
rg -n -C3 "override fun onLost|isWifiConnected|allNetworks|getNetworkCapabilities" \
  app/src/main/java/app/gamenative/service/SteamService.kt \
  app/src/main/java/app/gamenative/NetworkMonitor.kt

Also applies to: 2628-2629

🤖 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 2619 -
2622, The onLost callback currently uses the shared snapshot isWifiConnected
which can be stale; change onLost in SteamService to query the live network
state from ConnectivityManager for remaining networks (e.g., iterate
ConnectivityManager.allNetworks and call getNetworkCapabilities(network) to
check for NET_CAPABILITY_WIFI) instead of relying on isWifiConnected, then if
PrefManager.downloadOnWifiOnly is true and no Wi‑Fi capability is found
pause/handle entries in downloadJobs as before; ensure the check runs inside
override fun onLost(network: Network) and remove dependence on the shared-flow
snapshot so pause logic is based on live connectivity.
🧹 Nitpick comments (1)
app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt (1)

239-242: PrefManager.downloadOnWifiOnly is not reactive — wifiAllowed won't recompose if the pref changes in-session.

wifiConnected is a StateFlow and will trigger recomposition correctly, but PrefManager.downloadOnWifiOnly is read as a plain property. In practice this is low-risk because changing the WiFi-only pref requires navigating to Settings (which will cause recomposition on return), but if the pref is ever mutated without navigation, the derived wifiAllowed and all downstream button states will be stale.

Consider exposing downloadOnWifiOnly as a StateFlow (or a derivedStateOf on a snapshot-observable value) so the entire expression recomposes reactively:

♻️ Suggested approach
- val wifiAllowed = !PrefManager.downloadOnWifiOnly || wifiConnected
+ val downloadOnWifiOnly by PrefManager.downloadOnWifiOnlyFlow.collectAsState()
+ val wifiAllowed = !downloadOnWifiOnly || wifiConnected
🤖 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/library/LibraryAppScreen.kt`
around lines 239 - 242, The code reads PrefManager.downloadOnWifiOnly as a plain
property so wifiAllowed won’t recompose when the preference changes; make the
preference reactive (e.g., expose PrefManager.downloadOnWifiOnly as a
StateFlow/Flow or provide a snapshot-observable/derivedState) and consume it
with collectAsState()/observeAsState() when computing wifiAllowed (keep the
existing wifiConnected from NetworkMonitor.isWifiConnected.collectAsState);
update the expression that defines wifiAllowed to use the reactive state so
changes to downloadOnWifiOnly trigger recomposition.
🤖 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 2619-2622: The onLost callback currently uses the shared snapshot
isWifiConnected which can be stale; change onLost in SteamService to query the
live network state from ConnectivityManager for remaining networks (e.g.,
iterate ConnectivityManager.allNetworks and call getNetworkCapabilities(network)
to check for NET_CAPABILITY_WIFI) instead of relying on isWifiConnected, then if
PrefManager.downloadOnWifiOnly is true and no Wi‑Fi capability is found
pause/handle entries in downloadJobs as before; ensure the check runs inside
override fun onLost(network: Network) and remove dependence on the shared-flow
snapshot so pause logic is based on live connectivity.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 239-242: The code reads PrefManager.downloadOnWifiOnly as a plain
property so wifiAllowed won’t recompose when the preference changes; make the
preference reactive (e.g., expose PrefManager.downloadOnWifiOnly as a
StateFlow/Flow or provide a snapshot-observable/derivedState) and consume it
with collectAsState()/observeAsState() when computing wifiAllowed (keep the
existing wifiConnected from NetworkMonitor.isWifiConnected.collectAsState);
update the expression that defines wifiAllowed to use the reactive state so
changes to downloadOnWifiOnly trigger recomposition.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4189ded and abdab69.

📒 Files selected for processing (7)
  • app/src/main/java/app/gamenative/MainActivity.kt
  • app/src/main/java/app/gamenative/NetworkMonitor.kt
  • app/src/main/java/app/gamenative/PluviaApp.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt
  • 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/PluviaApp.kt

@jeremybernstein
Copy link
Contributor Author

♻️ Duplicate comments (1)

🧹 Nitpick comments (1)

🤖 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 2619-2622: The onLost callback currently uses the shared snapshot
isWifiConnected which can be stale; change onLost in SteamService to query the
live network state from ConnectivityManager for remaining networks (e.g.,
iterate ConnectivityManager.allNetworks and call getNetworkCapabilities(network)
to check for NET_CAPABILITY_WIFI) instead of relying on isWifiConnected, then if
PrefManager.downloadOnWifiOnly is true and no Wi‑Fi capability is found
pause/handle entries in downloadJobs as before; ensure the check runs inside
override fun onLost(network: Network) and remove dependence on the shared-flow
snapshot so pause logic is based on live connectivity.

---

Nitpick comments:
In `@app/src/main/java/app/gamenative/ui/screen/library/LibraryAppScreen.kt`:
- Around line 239-242: The code reads PrefManager.downloadOnWifiOnly as a plain
property so wifiAllowed won’t recompose when the preference changes; make the
preference reactive (e.g., expose PrefManager.downloadOnWifiOnly as a
StateFlow/Flow or provide a snapshot-observable/derivedState) and consume it
with collectAsState()/observeAsState() when computing wifiAllowed (keep the
existing wifiConnected from NetworkMonitor.isWifiConnected.collectAsState);
update the expression that defines wifiAllowed to use the reactive state so
changes to downloadOnWifiOnly trigger recomposition.

ℹ️ Review info

@coderabbitai

  1. isWifiConnected reads from NetworkMonitor, which is process-wide and updated by the same connectivity callback system. Invalid
  2. out of scope, pre-existing behavior (could add an issue if it bothers you)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

Tip

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

@jeremybernstein

Thank you for the clarification!

  1. SteamService.onLost staleness concern: You're absolutely right—NetworkMonitor.isWifiConnected is a StateFlow that's reactively maintained by the centralized callback system, so the value is current when onLost reads it. My concern about staleness was incorrect. ✅

  2. PrefManager.downloadOnWifiOnly reactivity: Understood that this is pre-existing behavior and outside the scope of this refactoring PR. Would you like me to open a follow-up issue to track making the preference reactive so the UI updates automatically when users toggle "Download on WiFi only" in settings?


🧠 Learnings used
Learnt from: utkarshdalal
Repo: utkarshdalal/GameNative PR: 159
File: app/src/main/java/app/gamenative/service/DownloadService.kt:6-6
Timestamp: 2025-09-19T17:07:27.941Z
Learning: In app/src/main/java/app/gamenative/service/DownloadService.kt, the getSizeFromStoreDisplay() function uses runBlocking to call SteamService.getDownloadableDepots() because the latter was converted from synchronous to suspend in this PR. The user confirmed the previous synchronous implementation worked fine, and if performance is acceptable, keeping runBlocking is a valid approach.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this something we could also apply to other places such as Epic's offline launcher?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, it's initialized as a singleton on PluviaApp (so is always available) and should be importable by anything.

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.

Deduplicate network connectivity checks

2 participants