Move peer identity restoration to IO dispatcher to prevent blocking
main thread during JSON serialization. Also move interface status
polling to IO dispatcher to fix network status screen lag.
Changes:
- ColumbaApplication: Run restorePeerIdentities on Dispatchers.IO
- InterfaceManagementViewModel: Add injectable ioDispatcher for polling
- ConversationRepository: Remove per-identity logging (930 log calls)
- Update tests to inject test dispatcher for IO operations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a selected relay is offline and message propagation fails, the system
now automatically tries alternative relays before marking the message as
permanently failed. Also adds a manual "Retry" option in the message context
menu for failed messages.
Key changes:
- Python: Modified _on_message_failed() to request alternative relays from
Kotlin when propagation fails, with tracking to prevent infinite loops
- Kotlin: Added getAlternativeRelay() to PropagationNodeManager to find the
nearest available relay excluding previously tried ones
- IPC: Added onAlternativeRelayRequested callback and provideAlternativeRelay
method for Python-Kotlin communication
- UI: Added "Retry" menu item for failed messages and retryFailedMessage()
in MessagingViewModel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix TooManyFunctions in PropagationNodeManager by removing restartPeriodicSync
- Fix TooManyFunctions in MessagingViewModel by extracting helpers to top-level
- Fix CyclomaticComplexity in sendMessage by refactoring into smaller methods
- Fix DestructuringDeclarationWithTooManyEntries in MessageDetailScreen
- Remove runBlocking antipattern from PropagationNodeManager StateFlow init
- Apply ktlint formatting fixes across codebase
- Update tests for refactored code
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: During PropagationNodeManager singleton construction,
getMyRelay() returned null because localIdentityDao.getActiveIdentitySync()
had no active identity set yet. Additionally, loadSettings() in
SettingsViewModel was overwriting relay state from startRelayMonitor().
Changes:
- Add getAnyMyRelay() query to ContactDao (no identity filter)
- Add getAnyRelay() fallback method to ContactRepository
- Update getInitialRelayInfo() to use fallback when active identity unavailable
- Preserve relay state (currentRelayName, currentRelayHops, autoSelectPropagationNode)
when loadSettings() updates state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add "View Details" option to message context menu for sent messages.
New MessageDetailScreen shows:
- Timestamp when message was sent
- Delivery status (pending/sent/delivered/failed)
- Delivery method (opportunistic/direct/propagated)
- Error details for failed messages
Database changes:
- Add deliveryMethod and errorMessage columns to messages table
- Migration 22→23 for schema update
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix UnsafeCallOnNullableType by capturing nullable values to local vars
- Replace `?: ""` with `.orEmpty()` for idiomatic Kotlin
- Remove redundant suspend modifiers on non-suspending functions
- Fix TestFactories LongParameterList by using Config data classes
- Add justified @Suppress annotations for:
- TooManyFunctions on ContactDao (expected for DAOs)
- LargeClass on SettingsViewModel (many UI interactions)
- CyclomaticComplexMethod on ContactEntity.equals (ByteArray handling)
- InjectDispatcher on bridge classes (no DI in low-level bridges)
- Remove unused onSelectRelay parameter from MessageDeliveryCard
- Change RuntimeException to error() in PythonReticulumProtocol
- Regenerate detekt baselines for all modules
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add TestFactories.kt with shared test helpers and valid hex constants
- Add PropagationNodeManagerTest.kt (35 tests) for relay auto-selection
- Add ContactsViewModelTest.kt (~30 tests) for contacts/relay UI
- Expand ContactRepositoryTest.kt with 12 relay management tests
- Fix existing tests for API changes (sendLxmfMessageWithMethod)
- Fix AnnounceStreamViewModelTest and SettingsViewModelTest dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add PropagationNodeManager.start() call in ColumbaApplication after
Reticulum initialization to enable relay auto-selection on app startup
- Fix ContactsScreen empty state condition to check for relay contact,
preventing "No contacts yet" from showing when only a relay exists
- Add debug logging throughout relay selection flow for troubleshooting
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implement comprehensive relay (propagation node) support:
- Add PropagationNodeManager for auto-selection of nearest relay by hop count
- Add "Set as My Relay" button on node details for propagation nodes
- Add "MY RELAY" section in contacts screen (separate from pinned)
- Show "(auto)" badge when relay is auto-selected vs manually chosen
- Add "Unset as Your Relay?" confirmation dialog with auto-selection explanation
- Add relay management methods to ContactDao (setAsMyRelay, clearMyRelay, getMyRelay)
- Add isMyRelay field to ContactEntity and EnrichedContact
- Add message delivery settings (default method, retry via relay on fail)
- Add Python layer support for propagation node configuration
- Add instrumented tests for relay DAO operations
The relay system follows Sideband's algorithm: auto-select nearest node,
only switch if new node has fewer or equal hops. Users can manually select
a relay which disables auto-selection until re-enabled.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Use single return with chained conditions instead of multiple early
returns to reduce cyclomatic complexity. Extract nullable ByteArray
comparison to helper function.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add real-time validation in updateSpreadingFactor, updateCodingRate,
and updateBandwidth functions to show errors as the user types:
- Spreading Factor: 7-12
- Coding Rate: 5-8
- Bandwidth: 7.8 kHz - 1625 kHz
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
RNode Auto-Reconnection:
- RNodeCompanionService now triggers reconnection when CompanionDeviceManager
detects the RNode has reappeared after going out of BLE range
- Add reconnectRNodeInterface() to AIDL interface and ReticulumServiceBinder
- Add thread-safe initialization lock in reticulum_wrapper.py to prevent
concurrent RNode initialization race conditions
- Use 2-second debounce delay before reconnecting to ensure device stability
Interface Status UI Improvements:
- InterfaceManagementViewModel now polls Reticulum every 3 seconds for
interface online/offline status
- Update isBleInterface() to include RNode type for proper BLE handling
- Add "Interface Offline" error state to getErrorMessage() for enabled
interfaces that aren't passing traffic
- Make error badges clickable to show detailed error dialog
- Add InterfaceErrorDialog component for detailed interface issue info
- IdentityScreen: make offline interface rows clickable for troubleshooting
Build & Deploy:
- deploy.sh now supports multiple connected devices, deploying to all of
them in sequence instead of requiring a single device
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements RNode interface support for LoRa communication via paired
Bluetooth RNode devices. Uses a Kotlin Bridge architecture where Kotlin
handles Bluetooth I/O and Python handles the KISS protocol.
- **KotlinRNodeBridge**: Handles Bluetooth Classic (SPP/RFCOMM) and BLE
(Nordic UART Service) connections to RNode hardware. Manages connection
lifecycle, data buffering, and provides read/write APIs to Python.
- **ColumbaRNodeInterface**: Python interface implementing KISS protocol
for RNode communication. Handles frame escaping, command parsing, radio
configuration, and integrates with RNS Transport layer.
- **UI Components**: Added RNode configuration fields to InterfaceConfigDialog
including device name selector, connection mode (Classic/BLE), frequency,
bandwidth, spreading factor, coding rate, and TX power settings.
- Supports both Bluetooth Classic (UUID: 00001101-0000-1000-8000-00805F9B34FB)
and BLE via Nordic UART Service (UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e)
- Thread-safe circular buffer for BLE packet reassembly
- Automatic device discovery from paired devices list
- Connection state management with callbacks
- Full KISS protocol implementation (FEND/FESC escape sequences)
- RNode detection and firmware version validation
- Radio parameter configuration (frequency, bandwidth, SF, CR, TX power)
- Airtime limiting support (short-term and long-term)
- Required RNS Transport interface attributes for compatibility
- set_rnode_bridge() to receive Kotlin bridge reference
- initialize_rnode_interface() called during bridge setup
- RNode interface registered with RNS.Transport.interfaces
1. **Chaquopy ByteArray conversion**: Raw bytes from Kotlin needed explicit
`bytes()` conversion in Python due to Chaquopy's jarray handling.
2. **KISS frame format**: Initial detection commands were missing FEND
delimiters, causing RNode to not respond to detection requests.
3. **RNS Transport compatibility**: Required iteratively adding interface
attributes (bitrate, rxb, txb, mode, mtu, HW_MTU, FIXED_MTU,
AUTOCONFIGURE_MTU, announce_rate_target, ifac_size, etc.) and methods
(sent_announce(), received_announce(), process_held_announces(),
should_ingress_limit()) to satisfy RNS Transport requirements.
4. **Owner inbound routing**: Changed from owner.inbound() to direct
RNS.Transport.inbound() calls since owner was ReticulumWrapper, not
Transport.
Successfully tested bidirectional communication:
- Announces sent and received between Columba and Sideband via LoRa
- Links established with ~1.8s RTT over LoRa
- Messages delivered from Columba to Sideband
- Messages received from Sideband (routing to correct identity required)
- python/rnode_interface.py (NEW): KISS protocol and RNode interface
- reticulum/rnode/KotlinRNodeBridge.kt (NEW): Bluetooth bridge
- python/reticulum_wrapper.py: RNode bridge integration
- ReticulumServiceBinder.kt: Bridge initialization in setupBridges()
- InterfaceConfigDialog.kt: RNode UI configuration fields
- InterfaceManagementViewModel.kt: RNode state management
- ReticulumConfig.kt: RNode data model with targetDeviceName, connectionMode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extract and display stamp cost information from LXMF announces:
- Propagation nodes: show stamp cost with flexibility range and peering cost
- Regular peers: show stamp cost when available
Changes:
- Python: extract stamp costs using LXMF canonical functions
- Add stampCost, stampCostFlexibility, peeringCost to AnnounceEvent model
- Pass stamp costs through PollingManager and ServiceReticulumProtocol
- Add database migration 20->21 for new columns
- Display stamp cost info cards in AnnounceDetailScreen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add @Suppress("LongParameterList") to buildThemeEntity in CustomThemeRepository
(entity builder naturally requires all fields)
- Add @Suppress("ThrowsCount") to getLxmfDestination and getLxmfIdentity
(multiple validation checks require distinct error messages)
- Refactor to use checkNotNull() and require() for cleaner validation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Follow-up cleanup to remove parameters that are no longer used after
the display name fix. Resolves detekt UnusedParameter warnings.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Stop copying announce/conversation names to customNickname field,
allowing display names to reflect the latest announce name via the
existing COALESCE fallback.
Includes migration to clear stale auto-populated nicknames for existing
contacts while preserving user-customized ones.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Enable importing contacts using only a 32-character destination hash
from Sideband's "Copy Address" feature, in addition to full lxma:// URLs.
Key changes:
- Add ContactStatus enum (ACTIVE, PENDING_IDENTITY, UNRESOLVED)
- Make publicKey nullable in ContactEntity for pending contacts
- Add parseIdentityInput() to InputValidator for flexible input parsing
- Add IdentityResolutionManager for background identity resolution
- Check existing announces when adding hash-only contacts
- Update UI with pending/unresolved status indicators
- Add PendingContactBottomSheet for managing unresolved contacts
- Hook announce callbacks for instant resolution when peer announces
- Add recallIdentity() AIDL method to check Reticulum's identity cache
- Database migration 17->18 for new schema
The flow:
1. User pastes 32-char hash -> check announces table for existing identity
2. If found -> add as ACTIVE contact immediately
3. If not found -> add as PENDING_IDENTITY, background resolution kicks in
4. When announce received -> instantly resolve pending contact
5. After 48h timeout -> mark as UNRESOLVED with retry option
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add path traversal validation in extractAttachmentsFromZip() to prevent
malicious ZIP entries from writing files outside attachments directory
- Wrap main database operations in transaction for atomicity - if any step
fails, all changes are rolled back to prevent partial imports
- Use IGNORE conflict strategy for message import to preserve existing
messages and prevent LXMF replay from overwriting timestamps
- Track imported identities and filter dependent data (conversations,
messages, contacts) to prevent orphaned records if identity import fails
- Add minimum version check for backwards compatibility with old exports
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add peer_identities table to migration export/import
- Added PeerIdentityExport to MigrationData.kt
- Added batch insert to PeerIdentityDao
- Bumped migration version to 5
- Updated tests for new fields
- Fix message ordering after import
- Prevent LXMF replay from overwriting imported message timestamps
- Skip inserting messages that already exist in database
- Root cause: saveMessage() always inserted with REPLACE strategy,
overwriting original timestamps when LXMF replays buffered messages
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add ensureIdentityFileExists() to IdentityRepository to verify/recover
identity files from keyData backup before service restart
- Update InterfaceConfigManager to use canonical identity_<hash> paths
instead of fragile default_identity file
- Add Python safety check to refuse creating new identity when specific
path was requested but file is missing
- Remove copy-to-default_identity logic from IdentityManagerViewModel
- Add unit tests for identity file recovery scenarios
This fixes the bug where the Python service would silently create a new
identity when the default_identity file was deleted during service restart,
causing the UI to show different identities on different screens.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implements a complete data migration system allowing users to export all app
data from the old app (com.lxmf.messenger) and import it into the new app
(tech.torlando.columba) when changing applicationId for F-Droid publishing.
## Feature Overview
Users can export their data to a `.columba` file (ZIP archive) containing:
- manifest.json: Serialized MigrationBundle with all data
- attachments/: Directory with message attachments
The import process restores all data and restarts the Reticulum service to
apply the imported identities and peer information.
## Data Migrated
| Data Type | Description |
|-----------|-------------|
| Identities | Private keys, display names, destination hashes |
| Conversations | Peer info, last message, unread counts |
| Messages | Full message history with status and timestamps |
| Contacts | Custom nicknames, notes, tags, pinned status |
| Announces | Known peers with public keys for network recognition |
| Settings | Notifications, auto-announce, theme preferences |
| Attachments | Message attachments (images, files, etc.) |
## New Files
- `migration/MigrationData.kt` - Data classes for serialization
- `migration/MigrationExporter.kt` - Export logic with progress callbacks
- `migration/MigrationImporter.kt` - Import logic with validation
- `viewmodel/MigrationViewModel.kt` - UI state management
- `ui/screens/MigrationScreen.kt` - Export/import UI
- `ui/screens/settings/cards/DataMigrationCard.kt` - Settings integration
## Key Implementation Details
### Export Flow
1. Collect all data from Room database
2. Base64-encode binary fields (keys, attachments)
3. Serialize to JSON manifest
4. Create ZIP archive with manifest + attachments
5. Share via FileProvider
### Import Flow
1. Parse ZIP and validate manifest version
2. Import identities (with Reticulum key recovery)
3. Bulk insert conversations, messages, contacts
4. Import announces to database
5. Extract attachments to app storage
6. Apply settings and mark onboarding complete
7. Restart service to apply changes
### Announce Restoration
InterfaceConfigManager Step 9b restores all announce peer identities to
Python Reticulum's known_destinations cache on service restart, enabling
immediate peer recognition without re-announcing.
## DAO Additions
- `AnnounceDao`: getAllAnnouncesSync(), insertAnnounces(), getAnnounceCount()
- `ContactDao`: getAllContactsSync(), insertContacts()
- `ConversationDao`: getAllConversationsList(), insertConversations()
- `MessageDao`: getAllMessagesForIdentity(), insertMessages()
## Dependencies
Added kotlinx-serialization for JSON serialization of migration bundles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>