Commit graph

182 commits

Author SHA1 Message Date
torlando-tech
d692b1171f feat(ble): add duplicate identity detection callback for Android
On Linux, BLEInterface sets driver.on_duplicate_identity_detected to
check if an identity is already connected at a different MAC address.
On Android, this callback was never wired up, allowing duplicate
connections during MAC rotation.

Changes:
- KotlinBLEBridge: Add onDuplicateIdentityDetected callback field
- KotlinBLEBridge: Add setOnDuplicateIdentityDetected setter method
- KotlinBLEBridge: Check for duplicates in handleIdentityReceived()
  before accepting connection, using safe message format
- AndroidBLEDriver: Wire up callback in _setup_kotlin_callbacks()
- AndroidBLEDriver: Add _handle_duplicate_identity_detected() method
- Update tests to verify callback is properly wired
- Update BLE architecture docs with callback flow

When duplicate detected, connection is rejected with safe log message
"Duplicate identity rejected for {address}" which doesn't trigger the
blacklist mechanism.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 16:19:17 -05:00
Torlando
abf18472fd
Merge pull request #274 from torlando-tech/feature/settings-about-section
feat: add comprehensive About section to Settings (#262)
2026-01-16 20:20:04 -05:00
torlando-tech
3e3e7041d6 feat: add comprehensive About section to Settings
Implements issue #262 - adds a detailed About section at the bottom of
the Settings screen displaying app information, device details, protocol
versions, identity info, links, and build metadata.

Features:
- App information (version, build code, git commit, build date)
- Device information (Android version, API level, model, manufacturer)
- Protocol versions (Reticulum, LXMF, BLE-Reticulum)
- Identity hash display
- Links to GitHub repository, issue tracker, and Reticulum website
- Legal information and dependency attribution
- Copy to clipboard functionality for system info

Technical changes:
- Add BuildConfig fields for git commit hash and build timestamp
- Create DeviceInfoUtil for collecting and formatting system info
- Add Python methods to retrieve protocol versions
- Add AIDL and Kotlin wrappers for protocol version retrieval
- Update SettingsViewModel with retry logic for fetching versions
- Create AboutCard composable with comprehensive UI
- Add 23 unit tests (8 for DeviceInfoUtil, 15 for AboutCard)

All quality checks passing (detekt, ktlint, cpd, unit tests).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 19:48:40 -05:00
torlando-tech
b3345e66e0 test: update propagation node tests to match None-clearing behavior
The production code was fixed to not pass None to LXMF's
set_outbound_propagation_node() since LXMF doesn't support it.
Updated the tests to verify that the router method is NOT called
when clearing with None, only internal state is cleared.

Fixes two failing tests:
- test_set_propagation_node_clears_when_none
- test_set_outbound_propagation_node_clears_with_none

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 19:00:06 -05:00
torlando-tech
431d139e0b fix: don't pass None to LXMF set_outbound_propagation_node()
LXMF's set_outbound_propagation_node() expects a valid bytes hash and
calls len() on it, which fails with TypeError when passed None.

When clearing the propagation node, just clear our internal tracking
variable instead of calling the LXMF method. LXMF will handle the case
where no valid propagation node is set.

Fixes error: "object of type 'NoneType' has no len()"

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-16 17:36:27 -05:00
torlando-tech
0d452252a1 fix(#257): add Python tracking + Kotlin guard for propagated message status
Two-layer defense against spurious failure callbacks for propagated messages:

Python layer:
- Track successfully propagated messages in _successfully_propagated dict
- Guard in _on_message_failed to ignore failures for tracked messages
- Cleanup stale tracking entries (24h TTL)
- Immediate state check after propagation retry

Kotlin layer:
- Add isTerminalSuccessStatus() helper
- Guard in handleDeliveryStatusUpdate to block status degradation
  from terminal success states (sent/propagated/delivered) to failed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 16:55:42 -05:00
torlando-tech
d8a924862a fix: emit 'propagated' status for PROPAGATED method messages
When messages are sent via propagation node, the status should update
to "propagated" (not "sent") when the relay confirms receipt.

Changes:
- Modify _on_message_sent() to check desired_method and emit
  'propagated' status for PROPAGATED method messages
- Add state check after propagation retry in _on_message_failed()
  to detect immediate propagation success
- Handle pending file notifications when propagation is confirmed

This fixes the issue where Message Details showed "Sent" for messages
that were successfully propagated to a relay node.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 16:55:42 -05:00
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