Skip to content

feat: implement global Force Offline Mode - #1765

Open
Meloon33 wants to merge 1 commit into
utkarshdalal:masterfrom
Meloon33:feat/complete-offline-mode
Open

feat: implement global Force Offline Mode#1765
Meloon33 wants to merge 1 commit into
utkarshdalal:masterfrom
Meloon33:feat/complete-offline-mode

Conversation

@Meloon33

@Meloon33 Meloon33 commented Jul 23, 2026

Copy link
Copy Markdown

Description

This PR introduces a global "Force Offline Mode" toggle in the Settings > Interface section. When enabled, the application acts as if it is completely disconnected from the internet, regardless of the system's actual network state.

Key Benefits:

  • Silences Services: Prevents Steam, Epic, GOG, and Amazon background services from starting or attempting reconnection.
  • Suppresses UI Noise: Hides the "Connecting to Steam..." banner and connection-related retry dialogs.
  • Instant Game Launch: Bypasses platform login handshakes (e.g., SteamUtils.awaitSteamLogin) during game launches, allowing for immediate booting when offline.
  • Improved Performance: Skips automatic update checks on app startup.

Technical Implementation:

  • Uses a reactive NetworkMonitor override to force hasInternet to false when active, ensuring all observing UI components adapt.
  • Adds safety guards (stopSelf) to platform services to catch accidental triggers.
  • Centralized state in PrefManager and MainViewModel.

Recording

(User provided: Skipping recording/GIF as per intent)

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements (Handheld/Offline portability)
  • Other (Preference-driven behavior change)

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).
  • I have attached a recording of the change. (Skipped)
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Disclaimer: This pull request was prepared and implemented with the assistance of Claude AI (via browser) and Gemini 3 Flash Preview (via Android Studio AI Agent).


Summary by cubic

Adds a global Force Offline Mode that makes the app behave fully offline, regardless of real connectivity. It silences platform services, hides connection noise, and lets games launch instantly.

  • New Features
    • Toggle in Settings > Interface; stored as PrefManager.forceOffline.
    • NetworkMonitor forces offline and pushes updates via update().
    • Blocks platform service startup; SteamService, EpicService, GOGService, AmazonService self-stop if invoked.
    • Suppresses connection banners and skips update checks in PluviaMain.
    • Bypasses platform login waits during game launch for faster startup.

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

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a Force Offline Mode toggle in Interface settings, with English and Spanish localization.
    • Forces the app to remain offline and suppresses connection status alerts.
    • Skips update checks and platform sign-in or service startup while enabled.
    • Applies immediately when toggled and persists across app launches.

@Meloon33
Meloon33 requested a review from utkarshdalal as a code owner July 23, 2026 08:58
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a persisted “Force Offline” setting that drives network and view-model state, updates settings and lifecycle behavior, suppresses connection activity, skips update checks, and stops Steam, Epic, GOG, and Amazon services when enabled.

Changes

Force Offline Mode

Layer / File(s) Summary
Offline preference and state propagation
.artifacts/.../implementation_plan.artifact.md, .artifacts/.../task.artifact.md, app/src/main/java/app/gamenative/PrefManager.kt, app/src/main/java/app/gamenative/NetworkMonitor.kt, app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
Adds the persisted forceOffline preference, exposes NetworkMonitor.update(), forces connectivity flags offline, and initializes and preserves offline view-model state.
Settings and application lifecycle
app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt, app/src/main/java/app/gamenative/ui/PluviaMain.kt, app/src/main/res/values*/strings.xml, .artifacts/.../pull_request_description.artifact.md, .artifacts/.../walkthrough.artifact.md
Adds the settings switch and English/Spanish strings, skips update checks, gates service startup, bypasses forced-offline launch waits, and hides the connection banner.
Platform service safety guards
app/src/main/java/app/gamenative/service/SteamService.kt, app/src/main/java/app/gamenative/service/amazon/AmazonService.kt, app/src/main/java/app/gamenative/service/epic/EpicService.kt, app/src/main/java/app/gamenative/service/gog/GOGService.kt
Each service stops itself with START_NOT_STICKY and a stopping foreground notification when forced offline mode is active.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsGroupInterface
  participant PrefManager
  participant NetworkMonitor
  participant PluviaMain
  participant PlatformServices
  SettingsGroupInterface->>PrefManager: save forceOffline
  SettingsGroupInterface->>NetworkMonitor: update connectivity state
  NetworkMonitor-->>NetworkMonitor: report no internet
  PluviaMain->>PrefManager: read forceOffline
  PrefManager-->>PluviaMain: return preference
  PluviaMain->>PlatformServices: skip startup and reconnection
  PlatformServices-->>PlatformServices: stop service with START_NOT_STICKY
Loading

Possibly related PRs

Suggested reviewers: utkarshdalal, jeremybernstein

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% 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 summarizes the main change: a global Force Offline Mode feature.
Description check ✅ Passed The description follows the template sections and includes the change summary, recording note, change type, and checklist items.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

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

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/ui/PluviaMain.kt (1)

363-377: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Apply forced-offline state to every game-launch path.

The new check only covers launchIntentApp. HomeScreen callbacks at Lines 1403, 1412, and 1440 still pass the route’s isOffline directly to preLaunchApp; if the route is offline=false, enabling Force Offline does not prevent online prelaunch/cloud-sync work. Pass one effective value such as PrefManager.forceOffline || isOffline through all launch callbacks.

🤖 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/PluviaMain.kt` around lines 363 - 377,
Update the HomeScreen launch callbacks around the preLaunchApp calls to pass an
effective offline value that combines PrefManager.forceOffline with each route’s
isOffline value. Apply this to all three callbacks near the existing launch
paths, preserving true whenever either forced-offline mode or the route already
indicates offline operation.
🤖 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
@.artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/implementation_plan.artifact.md:
- Around line 7-10: Keep the blockquote in the implementation plan contiguous by
removing the blank line between the IMPORTANT admonition content and the
following NOTE admonition, or prefixing that line with “>”. Ensure the
admonition formatting no longer triggers MD028.

In `@app/src/main/java/app/gamenative/NetworkMonitor.kt`:
- Around line 71-96: Serialize the entire NetworkMonitor.update recomputation,
including the forceOffline check and both _hasInternet/_hasWifiOrEthernet
assignments, using one shared lock or equivalent synchronization. Ensure
concurrent network callbacks and settings-triggered updates cannot interleave
and allow a stale update to overwrite the forced-offline state.

In `@app/src/main/java/app/gamenative/PrefManager.kt`:
- Around line 553-558: Propagate committed Force Offline preference transitions
across all affected sites: in
app/src/main/java/app/gamenative/PrefManager.kt:553-558, expose an observable or
suspending update from forceOffline; in
app/src/main/java/app/gamenative/ui/PluviaMain.kt:677-680, collect transitions
within repeatOnLifecycle instead of returning after the initial check; in
app/src/main/java/app/gamenative/service/SteamService.kt:3442-3443, disconnect
and cancel reconnect work when enabled; and in
app/src/main/java/app/gamenative/service/amazon/AmazonService.kt:792-798,
app/src/main/java/app/gamenative/service/epic/EpicService.kt:644-650, and
app/src/main/java/app/gamenative/service/gog/GOGService.kt:730-736, cancel
active sync or download work when enabled while preserving startup handling and
allowing services to restart when disabled.

In `@app/src/main/java/app/gamenative/service/SteamService.kt`:
- Around line 3442-3448: Process the `ACTION_EXIT` intent before the
`PrefManager.forceOffline` guard in `SteamService`, ensuring it emits
`AndroidEvent.EndProcess` before stopping the service. Keep the existing offline
behavior for other intents unchanged, and preserve the later `when
(intent?.action)` handling without allowing `ACTION_EXIT` to be skipped.

In `@app/src/main/java/app/gamenative/ui/model/MainViewModel.kt`:
- Around line 87-91: Update the offline-state flow around
MainViewModel.setOffline and PrefManager.forceOffline so changes to Force
Offline Mode propagate to the active ViewModel, not just during initial
construction. Make the preference observable or explicitly notify the ViewModel
whenever the setting changes, ensuring isOffline remains true while
force-offline is enabled and handleExitCloudSync cannot upload saves based on
stale state.

---

Outside diff comments:
In `@app/src/main/java/app/gamenative/ui/PluviaMain.kt`:
- Around line 363-377: Update the HomeScreen launch callbacks around the
preLaunchApp calls to pass an effective offline value that combines
PrefManager.forceOffline with each route’s isOffline value. Apply this to all
three callbacks near the existing launch paths, preserving true whenever either
forced-offline mode or the route already indicates offline operation.
🪄 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: 1fe20e54-1509-4ba4-a094-b50dac8be53f

📥 Commits

Reviewing files that changed from the base of the PR and between 986b249 and ecc04b1.

📒 Files selected for processing (15)
  • .artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/implementation_plan.artifact.md
  • .artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/pull_request_description.artifact.md
  • .artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/task.artifact.md
  • .artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/walkthrough.artifact.md
  • app/src/main/java/app/gamenative/NetworkMonitor.kt
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/service/SteamService.kt
  • app/src/main/java/app/gamenative/service/amazon/AmazonService.kt
  • app/src/main/java/app/gamenative/service/epic/EpicService.kt
  • app/src/main/java/app/gamenative/service/gog/GOGService.kt
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt
  • app/src/main/java/app/gamenative/ui/model/MainViewModel.kt
  • app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt
  • app/src/main/res/values-es/strings.xml
  • app/src/main/res/values/strings.xml

Comment on lines +7 to +10
> [!IMPORTANT]
> **Global Override:** When "Force Offline" is enabled, the app will report no internet connection to all internal components. This will prevent library syncing, workshop downloads, and cloud save uploads/downloads until disabled.

> [!NOTE]

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the admonition blockquote contiguous.

The blank line at Line 9 triggers MD028. Prefix it with > or close the blockquote before starting the next admonition.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 9-9: Blank line inside blockquote

(MD028, no-blanks-blockquote)

🤖 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
@.artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/implementation_plan.artifact.md
around lines 7 - 10, Keep the blockquote in the implementation plan contiguous
by removing the blank line between the IMPORTANT admonition content and the
following NOTE admonition, or prefixing that line with “>”. Ensure the
admonition formatting no longer triggers MD028.

Source: Linters/SAST tools

Comment on lines +71 to +96
fun update() {
if (PrefManager.forceOffline) {
_hasInternet.value = false
_hasWifiOrEthernet.value = false
return
}

val validatedCaps = networkCaps.values.filter {
it.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
val nonVpnCaps = validatedCaps.filter { !hasVpn(it) }
val nonVpnExists = networkCaps.values.any { !hasVpn(it) }
// trust VPN for internet only if a non-VPN network physically exists
// (guards against stale VPN after underlying WiFi drops;
// allows VPN in censorship scenarios where WiFi exists but isn't validated)
val vpnValidated = validatedCaps.any { hasVpn(it) }
_hasInternet.value = nonVpnCaps.isNotEmpty() || (vpnValidated && nonVpnExists)
// WiFi/Ethernet transport: only trust non-VPN networks (VPN reports stale transports).
// known edge case: censored WiFi + VPN → WiFi not validated → hasWifiOrEthernet=false,
// so "WiFi only" blocks downloads. user must disable "WiFi only" to download via VPN.
// fixing this would risk treating always-on VPN without real WiFi as valid.
_hasWifiOrEthernet.value = nonVpnCaps.any {
it.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
it.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)
}
}

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

Serialize network-state recomputation.

A callback that read forceOffline=false can finish after the settings-triggered call has set both flows to false, overwriting them back to online. This can leave forced offline ineffective until another network event arrives. Guard the full recomputation with one lock (or otherwise serialize updates).

🤖 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/NetworkMonitor.kt` around lines 71 - 96,
Serialize the entire NetworkMonitor.update recomputation, including the
forceOffline check and both _hasInternet/_hasWifiOrEthernet assignments, using
one shared lock or equivalent synchronization. Ensure concurrent network
callbacks and settings-triggered updates cannot interleave and allow a stale
update to overwrite the forced-offline state.

Comment on lines +553 to +558
private val FORCE_OFFLINE = booleanPreferencesKey("force_offline")
var forceOffline: Boolean
get() = getPref(FORCE_OFFLINE, false)
set(value) {
setPref(FORCE_OFFLINE, value)
}

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 | 🟠 Major | 🏗️ Heavy lift

Propagate Force Offline transitions, not just startup checks.

The preference write is asynchronous and the lifecycle/service checks only run during startup. Enabling Force Offline while services are active therefore does not stop downloads, sync jobs, or Steam reconnects; disabling it while the app remains STARTED does not restart services.

  • app/src/main/java/app/gamenative/PrefManager.kt#L553-L558: expose a committed observable/suspending preference update.
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt#L677-L680: collect preference transitions instead of returning once from repeatOnLifecycle.
  • app/src/main/java/app/gamenative/service/SteamService.kt#L3442-L3443: disconnect and cancel reconnect work when the setting becomes true.
  • app/src/main/java/app/gamenative/service/amazon/AmazonService.kt#L792-L798: cancel active sync/download work on transition.
  • app/src/main/java/app/gamenative/service/epic/EpicService.kt#L644-L650: cancel active sync work on transition.
  • app/src/main/java/app/gamenative/service/gog/GOGService.kt#L730-L736: cancel active sync work on transition.
📍 Affects 6 files
  • app/src/main/java/app/gamenative/PrefManager.kt#L553-L558 (this comment)
  • app/src/main/java/app/gamenative/ui/PluviaMain.kt#L677-L680
  • app/src/main/java/app/gamenative/service/SteamService.kt#L3442-L3443
  • app/src/main/java/app/gamenative/service/amazon/AmazonService.kt#L792-L798
  • app/src/main/java/app/gamenative/service/epic/EpicService.kt#L644-L650
  • app/src/main/java/app/gamenative/service/gog/GOGService.kt#L730-L736
🤖 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/PrefManager.kt` around lines 553 - 558,
Propagate committed Force Offline preference transitions across all affected
sites: in app/src/main/java/app/gamenative/PrefManager.kt:553-558, expose an
observable or suspending update from forceOffline; in
app/src/main/java/app/gamenative/ui/PluviaMain.kt:677-680, collect transitions
within repeatOnLifecycle instead of returning after the initial check; in
app/src/main/java/app/gamenative/service/SteamService.kt:3442-3443, disconnect
and cancel reconnect work when enabled; and in
app/src/main/java/app/gamenative/service/amazon/AmazonService.kt:792-798,
app/src/main/java/app/gamenative/service/epic/EpicService.kt:644-650, and
app/src/main/java/app/gamenative/service/gog/GOGService.kt:730-736, cancel
active sync or download work when enabled while preserving startup handling and
allowing services to restart when disabled.

Comment on lines +3442 to +3448
if (PrefManager.forceOffline) {
Timber.i("[SteamService]: Force Offline Mode active - stopping service")
val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_STEAM, "Stopping...")
startForeground(NotificationHelper.NOTIFICATION_ID_STEAM, notification)
stopSelf()
return START_NOT_STICKY
}

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

Handle ACTION_EXIT before the offline guard.

This return runs before the when (intent?.action) block at Lines 3460–3468, so tapping an existing service notification’s Exit action while Force Offline is enabled only stops SteamService; it never emits AndroidEvent.EndProcess.

🤖 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/SteamService.kt` around lines 3442 -
3448, Process the `ACTION_EXIT` intent before the `PrefManager.forceOffline`
guard in `SteamService`, ensuring it emits `AndroidEvent.EndProcess` before
stopping the service. Keep the existing offline behavior for other intents
unchanged, and preserve the later `when (intent?.action)` handling without
allowing `ACTION_EXIT` to be skipped.

Comment on lines +87 to +91
private val _offline = MutableStateFlow(PrefManager.forceOffline)
val isOffline: StateFlow<Boolean> get() = _offline

fun setOffline(value: Boolean) {
_offline.value = value
_offline.value = value || PrefManager.forceOffline

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

Propagate force-offline changes into the active ViewModel.

This is only an initial snapshot; toggling the setting later does not update _offline. An already-running game can therefore reach handleExitCloudSync() with isOffline=false and upload saves after Force Offline Mode was enabled. Make the preference observable or explicitly notify the ViewModel when the switch changes.

🤖 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/model/MainViewModel.kt` around lines 87 -
91, Update the offline-state flow around MainViewModel.setOffline and
PrefManager.forceOffline so changes to Force Offline Mode propagate to the
active ViewModel, not just during initial construction. Make the preference
observable or explicitly notify the ViewModel whenever the setting changes,
ensuring isOffline remains true while force-offline is enabled and
handleExitCloudSync cannot upload saves based on stale state.

@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 across 15 files

Prompt for AI agents (unresolved issues)

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


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

<violation number="1" location="app/src/main/java/app/gamenative/service/epic/EpicService.kt:646">
P3: Hardcoded notification string "Stopping..." should be moved to strings.xml for localization support. The team prefers all user-facing text to go into resource files.</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:3444">
P2: User-facing notification text `"Stopping..."` is hardcoded inline. For localization and maintainability, define it as a string resource in `strings.xml` and reference it via `getString(R.string.steam_service_stopping)` — consistent with how other notification messages in this file use `getString(R.string.download_paused_wifi)`.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/amazon/AmazonService.kt:794">
P2: "Stopping..." notification text is hardcoded instead of using a string resource. Move it to strings.xml and reference it via getString(R.string. ...) for localization support.

(Based on your team's feedback about hardcoded UI strings.) [FEEDBACK_USED]</violation>
</file>

<file name="app/src/main/java/app/gamenative/ui/model/MainViewModel.kt">

<violation number="1" location="app/src/main/java/app/gamenative/ui/model/MainViewModel.kt:87">
P1: Read-once snapshot of PrefManager.forceOffline at init creates a stale-state window: if the user toggles Force Offline in Settings while MainViewModel is alive (same-activity navigation), _offline won't reactively update. Consider wrapping the preference in a Flow (e.g., dataStore.data.map) and collecting it in init { } so _offline stays in sync whenever the preference changes.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/ui/PluviaMain.kt:677">
P1: Toggling Force Offline OFF while in the foreground won't restart platform services until the next lifecycle transition (e.g., background → foreground). `repeatOnLifecycle` only re-runs the block when the lifecycle state enters STARTED — it doesn't react to `PrefManager.forceOffline` changes. Since `LaunchedEffect(Unit)` never restarts either, the user has to background/foreground the app to restore service connections after disabling offline mode. This contradicts the PR's goal of clean restoration.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/PluviaMain.kt:1286">
P2: An existing “Connecting to Steam…” banner can remain on screen after Force Offline Mode is enabled because this direct preference read is not observable by Compose. The banner condition would need to consume a reactive force-offline state (or an explicitly emitted UI event) so the visibility is recomputed immediately.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/service/gog/GOGService.kt:732">
P3: `</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt:361">
P2: Changing the switch does not reliably propagate the new offline state immediately because the preference write is asynchronous but `NetworkMonitor.update()` reads the preference synchronously on the following line. The network state can remain stale after either direction of the toggle; passing the new value directly to the monitor or maintaining a synchronous in-memory override would make the transition atomic.</violation>

<violation number="2" location="app/src/main/java/app/gamenative/ui/screen/settings/SettingsGroupInterface.kt:362">
P2: The toggle triggers a blocking DataStore read on the main thread. `NetworkMonitor.update()` reads back `PrefManager.forceOffline` via `getPref()`, which runs `runBlocking { dataStore.data.first() }` — blocking the main thread on each toggle. The MainViewModel's `_offline` flow (initialized once at creation) also won't reflect this toggle change, creating a stale state consumers of `MainViewModel.isOffline` won't see. Consider passing the known value directly to avoid the blocking read (`NetworkMonitor.onForceOfflineChanged(newValue)`) and connecting this toggle to MainViewModel's offline flow for consistency.</violation>
</file>

<file name=".artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/implementation_plan.artifact.md">

<violation number="1" location=".artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/implementation_plan.artifact.md:17">
P3: The `file:///E:/workspace/StudioProjects/GameNative/...` absolute paths in the file references won't resolve for any other developer or on CI. Since these are just reference annotations in markdown, consider either removing the URI portion entirely (the descriptive text in brackets already identifies each file) or using a relative path from the repo root. This keeps the plan portable and avoids confusion when someone follows a dead link.</violation>
</file>

<file name=".artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/walkthrough.artifact.md">

<violation number="1" location=".artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/walkthrough.artifact.md:21">
P2: The companion `start()` methods for EpicService, GOGService, and AmazonService call `startForegroundService()` before the `onStartCommand` guard fires. This causes a "Stopping..." notification to briefly flash when force offline is active. Add `if (PrefManager.forceOffline) return` at the top of each `start()` method so the service is never started and no notification is shown.</violation>
</file>

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

<violation number="1" location="app/src/main/res/values/strings.xml:1088">
P3: The setting subtitle says it disables all network connections, but this feature only forces the app's observed connectivity state and suppresses selected services/alerts; other network requests can still be made. A less misleading description such as “Make the app behave as offline and suppress connection alerts” would match the actual behavior in both locales.</violation>
</file>

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

<violation number="1" location="app/src/main/java/app/gamenative/NetworkMonitor.kt:71">
P2: Race condition: `update()` reads `PrefManager.forceOffline` and then writes to the state flows without any synchronization. A network callback that entered `update()` before the toggle was set can finish after the settings-triggered `update()` call, overwriting `_hasInternet` back to `true`. Consider serializing state recomputation (e.g., with a `Mutex` or by confining updates to a single-threaded dispatcher) so that a concurrent network callback cannot clobber the forced-offline state.</violation>
</file>

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

Re-trigger cubic

val uiEvent = _uiEvent.receiveAsFlow()

private val _offline = MutableStateFlow(false)
private val _offline = MutableStateFlow(PrefManager.forceOffline)

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.

P1: Read-once snapshot of PrefManager.forceOffline at init creates a stale-state window: if the user toggles Force Offline in Settings while MainViewModel is alive (same-activity navigation), _offline won't reactively update. Consider wrapping the preference in a Flow (e.g., dataStore.data.map) and collecting it in init { } so _offline stays in sync whenever the preference changes.

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/model/MainViewModel.kt, line 87:

<comment>Read-once snapshot of PrefManager.forceOffline at init creates a stale-state window: if the user toggles Force Offline in Settings while MainViewModel is alive (same-activity navigation), _offline won't reactively update. Consider wrapping the preference in a Flow (e.g., dataStore.data.map) and collecting it in init { } so _offline stays in sync whenever the preference changes.</comment>

<file context>
@@ -84,11 +84,11 @@ class MainViewModel @Inject constructor(
     val uiEvent = _uiEvent.receiveAsFlow()
 
-    private val _offline = MutableStateFlow(false)
+    private val _offline = MutableStateFlow(PrefManager.forceOffline)
     val isOffline: StateFlow<Boolean> get() = _offline
 
</file context>


LaunchedEffect(Unit) {
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
if (PrefManager.forceOffline) {

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.

P1: Toggling Force Offline OFF while in the foreground won't restart platform services until the next lifecycle transition (e.g., background → foreground). repeatOnLifecycle only re-runs the block when the lifecycle state enters STARTED — it doesn't react to PrefManager.forceOffline changes. Since LaunchedEffect(Unit) never restarts either, the user has to background/foreground the app to restore service connections after disabling offline mode. This contradicts the PR's goal of clean restoration.

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/PluviaMain.kt, line 677:

<comment>Toggling Force Offline OFF while in the foreground won't restart platform services until the next lifecycle transition (e.g., background → foreground). `repeatOnLifecycle` only re-runs the block when the lifecycle state enters STARTED — it doesn't react to `PrefManager.forceOffline` changes. Since `LaunchedEffect(Unit)` never restarts either, the user has to background/foreground the app to restore service connections after disabling offline mode. This contradicts the PR's goal of clean restoration.</comment>

<file context>
@@ -673,6 +674,11 @@ fun PluviaMain(
 
     LaunchedEffect(Unit) {
         lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+            if (PrefManager.forceOffline) {
+                Timber.i("[PluviaMain]: Force Offline Mode active - skipping platform service startup")
+                return@repeatOnLifecycle
</file context>

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (PrefManager.forceOffline) {
Timber.i("[SteamService]: Force Offline Mode active - stopping service")
val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_STEAM, "Stopping...")

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: User-facing notification text "Stopping..." is hardcoded inline. For localization and maintainability, define it as a string resource in strings.xml and reference it via getString(R.string.steam_service_stopping) — consistent with how other notification messages in this file use getString(R.string.download_paused_wifi).

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

<comment>User-facing notification text `"Stopping..."` is hardcoded inline. For localization and maintainability, define it as a string resource in `strings.xml` and reference it via `getString(R.string.steam_service_stopping)` — consistent with how other notification messages in this file use `getString(R.string.download_paused_wifi)`.</comment>

<file context>
@@ -3439,6 +3439,13 @@ class SteamService : Service(), IChallengeUrlChanged {
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (PrefManager.forceOffline) {
+            Timber.i("[SteamService]: Force Offline Mode active - stopping service")
+            val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_STEAM, "Stopping...")
+            startForeground(NotificationHelper.NOTIFICATION_ID_STEAM, notification)
+            stopSelf()
</file context>

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (PrefManager.forceOffline) {
Timber.i("[AmazonService] Force Offline Mode active - stopping service")
val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_AMAZON, "Stopping...")

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: "Stopping..." notification text is hardcoded instead of using a string resource. Move it to strings.xml and reference it via getString(R.string. ...) for localization support.

(Based on your team's feedback about hardcoded UI strings.)

View Feedback

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/amazon/AmazonService.kt, line 794:

<comment>"Stopping..." notification text is hardcoded instead of using a string resource. Move it to strings.xml and reference it via getString(R.string. ...) for localization support.

(Based on your team's feedback about hardcoded UI strings.) </comment>

<file context>
@@ -788,6 +789,13 @@ class AmazonService : Service() {
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (PrefManager.forceOffline) {
+            Timber.i("[AmazonService] Force Offline Mode active - stopping service")
+            val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_AMAZON, "Stopping...")
+            startForeground(NotificationHelper.NOTIFICATION_ID_AMAZON, notification)
+            stopSelf()
</file context>

onCheckedChange = { newValue ->
forceOffline = newValue
PrefManager.forceOffline = newValue
NetworkMonitor.update()

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 toggle triggers a blocking DataStore read on the main thread. NetworkMonitor.update() reads back PrefManager.forceOffline via getPref(), which runs runBlocking { dataStore.data.first() } — blocking the main thread on each toggle. The MainViewModel's _offline flow (initialized once at creation) also won't reflect this toggle change, creating a stale state consumers of MainViewModel.isOffline won't see. Consider passing the known value directly to avoid the blocking read (NetworkMonitor.onForceOfflineChanged(newValue)) and connecting this toggle to MainViewModel's offline flow for consistency.

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/screen/settings/SettingsGroupInterface.kt, line 362:

<comment>The toggle triggers a blocking DataStore read on the main thread. `NetworkMonitor.update()` reads back `PrefManager.forceOffline` via `getPref()`, which runs `runBlocking { dataStore.data.first() }` — blocking the main thread on each toggle. The MainViewModel's `_offline` flow (initialized once at creation) also won't reflect this toggle change, creating a stale state consumers of `MainViewModel.isOffline` won't see. Consider passing the known value directly to avoid the blocking read (`NetworkMonitor.onForceOfflineChanged(newValue)`) and connecting this toggle to MainViewModel's offline flow for consistency.</comment>

<file context>
@@ -349,6 +350,19 @@ fun SettingsGroupInterface(
+            onCheckedChange = { newValue ->
+                forceOffline = newValue
+                PrefManager.forceOffline = newValue
+                NetworkMonitor.update()
+            },
+        )
</file context>

private fun hasVpn(caps: NetworkCapabilities) =
caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)

fun update() {

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: Race condition: update() reads PrefManager.forceOffline and then writes to the state flows without any synchronization. A network callback that entered update() before the toggle was set can finish after the settings-triggered update() call, overwriting _hasInternet back to true. Consider serializing state recomputation (e.g., with a Mutex or by confining updates to a single-threaded dispatcher) so that a concurrent network callback cannot clobber the forced-offline state.

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/NetworkMonitor.kt, line 71:

<comment>Race condition: `update()` reads `PrefManager.forceOffline` and then writes to the state flows without any synchronization. A network callback that entered `update()` before the toggle was set can finish after the settings-triggered `update()` call, overwriting `_hasInternet` back to `true`. Consider serializing state recomputation (e.g., with a `Mutex` or by confining updates to a single-threaded dispatcher) so that a concurrent network callback cannot clobber the forced-offline state.</comment>

<file context>
@@ -88,4 +59,39 @@ object NetworkMonitor {
+    private fun hasVpn(caps: NetworkCapabilities) =
+        caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
+
+    fun update() {
+        if (PrefManager.forceOffline) {
+            _hasInternet.value = false
</file context>

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (PrefManager.forceOffline) {
Timber.tag("EPIC").i("Force Offline Mode active - stopping service")
val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_EPIC, "Stopping...")

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: Hardcoded notification string "Stopping..." should be moved to strings.xml for localization support. The team prefers all user-facing text to go into resource files.

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/epic/EpicService.kt, line 646:

<comment>Hardcoded notification string "Stopping..." should be moved to strings.xml for localization support. The team prefers all user-facing text to go into resource files.</comment>

<file context>
@@ -640,6 +641,13 @@ class EpicService : Service() {
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (PrefManager.forceOffline) {
+            Timber.tag("EPIC").i("Force Offline Mode active - stopping service")
+            val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_EPIC, "Stopping...")
+            startForeground(NotificationHelper.NOTIFICATION_ID_EPIC, notification)
+            stopSelf()
</file context>

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (PrefManager.forceOffline) {
Timber.i("[GOGService] Force Offline Mode active - stopping service")
val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_GOG, "Stopping...")

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

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/gog/GOGService.kt, line 732:

<comment>`</comment>

<file context>
@@ -726,6 +727,13 @@ class GOGService : Service() {
     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        if (PrefManager.forceOffline) {
+            Timber.i("[GOGService] Force Offline Mode active - stopping service")
+            val notification = notificationHelper.createServiceNotification(NotificationHelper.NOTIFICATION_ID_GOG, "Stopping...")
+            startForeground(NotificationHelper.NOTIFICATION_ID_GOG, notification)
+            stopSelf()
</file context>


### Core Logic & State

#### [MODIFY] [PrefManager.kt](file:///E:/workspace/StudioProjects/GameNative/app/src/main/java/app/gamenative/PrefManager.kt)

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 file:///E:/workspace/StudioProjects/GameNative/... absolute paths in the file references won't resolve for any other developer or on CI. Since these are just reference annotations in markdown, consider either removing the URI portion entirely (the descriptive text in brackets already identifies each file) or using a relative path from the repo root. This keeps the plan portable and avoids confusion when someone follows a dead link.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .artifacts/52d073f8-a39e-4a31-878e-015eb94da79a/implementation_plan.artifact.md, line 17:

<comment>The `file:///E:/workspace/StudioProjects/GameNative/...` absolute paths in the file references won't resolve for any other developer or on CI. Since these are just reference annotations in markdown, consider either removing the URI portion entirely (the descriptive text in brackets already identifies each file) or using a relative path from the repo root. This keeps the plan portable and avoids confusion when someone follows a dead link.</comment>

<file context>
@@ -0,0 +1,59 @@
+
+### Core Logic & State
+
+#### [MODIFY] [PrefManager.kt](file:///E:/workspace/StudioProjects/GameNative/app/src/main/java/app/gamenative/PrefManager.kt)
+- Add `forceOffline` boolean preference (default: `false`).
+
</file context>

<string name="settings_interface_show_gamepad_hints_title">Show controller hints</string>
<string name="settings_interface_show_gamepad_hints_subtitle">Show the controller button hints bar at the bottom of the screen</string>
<string name="settings_interface_force_offline_title">Force Offline Mode</string>
<string name="settings_interface_force_offline_subtitle">Disable all network connections and suppress connection alerts</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.

P3: The setting subtitle says it disables all network connections, but this feature only forces the app's observed connectivity state and suppresses selected services/alerts; other network requests can still be made. A less misleading description such as “Make the app behave as offline and suppress connection alerts” would match the actual behavior in both locales.

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

<comment>The setting subtitle says it disables all network connections, but this feature only forces the app's observed connectivity state and suppresses selected services/alerts; other network requests can still be made. A less misleading description such as “Make the app behave as offline and suppress connection alerts” would match the actual behavior in both locales.</comment>

<file context>
@@ -1084,6 +1084,8 @@
     <string name="settings_interface_show_gamepad_hints_title">Show controller hints</string>
     <string name="settings_interface_show_gamepad_hints_subtitle">Show the controller button hints bar at the bottom of the screen</string>
+    <string name="settings_interface_force_offline_title">Force Offline Mode</string>
+    <string name="settings_interface_force_offline_subtitle">Disable all network connections and suppress connection alerts</string>
     <string name="settings_interface_icon_style">Icon style</string>
     <string name="settings_interface_custom_games">Custom Games</string>
</file context>

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