- 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>
Server D needs ["*"] instead of [] for global coverage after the
covers_geohash behavior change.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- LinkSpeedProbeResult.bestRateBps now prefers expectedRateBps (actual
measured throughput from prior transfers) over establishment_rate
or interface bitrate
- For propagated delivery mode, also check backchannel link for
expected_rate from the recipient
- Remove unused conversationLinkManager from LinkSpeedProbe
- Add comprehensive unit tests for LinkSpeedProbeResult (23 tests)
- Add Python tests for backchannel expected_rate in propagated mode
This improves transfer time estimates by using actual measured
throughput from prior image transfers rather than heuristics.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Change ORIGINAL preset maxDimensionPx from Int.MAX_VALUE to 8192
to prevent OOM errors on large images (Android Canvas limit ~16K)
- Add negative transfer time validation in formatTransferTime()
to handle edge case of invalid probe data
- Fix backchannel_links cleanup: when a stale link is found during
establish_link(), clean up both direct_links and backchannel_links
dictionaries using both key formats (bytes and hex string)
- Update tests to expect new 8K dimension limit for ORIGINAL preset
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Test now expects 'success' instead of 'no_link' when propagated delivery
has no active link, matching the code change that returns success with
heuristic data for compression recommendations.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When no active propagation link exists, return status="success" instead
of "no_link" so Kotlin UI uses the nextHopBitrateBps heuristic for
compression recommendations instead of treating it as a failure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove conditional that only checked recipient_dest.hash when hashes
didn't match - links are stored under recipient_dest.hash so this check
must always run. Also clean up stale links from both direct_links and
backchannel_links when found.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Clean up stale/closed links when discovered during existing link check
- Use identity comparison in finally block to only remove our own link,
preventing concurrent calls from incorrectly removing each other's links
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clean up stored link and call teardown() when an exception occurs,
matching the cleanup done on timeout. Safely handles cases where
recipient_dest or link may not be defined yet.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, request_path() was called unconditionally even when a path
already existed. This generated unnecessary network broadcasts. Now we
only request a path if we don't have one. If the existing path is stale,
link establishment will fail and can be retried.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When link establishment times out, the link object was being removed
from direct_links but teardown() wasn't called. This left the underlying
RNS link in limbo, potentially consuming resources. Now properly closes
the link before removing from the dictionary.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add 35 new tests for link management in reticulum_wrapper.py:
- TestEstablishLink: 12 tests for establish_link()
- TestCloseLink: 7 tests for close_link()
- TestGetLinkStatus: 6 tests for get_link_status()
- TestProbeLinkSpeed: 8 tests for probe_link_speed()
- TestNextHopBitrate: 2 tests for next_hop_bitrate functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add LXMF mock in setUp/tearDown for TestOnLXMFDelivery tests
(LXMF is None in test environment, causing AttributeError)
- Update test_get_hop_count_returns_none to properly mock RNS.Transport
(function now implements hop count, test needed updating)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add missing delivery_method field to error returns in send_lxmf_message_with_method
- Use try/finally for temp image file cleanup to prevent leaks
- Add race condition guard in LinkSpeedProbe.probe() to skip if already probing
- Populate next_hop_bitrate_bps in probe_link_speed results from RNS interface
- Add try/finally bitmap recycling in ImageUtils to prevent memory leaks
- Fix None→0 conversion in RoutingManager helpers to omit field instead
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Instead of sending empty LXMF messages to probe link speed, now uses
the reliable establish_link() function. Also checks backchannel_links
for existing incoming links from peer.
This removes ~80 lines of complex probe message handling code.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Always request fresh path even when one exists (avoids stale routes)
- Check backchannel_links in addition to direct_links for existing links
- Links are bidirectional, so incoming links from peer count as "active"
This fixes the issue where Device A shows "Online" but Device B shows
"last seen X minutes ago" even though they have an active link between them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixed 3 bugs in establish_link() that prevented manual link establishment
from working while LXMF DIRECT messages succeeded:
1. Missing identity recall fallbacks - now tries destination hash,
identity hash, and local cache (matching send_lxmf_message pattern)
2. Reversed path request logic - now requests path when one doesn't
exist instead of when one already exists
3. Wrong key for storing links - now uses recipient_dest.hash
(created destination hash) instead of input hash
Also updated close_link() and get_link_status() with same hash
handling logic for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add ConversationLinkManager to track active links per conversation
- Open link when entering conversation, close after 5min inactivity
- Update online indicator to show "Online" when link is active
- Add establish_link(), close_link(), get_link_status() to Python wrapper
- Add AIDL methods and protocol support for link management
- LinkSpeedProbe now uses existing links from ConversationLinkManager
Note: Manual link establishment times out (investigating), but detecting
existing links from LXMF DIRECT message exchange works correctly.
🐾 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
Large images (>500KB) exceed Android Binder IPC transaction limits (~1MB).
Now large images are written to temp files and the path is passed through
AIDL instead of the raw bytes.
Changes:
- Add imageDataPath parameter to AIDL sendLxmfMessageWithMethod
- ServiceReticulumProtocol writes large images to temp files
- Python reads from file path and deletes temp after reading
- Cap loadBitmap to MAX_PREVIEW_DIMENSION regardless of preset
(prevents Canvas crash for ORIGINAL preset with huge images)
🐛 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
- Fix direct_links key lookup (LXMF uses hex strings, not bytes)
- Add find_direct_link() helper to check both key formats
- Fix float-to-long conversion for link rates from Python
- Reset probe state before starting new probe
- Check for late link establishment after timeout
🐛 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
- Send empty LXMF message to establish link instead of raw Link
- Add delivery method parameter to probe (direct vs propagated)
- Filter empty probe messages on receive side to prevent rendering
- Fix UnboundLocalError for has_text_content in message filtering
- Add next_hop_bitrate_bps field for heuristic-based recommendations
- Monitor message state for SENT/DELIVERED detection
🐛 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
Add ability to probe actual link speed to a destination by establishing
a Link and measuring the establishment rate. This provides end-to-end
path speed measurement that accounts for all intermediate hops.
Python layer:
- probe_link_speed() establishes link, measures rate, tears down
- Reuses existing active links if available
- Returns establishment_rate_bps, expected_rate_bps, rtt_seconds, hops
Kotlin layer:
- LinkSpeedProbeResult data class with helper methods
- Full AIDL/Service/Protocol stack integration
- Mock implementation for testing
Next: UI integration for quality selection dialog
🤖 Generated with [Letta Code](https://letta.com)
Co-Authored-By: Letta <noreply@letta.com>
1. PropagationTestBase: Use setUp/tearDown to properly scope the LXMF
mock instead of setting it at module level (which leaked between tests)
2. test_propagated_falls_back_to_direct_without_propagation_node: Updated
test to match new behavior - PROPAGATED now falls back to DIRECT when
no propagation node is set (instead of failing)
3. test_on_message_failed_retries_via_propagation: Set mock_message.fields
to empty dict so "5 in fields" check works correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The get_propagation_state() method uses LXMF.LXMRouter.PR_* constants to
map state integers to human-readable names. When LXMF was mocked as a
plain MagicMock(), these constants became MagicMock objects instead of
integers, causing the state lookup to fail.
Fix: Define all PR_* constants in the mock setup and set
reticulum_wrapper.LXMF to the mock (since the module-level variable
is only populated during initialize() which tests skip).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Move native stamp generator registration to pre-init callback
(prevents Python's multiprocessing.Manager() from hanging on Android)
- Start PropagationNodeManager early after service binding
- Add retry logic for relay sync when Python not yet initialized
- Fall back to DIRECT when PROPAGATED requested but no relay set
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Run ktlintFormat to fix formatting issues
- Refactor supersedePendingFileNotifications to use functional approach
instead of loop with multiple continues
- Extract tryParseNotificationMatch helper to reduce return statements
- Use PropagationState constants from ReticulumProtocol instead of
duplicating them in ServiceNotificationManager
- Simplify complex condition in MessageMapper with helper variables
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Defer pending file notification until propagation is confirmed on relay
- Track hasFileAttachments before extraction to fix supersede matching
- Add service-side supersede logic for pending file notifications
- Only show sync complete toast for manual syncs, not automatic/periodic
- Add _isManualSync flag to PropagationNodeManager for toast control
- Add real-time progress tracking with 1% threshold for callbacks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>