feat: Doze-resistant location sharing with boot persistence - #16
Merged
Conversation
Bumps `cameraX` from 1.5.2 to 1.5.3. Updates `androidx.camera:camera-core` from 1.5.2 to 1.5.3 Updates `androidx.camera:camera-camera2` from 1.5.2 to 1.5.3 Updates `androidx.camera:camera-lifecycle` from 1.5.2 to 1.5.3 Updates `androidx.camera:camera-view` from 1.5.2 to 1.5.3 --- updated-dependencies: - dependency-name: androidx.camera:camera-core dependency-version: 1.5.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-camera2 dependency-version: 1.5.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-lifecycle dependency-version: 1.5.3 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: androidx.camera:camera-view dependency-version: 1.5.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
When an announce arrives via an auto-discovered BackboneInterface, the "Received Via" in Node Details showed just "TCP/IP" with no identifying information. Two fixes: Python: format_interface_name() now falls back to target_ip:port when the interface name equals the class name (auto-discovered). Kotlin: getInterfaceInfo() now includes bracket content (address) in the subtitle when no friendly name is available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
NomadNet Micron links can embed variables as key=value pairs in the link field list (e.g., [Download`:/page/download.mu`file=app.apk]). NomadNet's Browser.py splits on = and sends them as var_key in the request data. Columba was treating the entire key=value as a form field name, causing the value to never reach the server. Also adds support for the * wildcard which submits all form fields. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add progress_callback to link.request() matching NomadNet/MeshChatX - Store and check request receipt; evict link if request can't be sent - Evict cached links on timeout/failure to prevent reusing broken links - Cancel request receipt on user cancellation - Single request_path() call matching NomadNet's approach - Granular status reporting: "Looking up path...", "Path found (N hops)", "Connecting (N hops)...", "Requesting page..." shown in real-time Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests now expect the new subtitle format where bracket content (address/hostname) is shown when no friendly name is available. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers the three new fallback branches: target_ip/target_host, target without port, and str() with/without brackets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rns_api.py contains network-dependent code that calls RNS.Transport, RNS.Link, etc. — requiring a live Reticulum instance to test. This matches the existing exclusion of the Kotlin service/protocol layer which has the same constraint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-name-autodiscovered fix: show address for auto-discovered backbone interfaces
…line-field-variables fix: handle inline field=value variables in Micron links
Prevents a stale IO coroutine from cancelling a newer navigation's status poller during rapid navigation. stopStatusPolling() now accepts an optional epoch and skips cancellation if the epoch has advanced. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-request-improvements feat: improve NomadNet page request reliability and status reporting
Detect file responses (metadata with filename) from NomadNet nodes, save to disk in the app cache directory, and show a download dialog with progress indicator, Open, and Share actions. Changes across the full stack: - Python (rns_api.py): Read file data while handle is open in response callback, save via _save_file_response, return type=file - AIDL: Add getNomadnetDownloadProgress - Binder/WrapperManager: Pass downloadDir, expose download progress - ServiceReticulumProtocol: Extend NomadnetPageResult with file fields, parse type=file responses - ViewModel: Add DownloadState flow, downloadFile() with progress polling, /file/ path detection in navigateToLink, handle unexpected file responses in fetchPage - UI: NomadNetDownloadDialog composable with progress, Open/Share via FileProvider Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sanitize filename with os.path.basename() to prevent path traversal - Wire progress_update callback to actually set _download_progress - Add epoch guard after download completes to prevent stale state update Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…progress - Set BrowserState.Error when fetchPage gets an unexpected file response, preventing the loading spinner from lingering after the download dialog is dismissed - Reset _download_progress to -1.0 at the start of each request so stale values from a previous download don't show 100% briefly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-file-downloads feat: add NomadNet file download support
…-parser-js Identifies 7 discrepancies (2 critical, 3 moderate, 2 minor) between Columba's MicronParser.kt and the reference implementations. Key issues: links/fields parsed without backtick prefix, section depth reset exact match, trimEnd stripping trailing whitespace for pixel art. https://claude.ai/code/session_017GDeALxfLKXKyM9sr7XwRS
D1 (CRITICAL): Links `[` and fields `<` now require backtick prefix to enter formatting mode, matching NomadNet behavior. Bare `[` and `<` are treated as literal text. D2 (CRITICAL): `<` depth reset now re-parses the remainder of the line instead of requiring an exact match. `<Content after reset` works. D3 (MODERATE): Literal mode toggle requires exact `= match (removed trimStart that accepted indented toggles). D5 (MODERATE): Divider custom character restricted to exactly 2-char lines, matching NomadNet. Longer lines like `-Hello` use default ─. D6 (MODERATE): Removed trimEnd() from line processing to preserve trailing whitespace significant for pixel art rendering. D7 (MINOR): Added heading + field conflict sanitization — lines starting with `>` that contain backtick-field markers have `>` stripped. D8 (MINOR): Divider characters with ord < 32 replaced with default ─. All 105 tests pass (19 new tests for the 7 discrepancies + updated existing tests for new backtick-prefix requirement). https://claude.ai/code/session_017GDeALxfLKXKyM9sr7XwRS
Flatten nested if-blocks in the field parsing branch of parseInline() by hoisting the parseField call with a conditional expression, reducing nesting depth from 5 to 4. https://claude.ai/code/session_017GDeALxfLKXKyM9sr7XwRS
Updated audit summary table with FIXED status and verification note confirming all fixes checked against NomadNet MicronParser.py and MeshChatX source code (2026-03-28). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…columba-parser-qOEzI Add micron parser audit comparing Columba against NomadNet and micron-parser-js
…do-tech#677) When the user navigates back while a page is loading, the in-flight request's result could overwrite the restored page with an error or stale content. Two fixes: - goBack() now increments fetchEpoch to invalidate in-flight requests - fetchPage() and submitFormAndNavigate() check fetchEpoch after the blocking request returns and discard stale results Fixes torlando-tech#677 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vigate If requestNomadnetPage() throws (rather than returning Result.failure), the catch block would still overwrite the restored page with an error. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cache-hit navigation in loadPage() and navigateToLink() now increments fetchEpoch to invalidate in-flight requests. Protocol-null early exits also check epoch before writing error state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The LazyColumn key included the item index (hash_$index), so when a new announce inserted at the top, every existing item's key changed and Compose rebuilt the entire list — losing scroll position. Now uses destinationHash alone as the stable key. For the rare Paging3 transient-duplicate edge case (torlando-tech#542), only actual duplicates get a disambiguated key instead of every item. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Eagerly compute keys at composition time instead of lazily via a captured mutable set, so the returned lambda is safe regardless of call order or count. Also fix KDoc type reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-page-request-overwrites fix: prevent stale page request from overwriting current page
Foundation for rendering discovered interfaces as map pins: - Make InterfaceCategory public with mdiIconName and markerColor properties for map marker rendering - Add InterfaceMarker data class with location, radio, and TCP fields - Extend MapState with interfaceMarkers and interfaceFilterEnabled - Inject ReticulumProtocol into MapViewModel for interface discovery - Add loadInterfaceMarkers() with 30s periodic refresh - Add toggleInterfaceFilter() for per-category filtering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Discovered network interfaces with GPS coordinates are now shown on the map as rounded-rectangle pins with type-specific icons and colors: - LoRa/RNode: orange antenna icon - TCP: blue cloud icon - BLE: indigo bluetooth icon - Auto/Local: green wifi icon Features: - Filter chips below the TopAppBar to toggle interface types on/off - Tap any interface pin to see details in a bottom sheet (reuses existing FocusInterfaceBottomSheet with RNode params, TCP info, etc.) - Interface pins render below contact markers to avoid overlap - Refreshes every 30s alongside contact marker refresh - Category bitmaps are cached (only 4-6 images, not per-marker) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a Room table (interface_first_seen) that records when each interface was first discovered. Uses INSERT OR IGNORE so the original timestamp is preserved across re-discoveries and app restarts. The first-seen timestamp is shown in the interface detail bottom sheet alongside last-heard and hop count. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Map pins now use the same icon identity as the discovery screen: - LoRa/RNode: MDI "antenna" (matches Lucide Antenna in discovery) - TCP: MDI "earth" (matches Icons.Default.Public globe in discovery) - I2P: MDI "incognito" (new category, matches discovery screen) - Yggdrasil: MDI "pine-tree" (new category, matches Lucide TreePine) - Also adds Weave/KISS to the LoRa category Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace hardcoded version strings with libs.test.core so future bumps only need to update libs.versions.toml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bumps [gradle/actions](https://github.com/gradle/actions) from 5 to 6. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](gradle/actions@v5...v6) --- updated-dependencies: - dependency-name: gradle/actions dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](codecov/codecov-action@v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com>
…-dependency-upgrades chore: upgrade to Gradle 9.4.1 and AGP 9.1.0
Bumps the compose group with 2 updates: androidx.compose.runtime:runtime and androidx.compose:compose-bom. Updates `androidx.compose.runtime:runtime` from 1.10.4 to 1.10.5 Updates `androidx.compose:compose-bom` from 2026.02.01 to 2026.03.00 --- updated-dependencies: - dependency-name: androidx.compose.runtime:runtime dependency-version: 1.10.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: compose - dependency-name: androidx.compose:compose-bom dependency-version: 2026.03.00 dependency-type: direct:production dependency-group: compose ... Signed-off-by: dependabot[bot] <support@github.com>
Centralizes all path requests through `_request_path_if_needed()` which checks `has_path()` before calling `request_path()`, preventing redundant network traffic. Key changes: - Add `_request_path_if_needed()` to ReticulumWrapper — single entry point that guards every path request with a has_path check - Remove 5-second blocking retry loops from all send methods (send_lxmf_message, send_reaction, send_location_telemetry, send_telemetry_request, send_lxmf_message_with_method) — these blocked the Chaquopy thread and fired unguarded path requests - Remove retry thread from _on_lxmf_delivery telemetry stream response - Add hasPath guard to call_manager identity resolution - IdentityResolutionManager: startup sweep now only requests paths for 3 most recent conversations instead of all contacts - IdentityResolutionManager: periodic check interval 15min → 3h - IdentityResolutionManager: retryResolution() now checks hasPath first - Add ConversationDao.getRecentPeerHashes() for startup sweep query Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove dead inner `if not recipient_identity` guards from send_location_telemetry, send_telemetry_request, and send_lxmf_message_with_method (always True after _request_path_if_needed) - Update error messages from "Path requested but no response received" to "path requested, retry shortly" (no wait happens anymore) - Fix _request_path_if_needed docstring: clarify True=path present, False=request fired - Simplify request_path public API: remove unreachable except block, document that it always returns success=True - Fix test_handles_field_commands_with_identity_retry: retry thread was removed, test now verifies response is skipped when identity is not found - Fix test_error_message_mentions_path_requested: match new error text Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elivery The sender's path already exists (the message arrived over it), so _request_path_if_needed was a no-op. Instead, try Identity.recall with from_identity_hash=True as a fallback — the same pattern used by all other send methods. Only drop the telemetry response if both recall lookups fail. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
hasPath() can throw a bridge error if the Python process isn't ready; every other caller of requestPathIfNeeded handles this. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…est-dedup fix: deduplicate path requests with hasPath guard
…thub_actions/gradle/actions-6 ci: bump gradle/actions from 5 to 6
…adle/compose-895afb3ff6 deps(deps): Bump the compose group with 2 updates
…adle/cameraX-1.5.3 deps(deps): Bump cameraX from 1.5.2 to 1.5.3
…thub_actions/codecov/codecov-action-6 ci: bump codecov/codecov-action from 5 to 6
…tech#734) MapViewModel loaded all 12,557 announces at startup via getEnrichedAnnounces() to build a lookup map for ~17 location markers. The 2MB CursorWindow was exhausted at row ~1372, crashing the app on large databases. Replace with getAnnouncesForLocationSenders() — a scoped subquery on received_locations with a lightweight 6-column projection (MapAnnounceLookup), reducing the result set from ~12,557 rows × 19 columns to ~17 rows × 6 columns. Closes torlando-tech#734 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap both sides of the IN comparison with lower() so the query matches announces even if destination hash casing diverges from received_locations. Matches the case-insensitive fallback already present in MapViewModel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ndow-overflow-announces fix: scope MapViewModel announce query to location senders
Added statistics section with download badges and star history chart.
Location sharing and telemetry collection stopped updating during Android Doze because the main process had no foreground service to keep GPS callbacks alive. - Add LocationForegroundService with foregroundServiceType="location" and FOREGROUND_SERVICE_LOCATION permission - Add LocationServiceCoordinator (reference-counted singleton) to manage the service lifecycle across LocationSharingManager and TelemetryCollectorManager - Switch GPS priority from BALANCED_POWER_ACCURACY to HIGH_ACCURACY (reliable in Doze with a location foreground service) - Service only runs when sharing or telemetry is active Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- START_NOT_STICKY: don't restart service after process death (sessions are in-memory, nothing to resume) - try-catch SecurityException in startForeground for Android 14+ permission revocation - Make release() idempotent in coordinator (guard against double release from concurrent stopSharing + session expiry) - Release foreground service on SecurityException in location updates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sessions are serialized to DataStore on every change (start, stop, expiry) and restored in restoreIfActive() called from ColumbaApplication after Reticulum service bind. On restore: expired sessions are filtered, foreground service is acquired, GPS updates and session check are started. Combined with the LocationForegroundService, this enables location sharing to survive app restarts without requiring the user to open the UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The foreground service was only acquired when GPS tracking was active (send mode). Clients in request-only mode (polling host for positions without sending their own) had no foreground service, so periodic requests were throttled in Doze. Now needsForegroundService() checks both send and request enabled states. Also triggers updateLocationTracking() when request settings change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
LocationServiceCoordinator: - Rollback reason on acquire if startForegroundService throws - Add clearAll() called by service on SecurityException self-destruct LocationForegroundService: - Call clearAll() before stopSelf on permission failure LocationSharingManager: - Guard restoreIfActive with _isSharing check (pre and post dispatch) - Guard startLocationUpdates/startSessionCheck against double-start - Clear corrupted JSON on deserialize failure - Cancel sessionCheckJob in SecurityException cleanup + persist - Cancel sessionCheckJob when all sessions expire TelemetryCollectorManager: - Poison state flows before cancelling jobs in stop() to prevent straggling flow collectors from re-acquiring the foreground service Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Notification now shows: - "Location sharing active" (peer-to-peer only) - "Telemetry collection active" (group send/request only) - "Location sharing & telemetry active" (both) Updated on every acquire/release so it always reflects current state. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MapViewModel: keep both identityRepository (SOS) and reticulumProtocol + interfaceFirstSeenDao (upstream) - MapViewModelTest: take upstream version, add identityRepository mock - DatabaseModule: combine source column + index + interface_first_seen table in migration 43→44 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
MatthieuTexier
pushed a commit
that referenced
this pull request
Apr 25, 2026
Pulls in two LXMF-kt fixes that affect every Columba user: * LXMF-kt#14 (PR #14): receiver delivered any DIRECT-delivered multi-packet LXMessage (>319 bytes — every attachment, every voice frame) twice because the dedup guard keyed off transientId, a sender-side field never populated on the incoming path. Now keys on message.hash. Eliminates the duplicate inbox entries Columba users saw on every received attachment. * LXMF-kt#15 (PR #16): propagation-fetch and paper-message dedup callsites queried the dedup map by wire-source transient_id while writes (post-#14) used message.hash, so both lookups always missed. Propagation users re-downloaded every offered message every fetch cycle (bandwidth waste, no double delivery thanks to inner dedup). Paper ingestion did the unpack+decrypt every time before inner dedup short-circuited (CPU waste). Now both readers find their matching key shape via Python-style heterogeneous-key map. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
LocationForegroundServicewithforegroundServiceType="location"to keep GPS alive during Android Doze modeLocationServiceCoordinator(reference-counted singleton) to manage service lifecycle across LocationSharingManager and TelemetryCollectorManagerBALANCED_POWER_ACCURACYtoHIGH_ACCURACYDepends on
Test plan
adb shell dumpsys deviceidle force-idle, positions continue updating