Commit graph

175 commits

Author SHA1 Message Date
torlando-tech
b6e5f331a6 fix: consistent filtering of deprecated propagation nodes in all queries
Problem: Auto-select relay was enabled but no relay was being selected.
Root cause: Different queries had inconsistent filtering:
- getAllAnnouncesPaged() had NO filter - showed deprecated nodes
- getAnnouncesByTypes() filtered deprecated nodes (stampCostFlexibility IS NULL)
- This caused relays to appear in announce stream but not be available for auto-selection

Changes:
- Make all announce queries consistently filter deprecated propagation nodes
- Add logging when pn_announce_data_is_valid() returns False (Python)
- Add debug logging to PropagationNodeManager for auto-selection diagnosis

Deprecated nodes are propagation nodes running old LXMF versions that
don't include stamp cost data in their announces. These cause sync failures
so they are intentionally filtered from relay selection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 16:42:59 -05:00
torlando-tech
517ece890f Fix test_extract_interface_from_announce_table mock setup
The test was expecting the interface's .name attribute value, but the
code uses type().__name__ to get the class name. Fixed by:
- Setting mock_interface.__class__.__name__ = 'TCPInterface'
- Updating assertion to expect "TCPInterface" (class name)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 19:25:27 -05:00
torlando-tech
467dd09602 Add test for direct LXMF receiving_interface capture
Tests the opportunistic message scenario where LXMF provides
the receiving_interface directly on the message object, rather
than needing to look it up from path_table.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 18:52:13 -05:00
torlando-tech
de43ce9c92 Fix receiving interface capture for AutoInterface messages
AutoInterfacePeer uses 'ifname' attribute (network interface name like
'wlan0') instead of 'name'. For the first opportunistic message before
path_table is populated, the interface was not being captured.

Changes:
- Use class name (type().__name__) to identify interface type reliably
- Extract shared interface display logic to InterfaceUtils.kt
- Both MessageDetailScreen and AnnounceDetailScreen use same utility
- AutoInterface class names map to "Local Network" with Wifi icon

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 18:50:36 -05:00
Tyler Orlando
6d511bd0db Fix Python tests by adding type checking for Mock objects
- Add module-level LXMF field constants (FIELD_FILE_ATTACHMENTS,
  FIELD_IMAGE, FIELD_AUDIO) so code works without LXMF imported
- Use local booleans to track captured hops/interface instead of
  hasattr() which always returns True for Mock objects
- Add isinstance() checks for hop count (int), interface name (str),
  and public key (bytes) before using them
- This ensures JSON serialization doesn't fail when tests use
  MagicMock objects that auto-create attributes

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-13 09:56:21 -05:00
torlando-tech
1c1bb78a32 Capture receiving interface and hops for opportunistic messages
For opportunistic (single-packet) messages that arrive before the path_table
is populated, we now capture the receiving interface and hop count directly
from the LXMF message object. This is enabled by a patch to our LXMF fork
that passes packet.receiving_interface and packet.hops to the delivery callback.

Changes:
- Update LXMF dependency to feature/receiving-interface-capture branch
- Check for receiving_interface and receiving_hops attributes on LXMessage first
- Fall back to path_table lookup for link-based messages (unchanged behavior)

This fixes the issue where messages received before an announce wouldn't have
hop count or interface info in the Message Details screen.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:59:47 -05:00
torlando-tech
82f3f7aef7 Fix race condition: register message callback before Python init
Move message received callback registration from setupBridges() to
setupPreInitializationBridges() so it's registered BEFORE Python's
LXMF router starts. This ensures messages that arrive immediately
after initialization use the event-driven path with hop count and
interface data, rather than falling back to polling.

Also clarify in comments that hop/interface capture is only available
when a path exists in path_table (opportunistic messages without an
established path won't have this info).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-12 23:24:16 -05:00
torlando-tech
21695ebdbb Make message receiving truly event-driven (no polling)
Python callback now sends full message data including content, fields,
public key, hop count, and receiving interface. Kotlin processes the
message directly from the callback JSON instead of polling back to
Python.

This eliminates the round-trip poll and makes message delivery truly
event-driven.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:09:04 -05:00
torlando-tech
9ae5e052f5 Fix interface capture when interface.name is None
The interface object may have a name attribute that is None, resulting
in "None" being captured as the interface name. Now checks that
interface.name is truthy before using it, and skips storing if the
result is "None".

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:04:31 -05:00
torlando-tech
59b2c78c42 Include hop count and interface in event-driven callback
The kotlin_message_received_callback was missing hops and receiving_interface
fields. While the current flow triggers a poll that also includes this data,
including it in the callback makes the data available for a future true
event-driven implementation without polling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:55:47 -05:00
torlando-tech
012154bd6d Fix race condition in path_table access using .get()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
90f61ffc23 Capture receiving interface for all messages, not just direct
The path table stores the interface through which we received the
message, regardless of hop count. For multi-hop messages, this is
the interface of the last relay that delivered to us.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
ebfd013302 Add tests for receiving interface card and delivery hop capture
- Add 7 Kotlin tests for getReceivingInterfaceInfo() covering all interface
  type branches (AutoInterface, TCP, BLE, RNode, Serial, unknown, null)
- Add 7 Python tests for _on_lxmf_delivery() hop count and interface capture
- Use performScrollTo() for UI assertions on scrollable content

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
09c4d69162 Fix test mocks to set .name attribute on interface objects
Tests were mocking __str__ but code now uses .name attribute when
available. Added .name to mock interfaces to match actual behavior.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
53d72b3ecd Capture hop count at delivery time instead of poll time
RNS.Transport.hops_to() returns current routing info which may change
after message reception. Now capture hop count and interface immediately
in _on_lxmf_delivery() and store on message object for later retrieval.

This ensures hop count reflects the actual path taken at reception time,
not a potentially different path discovered later.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
f4f3fb6e9f Only capture receiving interface for direct messages (hops=0)
Path table stores outbound routing info, not reception history.
For multi-hop messages, path_entry[5] is the next-hop interface for
sending TO the sender, not the interface that received the message.
Interface info is only accurate when hops=0 (direct delivery).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
bc9eecf2a2 Add unit tests for hop count validation
Tests verify that poll_received_messages correctly:
- Extracts valid hop counts (positive integers)
- Extracts zero hop count (direct delivery)
- Skips negative hop counts (-1)
- Skips None hop counts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
1b871804b5 Validate hop count before storing (skip None or negative values)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
ea73038939 Use interface .name attribute in announce handlers too
Apply same fix to _announce_handler and poll_received_announces
for consistency across all interface name extraction.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:51 -05:00
torlando-tech
3813ad1936 Use interface .name attribute for cleaner interface names
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:50 -05:00
torlando-tech
5db8606bf0 Add hop count and receiving interface to received message info
Capture network metadata when messages are received and display it on the
message info screen:

- Add receivedHopCount and receivedInterface columns to MessageEntity
- Create database migration 28→29
- Capture hop count via RNS.Transport.hops_to() at message reception
- Capture receiving interface from RNS path table at message reception
- Pass new fields through EventHandler and ServicePersistenceManager
- Update Message, MessageUi models and mappers
- Display hop count (Direct/N hops) and interface (WiFi/BLE/LoRa/TCP)
  on received message info screen with appropriate icons

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:49:50 -05:00
torlando-tech
75f0d6addc Fix race condition in path_table and announce_table access
Use .get() instead of checking membership then accessing, which
eliminates the race condition where another thread could remove
the entry between the two operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 22:15:03 -05:00
torlando-tech
da03e074c3 Revert "fix(security): correct destination direction in RMSP hash verification"
This reverts commit 89b458d894.
2026-01-12 21:25:45 -05:00
torlando-tech
89b458d894 fix(security): correct destination direction in RMSP hash verification
Change RNS.Destination direction from IN to OUT when verifying server
identity. Servers announce with OUT direction (announcing their service),
so verification must use the same direction to correctly validate the
destination hash against the public key.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 21:24:25 -05:00
torlando-tech
71fbc2691d fix: remove redundant assignment before return in hash verification
The `server = None` assignment was unreachable since `return None`
immediately follows. Removed the dead code for clarity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:52:50 -05:00
torlando-tech
9e9e7f37ed fix: address 5 code review issues from Greptile round 2
1. RMSP cancellation path - add retry logic with backoff for file deletion
   (matching HTTP path behavior)
2. SQLite database close - use explicit try-finally to prevent fd leaks
3. Database error recovery - attempt immediate orphan import instead of
   waiting for app restart
4. Python hash verification - explicitly set server=None on failure
   (defensive coding)
5. CONCURRENT_DOWNLOADS - increase to 10 for I/O-bound HTTP operations

Issue 1 (finally block file deletion) was already correct - only deletes
when success=false.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:36:23 -05:00
torlando-tech
da560668ff fix: clarify hash verification flow and fix flaky ImageCache test
- Add explicit log message and comment in RMSP client to clarify that
  hash verification happens before server object creation (addressing
  code review concern about P0 security vulnerability)
- Add resetForTest() to ImageCache to properly reset hit/miss stats
  between tests, fixing flaky getStats test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:22:57 -05:00
torlando-tech
398ebfc957 fix(tests): use explicit global coverage marker in Server D test
Server D needs ["*"] instead of [] for global coverage after the
covers_geohash behavior change.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:44:49 -05:00
torlando-tech
2f5ed6c24f fix: align Python covers_geohash with Kotlin behavior
Both now require explicit "*" for global coverage; empty list means
no coverage data available. This prevents servers with incomplete
announce data from incorrectly matching all geohash queries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:02:14 -05:00
torlando-tech
90ac143fa3 fix: use fixed 30s path timeout instead of percentage-based
Previous 10s max was too short for mesh networks. Now uses 30s fixed
timeout (or half of total for short timeouts), giving path establishment
reasonable time while still leaving most of the timeout for data transfer.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:57:25 -05:00
torlando-tech
59f8c73b70 security: fix destination direction in hash verification
RMSP servers announce with IN direction (listening for requests),
not OUT. Using OUT direction caused the hash computation to always
fail for legitimate servers, defeating the security check.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:47:03 -05:00
torlando-tech
dfc179384a fix: simplify path waiting with fixed polling interval
Replace exponential backoff with fixed 250ms polling for path requests.
Reduce path timeout to 30% of caller's timeout (max 10s) to avoid
nested timeout confusion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:24:54 -05:00
torlando-tech
2b622f2551 fix: increase tile count limit from 100K to 1M
100K tiles was too restrictive for large region downloads (~100km radius).
Updated limit to 1M in both Kotlin and Python code with corresponding tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:23:30 -05:00
torlando-tech
51570c92fd security: verify destination hash matches public key in RMSP client
Compute expected destination hash from provided public key and compare
with claimed dest_hash to prevent server impersonation attacks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 19:14:01 -05:00
torlando-tech
1e2b5f47a7 fix: improve RMSP error response detection using msgpack format markers
Instead of unreliably checking if response < 1000 bytes, detect error
responses by checking for msgpack map format markers (0x80-0x8f fixmap,
0xde map16, 0xdf map32). This correctly handles small valid tiles and
avoids misinterpreting binary tile data as errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:59:51 -05:00
torlando-tech
38c994680b fix(test): make sleep count assertion less strict in path request test
The test was expecting exactly 10 sleep calls, but background threads
(from other tests or the same test) may also call time.sleep, causing
the count to be higher. Changed to assertGreaterEqual(10) which still
verifies the retry loop ran while being robust to background activity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 18:25:50 -05:00
torlando-tech
d739efca58 fix: add warning log for truncated tile data in Python unpack_tiles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:46:52 -05:00
torlando-tech
f46a888070 test: add Python unpack_tiles validation tests
Tests for:
- Excessive tile count (>100,000) rejected
- Maximum tile count (100,000) accepted
- Excessive tile size (>1MB) rejected
- Maximum tile size (1MB) accepted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:24:18 -05:00
torlando-tech
6c78dc8827 fix(security): add validation to Python unpack_tiles function
Add tile_count (max 100,000) and tile size (max 1MB) validation to
match the Kotlin implementation. Prevents DoS attacks from malicious
RMSP servers sending invalid counts or sizes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:17:23 -05:00
torlando-tech
6563e29a57 perf: use exponential backoff for RMSP link status polling
Replace fixed 0.1s sleep with exponential backoff starting at 50ms,
capping at 1s. Reduces CPU usage during link establishment while
maintaining responsiveness.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:32:48 -05:00
torlando-tech
1b9fbf0a3b perf: use exponential backoff for RMSP path establishment
Replace fixed 0.5s sleep with exponential backoff starting at 100ms,
capping at 2s. This improves responsiveness when paths are established
quickly while reducing CPU usage during longer waits.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:32:17 -05:00
torlando-tech
cef6995ad5 test: add comprehensive tests for offline maps feature
Add Robolectric UI tests for offline maps screens:
- OfflineMapsScreenTest: tests for EmptyOfflineMapsState, StorageSummaryCard,
  OfflineMapRegionCard, StatusChip, and delete dialog interactions
- OfflineMapDownloadScreenTest: tests for LocationSelectionStep,
  RadiusSelectionStep, ConfirmDownloadStep, DownloadingStep status states

Also includes:
- OfflineMapsViewModelTest: comprehensive ViewModel tests
- TileDownloadManagerTest: tests for geohash, tile coords, RMSP downloads
- MBTilesWriterTest: tests for MBTiles file creation
- Python tests for rmsp_client.py and reticulum_wrapper.py

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:09:51 -05:00
torlando-tech
8c53d12d21 Add RMSP tile download via AIDL with multi-geohash support
Implements RMSP (Reticulum Map Service Protocol) tile fetching through
the cross-process AIDL interface, enabling offline map downloads over
mesh networks.

Key changes:
- Add fetchRmspTiles() and getRmspServers() to IReticulumService.aidl
- Implement RMSP methods in ReticulumServiceBinder with Python interop
- Add ServiceReticulumProtocol.fetchRmspTiles() for suspend function access
- Update ReticulumModule to expose ServiceReticulumProtocol for injection
- Add TileSource.Rmsp sealed class for RMSP tile source abstraction
- Implement multi-geohash coverage calculation for complete region downloads
  - decodeGeohashBounds() to get bounding box from geohash
  - geohashesForBounds() to find all cells covering a region
- Update downloadRegionRmsp() to iterate over all geohash cells
- Add RMSP server discovery UI in OfflineMapDownloadScreen
- Update rmsp_client.py with announce waiting and path resolution
- Add public key parameter for server identity reconstruction

The RMSP download now fetches tiles for all geohash cells covering the
requested region, matching HTTP download coverage behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:09:31 -05:00
torlando-tech
0c93ccd397 fix: Complete offline maps feature with navigation and bug fixes
- Fix MapSourcesCard toggle not working (preserve state in combine flow)
- Add navigation routes for OfflineMapsScreen and OfflineMapDownloadScreen
- Add Offline Maps FAB on MapScreen with navigation callback
- Hide bottom nav bar on offline maps screens
- Fix FAB positioning with navigationBarsPadding
- Fix OpenFreeMap tile URL (add version string)
- Fix SQLite threading issues:
  - Remove transactions (incompatible with coroutine thread pools)
  - Add mutex for serialized database writes
  - Use beginTransactionNonExclusive for safer transaction handling
- Add logging to TileDownloadManager for debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:09:31 -05:00
torlando-tech
e74d1fd4ee feat: Phase 3-6 - MapLibre integration, RMSP client, Kotlin bridge, and settings
Phase 3: MapLibre offline style integration
- Add OfflineMapStyleBuilder for generating MapLibre style JSON from MBTiles
- Add MapTileSourceManager for source selection (offline/HTTP/RMSP)

Phase 4: RMSP Python client port
- Add rmsp_client.py with RmspClientWrapper class
- Support server discovery, query, and tile fetching over Reticulum
- Parse RMSP announces and track servers

Phase 5: Kotlin bridge for RMSP
- Add RMSP methods to PythonWrapperManager (getRmspServers, queryRmspServer, etc.)
- Update PollingManager to handle rmsp.maps announces with RMSP fields
- Update reticulum_wrapper.py with RMSP announce handler and methods
- Register rmsp.maps aspect for announce handling

Phase 6: Settings & source selection
- Add map source preferences to SettingsRepository (HTTP/RMSP toggles)
- Create MapSourcesCard UI for settings screen
- Wire MapTileSourceManager to use settings from repository
- Add validation to prevent disabling all map sources

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 01:09:31 -05:00
torlando-tech
6830ac7532 test: add unit tests for reticulum_wrapper coverage
Add tests covering previously untested code paths:
- Transport.active_links fallback for establish_link and get_link_status
- link.identify() exception handling during link establishment
- image_data_path file reading and cleanup in send_lxmf_message_with_method
- Missing image file error handling
- delivery_method field in error responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 23:45:26 -05:00
torlando-tech
270954042d test: add unit test for link.identify() backchannel behavior
Verify that establish_link calls link.identify() with the router's
identity after the link becomes active. This enables the remote peer's
LXMRouter to add the link to their backchannel_links dictionary.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:32:25 -05:00
torlando-tech
638e4edfaf chore: remove debug logging from link status functions
Clean up verbose debug logging added during troubleshooting of
incoming link detection. The Transport.active_links fallback is
now a simple, silent check since backchannel_links should be
populated via link.identify() in normal operation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:30:13 -05:00
torlando-tech
87933ede1f refactor: consolidate link management into ConversationLinkManager
Remove LinkSpeedProbe and enhance ConversationLinkManager to provide
all link metrics needed for image quality recommendations:

- Add link status indicator next to online indicator in conversation
- Enhance LinkState with expectedRateBps, nextHopBitrateBps, rttSeconds,
  hops, and linkMtu fields from Python establish_link response
- Add periodic refresh to detect incoming links from peers
- Call link.identify() after link establishment to enable backchannel
  detection on the remote peer (populates their backchannel_links)
- Update ImageQualitySelectionDialog to use LinkState instead of ProbeState
- Remove LinkSpeedProbe.kt and related probe_link_speed Python function
- Update tests to use ConversationLinkManager instead of LinkSpeedProbe

The key fix: when establishing a link, we now call link.identify() which
triggers the remote peer's delivery_remote_identified callback, adding
the link to their backchannel_links dictionary. This allows bidirectional
link detection without requiring a message to be sent first.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 22:28:03 -05:00
torlando-tech
acf026e23a fix(ble): load BLEInterface.py from correct package
BLEInterface.py is in ble_reticulum, not ble_modules. This was
accidentally changed in 94c37167, causing the BLE interface to
fail deployment silently and preventing the AndroidBLE interface
from loading.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 19:37:42 -05:00