feat(android): device location sync, GMS mock support, and mobile transport hardening - #479
Merged
Ryanmello07 merged 2 commits intoSep 6, 2026
Conversation
…nsport hardening - Decouple mock location feeder into an application-scoped singleton (MockLocationFeeder) isolated strictly to active client tunnels, with 10s provider debounce and generation tracking. - Declare ACCESS_COARSE_LOCATION (without fine location or hardware GPS requirement) and gate GMS FusedLocationProviderClient mock mode on runtime grant, resolving Google Chrome leaks on stock Android without exposing precise GPS. - Add aligned URSwitch toggle and dynamic status feedback to Settings screen, matching design system vertical alignment and spacing. - Introduce interactive permission step and zero-ambiguity orphaned mock provider recovery flow with direct developer options shortcut in MockLocationGuideScreen. - Deduplicate contract status logging and reset cached state across device teardown/initialization.
full-bars
force-pushed
the
fix/location-sync-and-transport-hardening
branch
from
September 6, 2026 04:59
fc4ff9c to
b9bbb2c
Compare
Review fixes on top of urnetwork#479. The feature and its architecture are the contributor's; these are the corrections that came out of reviewing it. B1 The permission gate sat above the whole engine. resolveMockLocationStatus returned NEEDS_LOCATION_PERMISSION before it could return ACTIVE, and reconcile() gates all arming on ACTIVE, so a missing grant stopped arm() entirely -- including the AOSP test providers, which need no runtime permission at all (MOCKLOCATION.md 8). Every GMS-flavor user with the toggle already on would have lost the feature silently on upgrade, since the permission was tools:node="remove"-blocked before this PR and nobody has it granted. The grant now gates only the optional FLP mirror, which is what 3.2 always said it should: an optional, gracefully-degrading enhancement. NEEDS_LOCATION_PERMISSION stays as an advisory the guide reads; the resolver no longer returns it. B2 src/main merges into all four flavors, so the F-Droid build shipped a location permission src/ungoogle can never use. src/github now removes it. Restored the ACCESS_FINE_LOCATION and hardware.location.gps guards the PR deleted without needing to (dac1d14 put them there deliberately), and declared hardware.location not-required, since COARSE otherwise implies it as required and Play would filter out the TVs the block above supports. B4 arm() is the only thing that reclaims orphaned test providers, and reconcile() could only reach disarm() when already posting. With B1 that was reachable: providers registered, nothing able to reclaim them, device location frozen for every app. Startup now reclaims whenever the resolved status is not ACTIVE, and disarm() also runs when a claimed provider set survives. disarm() only logs when it really stopped a posting run, so the orphaned retry path does not spam. B5 MOCKLOCATION.md said the opposite of what ships in six places and is cited by section number from nine source files. Reconciled, with 6.7 added for the grace window; nothing renumbered. Whether GMS enforces the permission at runtime is still open -- the vendor prose names only ACCESS_MOCK_LOCATION while the annotation is CLASS-retention -- and is recorded as open. Getting it wrong is now harmless, which is the point of B1. Also: consume the permission result and route to App info once the system dialog will not appear again; give the section's ORPHANED row its weight so the chevron measures; add an ERROR_TRANSIENT arm to the settings subtitle; throttle the 1 Hz setMockLocation failure log; drop the PR's @PARAM KDoc, which four files in ui/ have and all four are this PR's; extract the grace window into a pure MockLocationGracePolicy so it is testable at all, with the 10s figure derived from 6.1 rather than asserted. Known, and blocking a release rather than this branch: the new strings are hand-written into generated files. build.sh regenerates res/values*/strings.xml from ../localizations before every pipeline build, so the seven mock_location_* keys must land in the store first or a regeneration drops them. CI cannot catch this -- the workflow says so at build-and-test.yml:31. The locale edits an earlier pass made were reverted for that reason; Android falls back to values/ for a missing key, so English-only is the safe state until the store syncs. Verified locally against a from-scratch toolchain (JDK 21, SDK 36, NDK 29.0.14206865, gradle 9.5.1, locally built URnetworkSdk.aar): testGithubDebugUnitTest 313 tests, 0 failures (was 296) compile{Github,Play,Solana_dapp,Ethos_dapp}ReleaseKotlin ok assembleGithubDebug ok merged manifest, per flavor github COARSE absent; play/solana/ethos present; FINE absent everywhere That last one is the check CI never runs: it builds only assembleGithubDebug plus compile*ReleaseKotlin, and compileXReleaseKotlin never runs processXReleaseManifest, so the flavor permission set is unverified upstream. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VYp99vhW9soGoTmNGVYJk
Ryanmello07
added a commit
that referenced
this pull request
Sep 6, 2026
Generated output of urnetwork/localizations#<store PR>; no hand edits. `npm run gen:android` wrote all 19 res/values*/strings.xml. #479 added seven mock_location_* strings directly to res/values/strings.xml. These files are generated, and build.sh regenerates from ../localizations before every pipeline build, so the release build dropped all seven and failed on nine unresolved R.string.mock_location_* references across MockLocationGuideScreen.kt, MockLocationSection.kt and SettingsScreen.kt. CI stayed green throughout: it runs gradle directly and never runs the generator, which build-and-test.yml:31 already documents as a known gap. Reproduced on main before the store keys landed -- `npm run gen:android` took the mock_location_* set from 24 keys to 17 and :app:compileGithubReleaseKotlin failed. With the keys in the store the same regeneration is clean. The seven keys now arrive translated in all 18 locales rather than English only, and mock_location_error_cleanup_required drops out: the ORPHANED copy moved to the _stuck_ strings and it has no kotlin references left, so it is retired in the store. Verified after regeneration: 345 unit tests, 0 failures; github, play, solana_dapp and ethos_dapp release variants all compile; assembleGithubDebug packages; merged manifests unchanged (github still has no ACCESS_COARSE_LOCATION, the three GMS flavors still do, no ACCESS_FINE_LOCATION anywhere). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019VYp99vhW9soGoTmNGVYJk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR addresses critical mobile transport reliability issues and completes the location synchronization architecture on URNetwork Connect for Android:
MockLocationFeeder), strictly isolates simulation to active client connections, adds an alignedURSwitchtoggle toSettingsScreen.kt, debounces provider reconnects with generation-tracked grace periods, and provides zero-ambiguity recovery for orphaned mock provider states.ACCESS_COARSE_LOCATION) and establishing an interactive runtime grant flow so Google Play Services'FusedLocationProviderClientaccepts mock mode.Important
Mobile VPN Capture & IPv6 Blackholing
When the VPN tunnel runs in capture mode with the Kill Switch enabled, Android intentionally blackholes IPv6 traffic to prevent leaks. The transport recovery policy and DNS-over-HTTPS (DoH) connection lifetimes now cleanly reconcile across physical radio changes rather than stalling on dead sockets or unrouteable IPv6 addresses.
Important
The Android Location Subsystem Split: Why Some Devices Require Location Permissions
A core subtlety of Android is that two completely different location architectures exist depending on whether Google Play Services (GMS) is present:
De-Googled / AOSP Android (GrapheneOS, CalyxOS, LineageOS, F-Droid builds):
LocationManager(gps,network, andfused).addTestProvider,setTestProviderLocation) requires zero runtime permissions. Security is governed exclusively by the system AppOp (android:mock_location) granted via Developer options > Select mock location app.Stock Android with Google Play Services (Pixel, Samsung, Motorola, etc.):
FusedLocationProviderClient(FLP).com.google.android.gms, Google enforces that any process invokingsetMockMode(true)orsetMockLocation()must holdACCESS_COARSE_LOCATIONorACCESS_FINE_LOCATION.tools:node="remove"), Google Play Services threwSecurityException: uid does not have any of [android.permission.ACCESS_COARSE_LOCATION, android.permission.ACCESS_FINE_LOCATION], rejected mock mode, and served Chrome the phone's hardware GPS.Dynamic Adaptation in URnetwork:
The engine dynamically inspects the host environment via
supportsFusedMockLocation(context):ACCESS_COARSE_LOCATION, Android 12+ presents a simplified "Approximate location only" prompt without ever offering or exposing precise hardware GPS (ACCESS_FINE_LOCATION). This fully satisfies Google Play Services FLP Binder IPC checks while guaranteeing zero access to high-accuracy device GPS.Tip
Privacy Invariant: One-Way Outbound Write Only
URnetwork never queries or reads the physical device location. There are zero calls to
requestLocationUpdates(),getLastKnownLocation(), orgetCurrentLocation(). Location data flows strictly outbound: coordinates from the authenticated exit provider are injected into the mock location engine.Tip
Toggling the switch ON when setup is incomplete (e.g., Developer options not enabled, mock location app not selected, or GMS permission missing) automatically routes the user directly to
Route.MockLocationGuide. Once prerequisites are satisfied, the toggle controls active location simulation in-place.Logical Impact & Changes
Stability & Transport Hardening
MainApplication.kt):MainApplication.kt&DeviceLocal.kt):MockLocationFeeder.kt):@Singleton MockLocationFeederstarted inMainApplication.onCreate().DeviceLocal.addTunnelChangeListenerandDeviceLocal.addConnectChangeListener.isClientConnected = device.connectEnabled && device.tunnelStarted. If Provide Mode is running (e.g. Provide mode = Always), location mocking remains disarmed.@Synchronizedlifecycle methods and listener callbacks withcurrentDeviceidentity validation to prevent race conditions during device swaps.Bug Fixes & Location Hardening
AndroidManifest.xml):ACCESS_COARSE_LOCATIONand declaredhardware.location.gpsasrequired="false", omittingACCESS_FINE_LOCATION. This satisfies GMS FLP Binder IPC checks without triggering precise location prompts or Play Store privacy scrutiny.MockLocationState.kt&MockLocationEligibility.kt):hasLocationPermission(context)helper andNEEDS_LOCATION_PERMISSIONstatus, ensuring GMS FLP mock mode only attempts arming when authorized.locationPermissionGrantedtofalseinresolveMockLocationStatuswhile maintaining existing behavior whenrequiresLocationPermission = false.MockLocationFeeder.kt):graceGeneration) so brief network reconnects or provider grid re-dials do not prematurely disarm mock GPS and reveal the physical location.MockLocationFeeder.kt):Deviceinstance (currentDevice !== device), ensuring clean state resets on device replacement.Features & UI Alignment
SettingsScreen.kt):URSwitchto the "Device location sync" row, matching vertical alignment, sizing, and spacing of "Kill switch" and "Cellular network".Route.MockLocationGuide.Syncing with <City>,Setup required,Waiting for provider location, orGPS frozen — action needed).MockLocationGuideScreen.kt):ActivityResultContracts.RequestMultiplePermissions()on devices where GMS FLP mocking requires it.URSwitchin the "Ready" state banner once all system prerequisites are met.Syncing with <City>) or waiting state.state.status != MockLocationStatus.ORPHANEDso contradictory green waiting text is suppressed during orphaned state.Zero-Ambiguity Orphan Recovery
MockLocationGuideScreen.kt&strings.xml):MockLocationStatus.ORPHANEDis active:GPS frozen — action needed.Simulated location is stuck.[Open developer options]button launching Android system developer settings.[Open developer options]button in Step 2 while inORPHANEDstatus to eliminate redundant UI actions.Visual Preview & UI Context
URSwitchtoggle and live status subtitle (highlighted by arrows: active toggle andSyncing with Berlin, State of Berlin), matching the vertical cadence and spacing of "Kill switch" and "Allow providing on cellular network". Dynamic feedback updates seamlessly (Syncing with <City>,Waiting for provider location,Setup required, orGPS frozen — action needed).What's Changed
Full Changelog
main...full-bars:android:fix/location-sync-and-transport-hardening