Skip to content

fix(NetworkChangeManager): classify transport from activeNetwork, not per-callback caps#903

Draft
torlando-agent[bot] wants to merge 2 commits into
mainfrom
columba-suite/issue-898-networkchangemanager-classify-from-active-network
Draft

fix(NetworkChangeManager): classify transport from activeNetwork, not per-callback caps#903
torlando-agent[bot] wants to merge 2 commits into
mainfrom
columba-suite/issue-898-networkchangemanager-classify-from-active-network

Conversation

@torlando-agent
Copy link
Copy Markdown
Contributor

@torlando-agent torlando-agent Bot commented May 6, 2026

Implements the approved plan for #898.

Summary

NetworkChangeManager.onCapabilitiesChanged at app/src/main/java/network/columba/app/service/manager/NetworkChangeManager.kt:104 calls emitTransportIfChanged(currentTransportOf(networkCapabilities)), classifying the transport from the per-callback NetworkCapabilities. The NetworkCallback is registered against NET_CAPABILITY_INTERNET, which fires for every network matching the request — not just the system-default route. On a dual-radio device (Wi-Fi default + cellular kept up as backup) the cellular network's capability update gets classified as CELLULAR and emitted as a transport change while Wi-Fi remains the active route.

Currently latent: ServiceModule.createManagers is invoked from ReticulumService.onCreate (ReticulumService.kt:59) without an onTransportChanged consumer, so the misclassified emission goes nowhere. Should be fixed before any service-process consumer relies on the callback.

Changes

  • app/src/main/java/network/columba/app/service/manager/NetworkChangeManager.ktonCapabilitiesChanged now classifies the transport via the existing currentTransportOf(connectivityManager) helper (reads connectivityManager.activeNetwork then its capabilities), mirroring the PR feat(InterfaceConfig): per-interface Wi-Fi/cellular network restriction #896 fix in InterfaceTransportObserver. The per-callback network/networkCapabilities parameters remain (still feed the Log.v line and are required by the framework signature). Comment block above the call was rewritten to describe the activeNetwork-classification rationale.
  • app/src/test/java/network/columba/app/service/manager/NetworkChangeManagerTest.kt — capability-driven tests now stub connectivityManager.activeNetwork and connectivityManager.getNetworkCapabilities(network) before each onCapabilitiesChanged(...) invocation. Added onCapabilitiesChanged cellular backup on wifi default does not flip transport covering the dual-radio repro.

Test plan

  • ./gradlew :app:testNoSentryDebugUnitTest --tests "network.columba.app.service.manager.NetworkChangeManagerTest" — 18/18 pass, including the new dual-radio repro.
  • ./gradlew ktlintCheck detekt cpdCheck --continue — BUILD SUCCESSFUL on this branch's diff.
  • ./gradlew :app:assembleNoSentryDebug — APK built clean for install verification.
  • Manual on-device sanity: not exercised here; the path is latent until a service-process consumer wires onTransportChanged. Behaviour change is purely classification source, observable only through the existing unit harness.

Implementer notes

  • The activeNetwork read can race with the callback delivery (Android delivers NetworkCallback on its own ConnectivityThread, and activeNetwork is the OS's atomic snapshot). If the active route flipped between callback firing and our read, we just see the newer value, which is also correct; the next callback re-classifies. Same race the PR feat(InterfaceConfig): per-interface Wi-Fi/cellular network restriction #896 InterfaceTransportObserver fix accepts.
  • The network: Network and networkCapabilities: NetworkCapabilities parameters on onCapabilitiesChanged stay — still feed the Log.v debug line and are required by the framework signature. Production now ignores networkCapabilities for classification but the parameter is still semantically there.
  • The plan referenced an "Audit trail in the vault REJECTIONS.md for issue 894" filed by greptile-iterator — that file does not exist in the vault. Not load-bearing for this fix; flagging in case anyone hunts for it later.

🤖 Generated with Claude Code

… per-callback caps

`onCapabilitiesChanged` was calling `currentTransportOf(networkCapabilities)`,
classifying off the per-callback caps. The `NetworkCallback` is registered
against `NET_CAPABILITY_INTERNET`, which fires for every network matching
the request — not just the system-default route. On a dual-radio device
(Wi-Fi default + cellular kept up as backup) the cellular network's
capability update was getting classified as `CELLULAR` and emitted as a
transport change while Wi-Fi remained the active route.

Switch to the existing `currentTransportOf(connectivityManager)` helper
that resolves `connectivityManager.activeNetwork` first — same fix PR #896
applied to `InterfaceTransportObserver`. The activeNetwork read can race
with callback delivery, but the OS snapshot is always self-consistent and
the next callback re-classifies if the active route flipped between the
two reads.

Updates the existing capability-driven tests to stub `activeNetwork` +
`getNetworkCapabilities(network)` and adds a dual-radio repro test that
fails before the fix.

Closes #898

Co-Authored-By: Claude claude-opus-4-7 <noreply@anthropic.com>
@sentry
Copy link
Copy Markdown
Contributor

sentry Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This PR's implementer pass inadvertently reverted the terminal-state
matcher in `viewModel init triggers startServer ...` (commit d1ca233,
PR #891 / issue #883). The restored strict `assertFalse(isServerRunning)`
flaked on the very next CI run on Linux — exactly the behaviour PR #891
documented and fixed: `getLocalIpAddress()` returns a non-loopback IPv4
in the GH Actions Ubuntu image, the server actually binds, the assertion
flips red. This change is also wholly out of scope for the issue #898
plan, which only touches `NetworkChangeManager`. Restore main's version
verbatim; verified locally.

Co-Authored-By: Claude claude-opus-4-7 <noreply@anthropic.com>
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.

0 participants