From 9ba8dca82a1e98bbd89e84e63ae913eb27fa0dd7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 8 Jul 2025 23:38:35 +0200 Subject: [PATCH] seems to work --- RSSI_COLOR_FIX_SUMMARY.md | 80 ++++ .../bitchat/android/mesh/FragmentManager.kt | 270 ++++++++++++++ .../bitchat/android/mesh/MessageHandler.kt | 346 ++++++++++++++++++ .../bitchat/android/mesh/PacketProcessor.kt | 184 ++++++++++ .../com/bitchat/android/mesh/PeerManager.kt | 257 +++++++++++++ .../bitchat/android/mesh/SecurityManager.kt | 319 ++++++++++++++++ .../android/mesh/StoreForwardManager.kt | 315 ++++++++++++++++ demo_colors.md | 62 ++++ refactoring_plan.md | 73 ++++ test_color.kt | 51 +++ 10 files changed, 1957 insertions(+) create mode 100644 RSSI_COLOR_FIX_SUMMARY.md create mode 100644 app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt create mode 100644 app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt create mode 100644 app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt create mode 100644 app/src/main/java/com/bitchat/android/mesh/PeerManager.kt create mode 100644 app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt create mode 100644 app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt create mode 100644 demo_colors.md create mode 100644 refactoring_plan.md create mode 100644 test_color.kt diff --git a/RSSI_COLOR_FIX_SUMMARY.md b/RSSI_COLOR_FIX_SUMMARY.md new file mode 100644 index 00000000..78c616c5 --- /dev/null +++ b/RSSI_COLOR_FIX_SUMMARY.md @@ -0,0 +1,80 @@ +# RSSI Color Change Fix - Implementation Summary + +## Problem Identified +The username colors in the chat were not changing based on RSSI signal strength even though RSSI values were being logged as changing. Investigation revealed that: + +1. **RSSI values were only captured once** during the initial BLE scan discovery +2. **No mechanism existed** to continuously update RSSI values from connected devices +3. **UI was not being notified** of RSSI changes to trigger recomposition +4. **Chat rendering was using stale RSSI values** that never changed after initial connection + +## Root Cause +The `BluetoothMeshService` was only recording RSSI during the scan phase (`handleScanResult`) but never updating it during the connection lifetime. Bluetooth GATT provides `readRemoteRssi()` for connected devices, but this wasn't being used. + +## Solution Implemented + +### 1. Device-to-Peer ID Mapping +- Added `deviceToPeerIDMapping` to link Bluetooth devices to peer IDs +- This allows RSSI updates to be associated with the correct peer ID + +### 2. GATT RSSI Reading Callback +- Added `onReadRemoteRssi()` callback in the GATT client callback +- Updates `peerRSSI` map when new RSSI values are read +- Maps device addresses to peer IDs for proper tracking + +### 3. Periodic RSSI Monitoring +- Added background coroutine that runs every 5 seconds +- Calls `readRemoteRssi()` on all active GATT connections +- Provides continuous RSSI updates during connection lifetime + +### 4. UI Notification System +- Added `didUpdateRSSI()` delegate method to notify UI of RSSI changes +- When RSSI changes, the delegate is called to potentially trigger UI updates +- Chat screen automatically recomposes when RSSI values change + +### 5. Key Exchange Enhancement +- Modified `handleKeyExchange()` to map devices to peer IDs +- Transfers any existing peripheral RSSI data to peer-based tracking +- Ensures proper RSSI association after peer identification + +## Code Changes Made + +### BluetoothMeshService.kt +1. **Added device mapping**: `deviceToPeerIDMapping` concurrent hash map +2. **RSSI callback**: `onReadRemoteRssi()` implementation in GATT callback +3. **Periodic monitoring**: `monitorRSSI()` function called every 5 seconds +4. **Key exchange update**: Links devices to peer IDs for RSSI tracking +5. **Delegate method**: `didUpdateRSSI()` interface method for UI notifications + +### ChatViewModel.kt +1. **Delegate implementation**: Added `didUpdateRSSI()` method +2. **Logging**: Debug output when RSSI values change + +### ChatScreen.kt +- **No changes needed** - existing `getRSSIColor(rssi)` function already works +- UI automatically recomposes when `meshService.getPeerRSSI()` returns updated values + +## How It Works Now + +1. **Initial Discovery**: RSSI captured during BLE scan (as before) +2. **Connection**: Device mapped to peer ID during key exchange +3. **Monitoring**: Every 5 seconds, `readRemoteRssi()` called on connected devices +4. **Update**: RSSI callback updates `peerRSSI` map with new values +5. **Notification**: Delegate notified of RSSI change +6. **Rendering**: Chat screen uses updated RSSI values for color calculation +7. **Recomposition**: UI automatically updates with new colors + +## Expected Behavior +- Username colors now change dynamically as users move closer/farther away +- Colors reflect real-time signal strength: green (strong) → yellow (medium) → red (weak) +- Updates occur every 5 seconds while devices are connected +- Your own username remains green regardless of signal strength +- Works for both client and server GATT connections + +## Testing +- Build successful with `./gradlew assembleDebug` +- No compilation errors +- All existing functionality preserved +- Ready for testing with actual devices + +The fix addresses the core issue where RSSI values were static after connection. Now they continuously update, providing the dynamic color feedback based on signal strength that was originally intended. diff --git a/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt new file mode 100644 index 00000000..4f21e295 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/FragmentManager.kt @@ -0,0 +1,270 @@ +package com.bitchat.android.mesh + +import android.util.Log +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import kotlinx.coroutines.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Manages message fragmentation and reassembly + * Extracted from BluetoothMeshService for better separation of concerns + */ +class FragmentManager { + + companion object { + private const val TAG = "FragmentManager" + private const val MAX_FRAGMENT_SIZE = 500 + private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds + private const val CLEANUP_INTERVAL = 10000L // 10 seconds + } + + // Fragment storage + private val incomingFragments = ConcurrentHashMap>() + private val fragmentMetadata = ConcurrentHashMap>() // originalType, totalFragments, timestamp + + // Delegate for callbacks + var delegate: FragmentManagerDelegate? = null + + // Coroutines + private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + startPeriodicCleanup() + } + + /** + * Create fragments from a large packet + */ + fun createFragments(packet: BitchatPacket): List { + val data = packet.toBinaryData() ?: return emptyList() + + if (data.size <= MAX_FRAGMENT_SIZE) { + return listOf(packet) // No fragmentation needed + } + + val fragments = mutableListOf() + val fragmentID = generateFragmentID() + + // Calculate header size (13 bytes for fragment metadata) + val headerSize = 13 + val dataPerFragment = MAX_FRAGMENT_SIZE - headerSize + val totalFragments = (data.size + dataPerFragment - 1) / dataPerFragment + + Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet") + + for (i in 0 until totalFragments) { + val start = i * dataPerFragment + val end = minOf(start + dataPerFragment, data.size) + val fragmentData = data.sliceArray(start until end) + + val fragmentPayload = createFragmentPayload( + fragmentID = fragmentID, + index = i, + total = totalFragments, + originalType = packet.type, + data = fragmentData + ) + + val fragmentType = when (i) { + 0 -> MessageType.FRAGMENT_START + totalFragments - 1 -> MessageType.FRAGMENT_END + else -> MessageType.FRAGMENT_CONTINUE + } + + val fragmentPacket = BitchatPacket( + type = fragmentType.value, + ttl = packet.ttl, + senderID = packet.senderID, + recipientID = packet.recipientID, + timestamp = packet.timestamp, + payload = fragmentPayload, + signature = null // Fragments aren't individually signed + ) + + fragments.add(fragmentPacket) + } + + return fragments + } + + /** + * Handle incoming fragment + */ + fun handleFragment(packet: BitchatPacket): BitchatPacket? { + if (packet.payload.size < 13) { + Log.w(TAG, "Fragment packet too small: ${packet.payload.size}") + return null + } + + try { + // Extract fragment metadata (same format as iOS) + val fragmentIDData = packet.payload.sliceArray(0..7) + val fragmentID = fragmentIDData.contentHashCode().toString() + + val index = ((packet.payload[8].toInt() and 0xFF) shl 8) or (packet.payload[9].toInt() and 0xFF) + val total = ((packet.payload[10].toInt() and 0xFF) shl 8) or (packet.payload[11].toInt() and 0xFF) + val originalType = packet.payload[12].toUByte() + val fragmentData = packet.payload.sliceArray(13 until packet.payload.size) + + Log.d(TAG, "Received fragment $index/$total for fragmentID: $fragmentID, originalType: $originalType") + + // Store fragment + if (!incomingFragments.containsKey(fragmentID)) { + incomingFragments[fragmentID] = mutableMapOf() + fragmentMetadata[fragmentID] = Triple(originalType, total, System.currentTimeMillis()) + } + + incomingFragments[fragmentID]?.put(index, fragmentData) + + // Check if we have all fragments + if (incomingFragments[fragmentID]?.size == total) { + Log.d(TAG, "All fragments received for $fragmentID, reassembling...") + + // Reassemble message + val reassembledData = mutableListOf() + for (i in 0 until total) { + incomingFragments[fragmentID]?.get(i)?.let { data -> + reassembledData.addAll(data.asIterable()) + } + } + + // Parse and return reassembled packet + val reassembledPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray()) + + // Cleanup + incomingFragments.remove(fragmentID) + fragmentMetadata.remove(fragmentID) + + if (reassembledPacket != null) { + Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes") + return reassembledPacket + } else { + Log.e(TAG, "Failed to parse reassembled packet") + } + } else { + val received = incomingFragments[fragmentID]?.size ?: 0 + Log.d(TAG, "Fragment $index stored, have $received/$total fragments for $fragmentID") + } + + } catch (e: Exception) { + Log.e(TAG, "Failed to handle fragment: ${e.message}") + } + + return null + } + + /** + * Create fragment payload with metadata + */ + private fun createFragmentPayload( + fragmentID: String, + index: Int, + total: Int, + originalType: UByte, + data: ByteArray + ): ByteArray { + val payload = ByteArray(13 + data.size) + + // Fragment ID (8 bytes) + val idBytes = fragmentID.toByteArray() + System.arraycopy(idBytes, 0, payload, 0, minOf(8, idBytes.size)) + + // Index (2 bytes, big-endian) + payload[8] = ((index shr 8) and 0xFF).toByte() + payload[9] = (index and 0xFF).toByte() + + // Total (2 bytes, big-endian) + payload[10] = ((total shr 8) and 0xFF).toByte() + payload[11] = (total and 0xFF).toByte() + + // Original type (1 byte) + payload[12] = originalType.toByte() + + // Fragment data + System.arraycopy(data, 0, payload, 13, data.size) + + return payload + } + + /** + * Generate unique fragment ID + */ + private fun generateFragmentID(): String { + return "${System.currentTimeMillis()}-${kotlin.random.Random.nextInt()}" + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("=== Fragment Manager Debug Info ===") + appendLine("Active Fragment Sets: ${incomingFragments.size}") + fragmentMetadata.forEach { (fragmentID, metadata) -> + val (originalType, totalFragments, timestamp) = metadata + val received = incomingFragments[fragmentID]?.size ?: 0 + val ageSeconds = (System.currentTimeMillis() - timestamp) / 1000 + appendLine(" - $fragmentID: $received/$totalFragments fragments, type: $originalType, age: ${ageSeconds}s") + } + } + } + + /** + * Start periodic cleanup of old fragments + */ + private fun startPeriodicCleanup() { + managerScope.launch { + while (isActive) { + delay(CLEANUP_INTERVAL) + cleanupOldFragments() + } + } + } + + /** + * Clean up old fragments (older than 30 seconds) + */ + private fun cleanupOldFragments() { + val cutoffTime = System.currentTimeMillis() - FRAGMENT_TIMEOUT + val fragmentsToRemove = mutableListOf() + + fragmentMetadata.entries.forEach { (fragmentID, metadata) -> + if (metadata.third < cutoffTime) { + fragmentsToRemove.add(fragmentID) + } + } + + fragmentsToRemove.forEach { fragmentID -> + incomingFragments.remove(fragmentID) + fragmentMetadata.remove(fragmentID) + } + + if (fragmentsToRemove.isNotEmpty()) { + Log.d(TAG, "Cleaned up ${fragmentsToRemove.size} old fragment sets") + } + } + + /** + * Clear all fragments + */ + fun clearAllFragments() { + incomingFragments.clear() + fragmentMetadata.clear() + } + + /** + * Shutdown the manager + */ + fun shutdown() { + managerScope.cancel() + clearAllFragments() + } +} + +/** + * Delegate interface for fragment manager callbacks + */ +interface FragmentManagerDelegate { + fun onPacketReassembled(packet: BitchatPacket) +} diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt new file mode 100644 index 00000000..d25caf1c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -0,0 +1,346 @@ +package com.bitchat.android.mesh + +import android.util.Log +import com.bitchat.android.crypto.MessagePadding +import com.bitchat.android.model.BitchatMessage +import com.bitchat.android.model.DeliveryAck +import com.bitchat.android.model.ReadReceipt +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import kotlinx.coroutines.* +import java.util.* +import kotlin.random.Random + +/** + * Handles processing of different message types + * Extracted from BluetoothMeshService for better separation of concerns + */ +class MessageHandler(private val myPeerID: String) { + + companion object { + private const val TAG = "MessageHandler" + } + + // Delegate for callbacks + var delegate: MessageHandlerDelegate? = null + + // Coroutines + private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + /** + * Handle announce message + */ + suspend fun handleAnnounce(packet: BitchatPacket, peerID: String): Boolean { + if (peerID == myPeerID) return false + + val nickname = String(packet.payload, Charsets.UTF_8) + Log.d(TAG, "Received announce from $peerID: $nickname") + + // Notify delegate to handle peer management + val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false + + // Relay announce if TTL > 0 + if (packet.ttl > 1u) { + val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) + delay(Random.nextLong(100, 300)) + delegate?.relayPacket(relayPacket) + } + + return isFirstAnnounce + } + + /** + * Handle broadcast or private message + */ + suspend fun handleMessage(packet: BitchatPacket, peerID: String) { + if (peerID == myPeerID) return + + val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) } + + if (recipientID == null) { + // BROADCAST MESSAGE + handleBroadcastMessage(packet, peerID) + } else if (String(recipientID).replace("\u0000", "") == myPeerID) { + // PRIVATE MESSAGE FOR US + handlePrivateMessage(packet, peerID) + } else if (packet.ttl > 0u) { + // RELAY MESSAGE + relayMessage(packet) + } + } + + /** + * Handle broadcast message + */ + private suspend fun handleBroadcastMessage(packet: BitchatPacket, peerID: String) { + try { + // Parse message + val message = BitchatMessage.fromBinaryPayload(packet.payload) + if (message != null) { + // Check for cover traffic (dummy messages) + if (message.content.startsWith("☂DUMMY☂")) { + Log.d(TAG, "Discarding cover traffic from $peerID") + return // Silently discard + } + + delegate?.updatePeerNickname(peerID, message.sender) + + // Handle encrypted channel messages + val finalContent = if (message.channel != null && message.isEncrypted && message.encryptedContent != null) { + delegate?.decryptChannelMessage(message.encryptedContent, message.channel) + ?: "[Encrypted message - password required]" + } else { + message.content + } + + // Replace timestamp with current time (same as iOS) + val messageWithCurrentTime = message.copy( + content = finalContent, + senderPeerID = peerID, + timestamp = Date() // Use current time instead of original timestamp + ) + + delegate?.onMessageReceived(messageWithCurrentTime) + } + + // Relay broadcast messages + relayMessage(packet) + + } catch (e: Exception) { + Log.e(TAG, "Failed to process broadcast message: ${e.message}") + } + } + + /** + * Handle private message addressed to us + */ + private suspend fun handlePrivateMessage(packet: BitchatPacket, peerID: String) { + try { + // Verify signature if present + if (packet.signature != null && !delegate?.verifySignature(packet, peerID)!!) { + Log.w(TAG, "Invalid signature for private message from $peerID") + return + } + + // Decrypt message + val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) + if (decryptedData == null) { + Log.e(TAG, "Failed to decrypt private message from $peerID") + return + } + + val unpaddedData = MessagePadding.unpad(decryptedData) + + // Parse message + val message = BitchatMessage.fromBinaryPayload(unpaddedData) + if (message != null) { + // Check for cover traffic (dummy messages) + if (message.content.startsWith("☂DUMMY☂")) { + Log.d(TAG, "Discarding private cover traffic from $peerID") + return // Silently discard + } + + delegate?.updatePeerNickname(peerID, message.sender) + + // Replace timestamp with current time (same as iOS) + val messageWithCurrentTime = message.copy( + senderPeerID = peerID, + timestamp = Date() // Use current time instead of original timestamp + ) + + delegate?.onMessageReceived(messageWithCurrentTime) + + // Send delivery ACK + sendDeliveryAck(message, peerID) + } + + } catch (e: Exception) { + Log.e(TAG, "Failed to process private message from $peerID: ${e.message}") + } + } + + /** + * Handle leave message + */ + suspend fun handleLeave(packet: BitchatPacket, peerID: String) { + val content = String(packet.payload, Charsets.UTF_8) + + if (content.startsWith("#")) { + // Channel leave + delegate?.onChannelLeave(content, peerID) + } else { + // Peer disconnect + val nickname = delegate?.getPeerNickname(peerID) + delegate?.removePeer(peerID) + if (nickname != null) { + delegate?.onPeerDisconnected(nickname) + } + } + + // Relay if TTL > 0 + if (packet.ttl > 1u) { + val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) + delegate?.relayPacket(relayPacket) + } + } + + /** + * Handle delivery acknowledgment + */ + suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { + if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { + try { + val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) + if (decryptedData != null) { + val ack = DeliveryAck.decode(decryptedData) + if (ack != null) { + delegate?.onDeliveryAckReceived(ack) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to decrypt delivery ACK: ${e.message}") + } + } else if (packet.ttl > 0u) { + // Relay + val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) + delegate?.relayPacket(relayPacket) + } + } + + /** + * Handle read receipt + */ + suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) { + if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) { + try { + val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID) + if (decryptedData != null) { + val receipt = ReadReceipt.decode(decryptedData) + if (receipt != null) { + delegate?.onReadReceiptReceived(receipt) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to decrypt read receipt: ${e.message}") + } + } else if (packet.ttl > 0u) { + // Relay + val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) + delegate?.relayPacket(relayPacket) + } + } + + /** + * Relay message with adaptive probability (same as iOS) + */ + private suspend fun relayMessage(packet: BitchatPacket) { + if (packet.ttl == 0u.toUByte()) return + + val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) + + // Check network size and apply adaptive relay probability + val networkSize = delegate?.getNetworkSize() ?: 1 + val relayProb = when { + networkSize <= 10 -> 1.0 + networkSize <= 30 -> 0.85 + networkSize <= 50 -> 0.7 + networkSize <= 100 -> 0.55 + else -> 0.4 + } + + val shouldRelay = relayPacket.ttl >= 4u || networkSize <= 3 || Random.nextDouble() < relayProb + + if (shouldRelay) { + val delay = Random.nextLong(50, 500) // Random delay like iOS + delay(delay) + delegate?.relayPacket(relayPacket) + } + } + + /** + * Send delivery acknowledgment for a received private message + */ + private fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) { + handlerScope.launch { + val nickname = delegate?.getMyNickname() ?: myPeerID + val ack = DeliveryAck( + originalMessageID = message.id, + recipientID = myPeerID, + recipientNickname = nickname, + hopCount = 0u // Will be calculated during relay + ) + + try { + val ackData = ack.encode() ?: return@launch + val encryptedPayload = delegate?.encryptForPeer(ackData, senderPeerID) + if (encryptedPayload != null) { + val packet = BitchatPacket( + type = MessageType.DELIVERY_ACK.value, + senderID = myPeerID.toByteArray(), + recipientID = senderPeerID.toByteArray(), + timestamp = System.currentTimeMillis().toULong(), + payload = encryptedPayload, + signature = null, + ttl = 3u + ) + + delegate?.sendPacket(packet) + } + + } catch (e: Exception) { + Log.e(TAG, "Failed to send delivery ACK: ${e.message}") + } + } + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("=== Message Handler Debug Info ===") + appendLine("Handler Scope Active: ${handlerScope.isActive}") + appendLine("My Peer ID: $myPeerID") + } + } + + /** + * Shutdown the handler + */ + fun shutdown() { + handlerScope.cancel() + } +} + +/** + * Delegate interface for message handler callbacks + */ +interface MessageHandlerDelegate { + // Peer management + fun addOrUpdatePeer(peerID: String, nickname: String): Boolean + fun removePeer(peerID: String) + fun updatePeerNickname(peerID: String, nickname: String) + fun getPeerNickname(peerID: String): String? + fun getNetworkSize(): Int + fun getMyNickname(): String? + + // Packet operations + fun sendPacket(packet: BitchatPacket) + fun relayPacket(packet: BitchatPacket) + fun getBroadcastRecipient(): ByteArray + + // Cryptographic operations + fun verifySignature(packet: BitchatPacket, peerID: String): Boolean + fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? + fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? + + // Message operations + fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? + + // Callbacks + fun onMessageReceived(message: BitchatMessage) + fun onChannelLeave(channel: String, fromPeer: String) + fun onPeerDisconnected(nickname: String) + fun onDeliveryAckReceived(ack: DeliveryAck) + fun onReadReceiptReceived(receipt: ReadReceipt) +} diff --git a/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt new file mode 100644 index 00000000..35084d27 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/PacketProcessor.kt @@ -0,0 +1,184 @@ +package com.bitchat.android.mesh + +import android.util.Log +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import kotlinx.coroutines.* + +/** + * Processes incoming packets and routes them to appropriate handlers + * Extracted from BluetoothMeshService for better separation of concerns + */ +class PacketProcessor(private val myPeerID: String) { + + companion object { + private const val TAG = "PacketProcessor" + } + + // Delegate for callbacks + var delegate: PacketProcessorDelegate? = null + + // Coroutines + private val processorScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + /** + * Process received packet - main entry point for all incoming packets + */ + fun processPacket(packet: BitchatPacket, peerID: String) { + processorScope.launch { + handleReceivedPacket(packet, peerID) + } + } + + /** + * Handle received packet - core protocol logic (exact same as iOS) + */ + private suspend fun handleReceivedPacket(packet: BitchatPacket, peerID: String) { + // Basic validation and security checks + if (!delegate?.validatePacketSecurity(packet, peerID)!!) { + Log.d(TAG, "Packet failed security validation from $peerID") + return + } + + // Update last seen timestamp + delegate?.updatePeerLastSeen(peerID) + + Log.d(TAG, "Processing packet type ${packet.type} from $peerID") + + // Process based on message type (exact same logic as iOS) + when (MessageType.fromValue(packet.type)) { + MessageType.KEY_EXCHANGE -> handleKeyExchange(packet, peerID) + MessageType.ANNOUNCE -> handleAnnounce(packet, peerID) + MessageType.MESSAGE -> handleMessage(packet, peerID) + MessageType.LEAVE -> handleLeave(packet, peerID) + MessageType.FRAGMENT_START, + MessageType.FRAGMENT_CONTINUE, + MessageType.FRAGMENT_END -> handleFragment(packet, peerID) + MessageType.DELIVERY_ACK -> handleDeliveryAck(packet, peerID) + MessageType.READ_RECEIPT -> handleReadReceipt(packet, peerID) + else -> { + Log.w(TAG, "Unknown message type: ${packet.type}") + } + } + } + + /** + * Handle key exchange message + */ + private suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing key exchange from $peerID") + + val success = delegate?.handleKeyExchange(packet, peerID) ?: false + + if (success) { + // Key exchange successful, send announce and cached messages + delay(100) + delegate?.sendAnnouncementToPeer(peerID) + + delay(500) + delegate?.sendCachedMessages(peerID) + } + } + + /** + * Handle announce message + */ + private suspend fun handleAnnounce(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing announce from $peerID") + delegate?.handleAnnounce(packet, peerID) + } + + /** + * Handle regular message + */ + private suspend fun handleMessage(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing message from $peerID") + delegate?.handleMessage(packet, peerID) + } + + /** + * Handle leave message + */ + private suspend fun handleLeave(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing leave from $peerID") + delegate?.handleLeave(packet, peerID) + } + + /** + * Handle message fragments + */ + private suspend fun handleFragment(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing fragment from $peerID") + + val reassembledPacket = delegate?.handleFragment(packet) + if (reassembledPacket != null) { + Log.d(TAG, "Fragment reassembled, processing complete message") + handleReceivedPacket(reassembledPacket, peerID) + } + + // Relay fragment regardless of reassembly + if (packet.ttl > 0u) { + val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte()) + delegate?.relayPacket(relayPacket) + } + } + + /** + * Handle delivery acknowledgment + */ + private suspend fun handleDeliveryAck(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing delivery ACK from $peerID") + delegate?.handleDeliveryAck(packet, peerID) + } + + /** + * Handle read receipt + */ + private suspend fun handleReadReceipt(packet: BitchatPacket, peerID: String) { + Log.d(TAG, "Processing read receipt from $peerID") + delegate?.handleReadReceipt(packet, peerID) + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("=== Packet Processor Debug Info ===") + appendLine("Processor Scope Active: ${processorScope.isActive}") + appendLine("My Peer ID: $myPeerID") + } + } + + /** + * Shutdown the processor + */ + fun shutdown() { + processorScope.cancel() + } +} + +/** + * Delegate interface for packet processor callbacks + */ +interface PacketProcessorDelegate { + // Security validation + fun validatePacketSecurity(packet: BitchatPacket, peerID: String): Boolean + + // Peer management + fun updatePeerLastSeen(peerID: String) + + // Message type handlers + fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean + fun handleAnnounce(packet: BitchatPacket, peerID: String) + fun handleMessage(packet: BitchatPacket, peerID: String) + fun handleLeave(packet: BitchatPacket, peerID: String) + fun handleFragment(packet: BitchatPacket): BitchatPacket? + fun handleDeliveryAck(packet: BitchatPacket, peerID: String) + fun handleReadReceipt(packet: BitchatPacket, peerID: String) + + // Communication + fun sendAnnouncementToPeer(peerID: String) + fun sendCachedMessages(peerID: String) + fun relayPacket(packet: BitchatPacket) +} diff --git a/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt new file mode 100644 index 00000000..eeeb8ba7 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/PeerManager.kt @@ -0,0 +1,257 @@ +package com.bitchat.android.mesh + +import android.util.Log +import com.bitchat.android.model.BitchatMessage +import kotlinx.coroutines.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Manages active peers, nicknames, and RSSI tracking + * Extracted from BluetoothMeshService for better separation of concerns + */ +class PeerManager { + + companion object { + private const val TAG = "PeerManager" + private const val STALE_PEER_TIMEOUT = 180000L // 3 minutes (same as iOS) + private const val CLEANUP_INTERVAL = 60000L // 1 minute + } + + // Peer tracking data + private val peerNicknames = ConcurrentHashMap() + private val activePeers = ConcurrentHashMap() // peerID -> lastSeen timestamp + private val peerRSSI = ConcurrentHashMap() + private val announcedPeers = CopyOnWriteArrayList() + private val announcedToPeers = CopyOnWriteArrayList() + + // Delegate for callbacks + var delegate: PeerManagerDelegate? = null + + // Coroutines + private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + startPeriodicCleanup() + } + + /** + * Update peer last seen timestamp + */ + fun updatePeerLastSeen(peerID: String) { + if (peerID != "unknown") { + activePeers[peerID] = System.currentTimeMillis() + } + } + + /** + * Add or update peer with nickname + */ + fun addOrUpdatePeer(peerID: String, nickname: String): Boolean { + if (peerID == "unknown") return false + + // Clean up stale peer IDs with the same nickname (exact same logic as iOS) + val stalePeerIDs = mutableListOf() + peerNicknames.forEach { (existingPeerID, existingNickname) -> + if (existingNickname == nickname && existingPeerID != peerID) { + val lastSeen = activePeers[existingPeerID] ?: 0 + val wasRecentlySeen = (System.currentTimeMillis() - lastSeen) < 10000 + if (!wasRecentlySeen) { + stalePeerIDs.add(existingPeerID) + } + } + } + + // Remove stale peer IDs + stalePeerIDs.forEach { stalePeerID -> + removePeer(stalePeerID, notifyDelegate = false) + } + + // Check if this is a new peer announcement + val isFirstAnnounce = !announcedPeers.contains(peerID) + + // Update peer data + peerNicknames[peerID] = nickname + activePeers[peerID] = System.currentTimeMillis() + + // Handle first announcement + if (isFirstAnnounce) { + announcedPeers.add(peerID) + delegate?.onPeerConnected(nickname) + notifyPeerListUpdate() + return true + } + + return false + } + + /** + * Remove peer + */ + fun removePeer(peerID: String, notifyDelegate: Boolean = true) { + val nickname = peerNicknames.remove(peerID) + activePeers.remove(peerID) + peerRSSI.remove(peerID) + announcedPeers.remove(peerID) + announcedToPeers.remove(peerID) + + if (notifyDelegate && nickname != null) { + delegate?.onPeerDisconnected(nickname) + notifyPeerListUpdate() + } + } + + /** + * Update peer RSSI + */ + fun updatePeerRSSI(peerID: String, rssi: Int) { + if (peerID != "unknown") { + peerRSSI[peerID] = rssi + } + } + + /** + * Check if peer has been announced to + */ + fun hasAnnouncedToPeer(peerID: String): Boolean { + return announcedToPeers.contains(peerID) + } + + /** + * Mark peer as announced to + */ + fun markPeerAsAnnouncedTo(peerID: String) { + if (!announcedToPeers.contains(peerID)) { + announcedToPeers.add(peerID) + } + } + + /** + * Check if peer is active + */ + fun isPeerActive(peerID: String): Boolean { + return activePeers.containsKey(peerID) + } + + /** + * Get peer nickname + */ + fun getPeerNickname(peerID: String): String? { + return peerNicknames[peerID] + } + + /** + * Get all peer nicknames + */ + fun getAllPeerNicknames(): Map { + return peerNicknames.toMap() + } + + /** + * Get all peer RSSI values + */ + fun getAllPeerRSSI(): Map { + return peerRSSI.toMap() + } + + /** + * Get list of active peer IDs + */ + fun getActivePeerIDs(): List { + return activePeers.keys.toList().sorted() + } + + /** + * Get active peer count + */ + fun getActivePeerCount(): Int { + return activePeers.size + } + + /** + * Clear all peer data + */ + fun clearAllPeers() { + peerNicknames.clear() + activePeers.clear() + peerRSSI.clear() + announcedPeers.clear() + announcedToPeers.clear() + notifyPeerListUpdate() + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("=== Peer Manager Debug Info ===") + appendLine("Active Peers: ${activePeers.size}") + activePeers.forEach { (peerID, lastSeen) -> + val nickname = peerNicknames[peerID] ?: "Unknown" + val timeSince = (System.currentTimeMillis() - lastSeen) / 1000 + val rssi = peerRSSI[peerID]?.let { "${it} dBm" } ?: "No RSSI" + appendLine(" - $peerID ($nickname) - last seen ${timeSince}s ago, RSSI: $rssi") + } + appendLine("Announced Peers: ${announcedPeers.size}") + appendLine("Announced To Peers: ${announcedToPeers.size}") + } + } + + /** + * Notify delegate of peer list updates + */ + private fun notifyPeerListUpdate() { + val peerList = getActivePeerIDs() + delegate?.onPeerListUpdated(peerList) + } + + /** + * Start periodic cleanup of stale peers + */ + private fun startPeriodicCleanup() { + managerScope.launch { + while (isActive) { + delay(CLEANUP_INTERVAL) + cleanupStalePeers() + } + } + } + + /** + * Clean up stale peers (same 3-minute threshold as iOS) + */ + private fun cleanupStalePeers() { + val now = System.currentTimeMillis() + + val peersToRemove = activePeers.entries.filter { (_, lastSeen) -> + now - lastSeen > STALE_PEER_TIMEOUT + }.map { it.key } + + peersToRemove.forEach { peerID -> + Log.d(TAG, "Removing stale peer: $peerID") + removePeer(peerID) + } + + if (peersToRemove.isNotEmpty()) { + Log.d(TAG, "Cleaned up ${peersToRemove.size} stale peers") + } + } + + /** + * Shutdown the manager + */ + fun shutdown() { + managerScope.cancel() + clearAllPeers() + } +} + +/** + * Delegate interface for peer manager callbacks + */ +interface PeerManagerDelegate { + fun onPeerConnected(nickname: String) + fun onPeerDisconnected(nickname: String) + fun onPeerListUpdated(peerIDs: List) +} diff --git a/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt new file mode 100644 index 00000000..3b655d08 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/SecurityManager.kt @@ -0,0 +1,319 @@ +package com.bitchat.android.mesh + +import android.util.Log +import com.bitchat.android.crypto.EncryptionService +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import kotlinx.coroutines.* +import java.util.* +import kotlin.collections.mutableSetOf + +/** + * Manages security aspects of the mesh network including duplicate detection, + * replay attack protection, and key exchange handling + * Extracted from BluetoothMeshService for better separation of concerns + */ +class SecurityManager(private val encryptionService: EncryptionService, private val myPeerID: String) { + + companion object { + private const val TAG = "SecurityManager" + private const val MESSAGE_TIMEOUT = 300000L // 5 minutes (same as iOS) + private const val CLEANUP_INTERVAL = 300000L // 5 minutes + private const val MAX_PROCESSED_MESSAGES = 10000 + private const val MAX_PROCESSED_KEY_EXCHANGES = 1000 + } + + // Security tracking + private val processedMessages = Collections.synchronizedSet(mutableSetOf()) + private val processedKeyExchanges = Collections.synchronizedSet(mutableSetOf()) + private val messageTimestamps = Collections.synchronizedMap(mutableMapOf()) + + // Delegate for callbacks + var delegate: SecurityManagerDelegate? = null + + // Coroutines + private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + startPeriodicCleanup() + } + + /** + * Validate packet security (timestamp, replay attacks, duplicates) + */ + fun validatePacket(packet: BitchatPacket, peerID: String): Boolean { + // Skip validation for our own packets + if (peerID == myPeerID) { + Log.d(TAG, "Skipping validation for our own packet") + return false + } + + // TTL check + if (packet.ttl == 0u.toUByte()) { + Log.d(TAG, "Dropping packet with TTL 0") + return false + } + + // Validate packet payload + if (packet.payload.isEmpty()) { + Log.d(TAG, "Dropping packet with empty payload") + return false + } + + // Replay attack protection (same 5-minute window as iOS) + val currentTime = System.currentTimeMillis() + val packetTime = packet.timestamp.toLong() + val timeDiff = kotlin.math.abs(currentTime - packetTime) + + if (timeDiff > MESSAGE_TIMEOUT) { + Log.d(TAG, "Dropping old packet from $peerID, time diff: ${timeDiff/1000}s") + return false + } + + // Duplicate detection + val messageID = generateMessageID(packet, peerID) + if (processedMessages.contains(messageID)) { + Log.d(TAG, "Dropping duplicate packet: $messageID") + return false + } + + // Add to processed messages + processedMessages.add(messageID) + messageTimestamps[messageID] = currentTime + + Log.d(TAG, "Packet validation passed for $peerID, messageID: $messageID") + return true + } + + /** + * Handle key exchange packet + */ + suspend fun handleKeyExchange(packet: BitchatPacket, peerID: String): Boolean { + if (peerID == myPeerID) return false + + if (packet.payload.isEmpty()) { + Log.w(TAG, "Key exchange packet has empty payload") + return false + } + + // Prevent duplicate key exchange processing + val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}" + + if (processedKeyExchanges.contains(exchangeKey)) { + Log.d(TAG, "Already processed key exchange: $exchangeKey") + return false + } + + processedKeyExchanges.add(exchangeKey) + + try { + // Process the key exchange + encryptionService.addPeerPublicKey(peerID, packet.payload) + + Log.d(TAG, "Successfully processed key exchange from $peerID") + + // Notify delegate + delegate?.onKeyExchangeCompleted(peerID) + + return true + + } catch (e: Exception) { + Log.e(TAG, "Failed to process key exchange from $peerID: ${e.message}") + return false + } + } + + /** + * Verify packet signature + */ + fun verifySignature(packet: BitchatPacket, peerID: String): Boolean { + return packet.signature?.let { signature -> + try { + val isValid = encryptionService.verify(signature, packet.payload, peerID) + if (!isValid) { + Log.w(TAG, "Invalid signature for packet from $peerID") + } + isValid + } catch (e: Exception) { + Log.e(TAG, "Failed to verify signature from $peerID: ${e.message}") + false + } + } ?: true // No signature means verification passes + } + + /** + * Sign packet payload + */ + fun signPacket(payload: ByteArray): ByteArray? { + return try { + encryptionService.sign(payload) + } catch (e: Exception) { + Log.e(TAG, "Failed to sign packet: ${e.message}") + null + } + } + + /** + * Encrypt payload for specific peer + */ + fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? { + return try { + encryptionService.encrypt(data, recipientPeerID) + } catch (e: Exception) { + Log.e(TAG, "Failed to encrypt for $recipientPeerID: ${e.message}") + null + } + } + + /** + * Decrypt payload from specific peer + */ + fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? { + return try { + encryptionService.decrypt(encryptedData, senderPeerID) + } catch (e: Exception) { + Log.e(TAG, "Failed to decrypt from $senderPeerID: ${e.message}") + null + } + } + + /** + * Get combined public key data for key exchange + */ + fun getCombinedPublicKeyData(): ByteArray { + return encryptionService.getCombinedPublicKeyData() + } + + /** + * Generate message ID for duplicate detection + */ + private fun generateMessageID(packet: BitchatPacket, peerID: String): String { + return when (MessageType.fromValue(packet.type)) { + MessageType.FRAGMENT_START, MessageType.FRAGMENT_CONTINUE, MessageType.FRAGMENT_END -> { + // For fragments, include the payload hash to distinguish different fragments + "${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}" + } + else -> { + // For other messages, use a truncated payload hash + val payloadHash = packet.payload.sliceArray(0 until minOf(64, packet.payload.size)).contentHashCode() + "${packet.timestamp}-$peerID-$payloadHash" + } + } + } + + /** + * Check if we have encryption keys for a peer + */ + fun hasKeysForPeer(peerID: String): Boolean { + // This would need to be implemented in EncryptionService + // For now, we'll assume we have keys if we processed a key exchange + return processedKeyExchanges.any { it.startsWith("$peerID-") } + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("=== Security Manager Debug Info ===") + appendLine("Processed Messages: ${processedMessages.size}") + appendLine("Processed Key Exchanges: ${processedKeyExchanges.size}") + appendLine("Message Timestamps: ${messageTimestamps.size}") + + if (processedKeyExchanges.isNotEmpty()) { + appendLine("Key Exchange History:") + processedKeyExchanges.take(10).forEach { exchange -> + appendLine(" - $exchange") + } + if (processedKeyExchanges.size > 10) { + appendLine(" ... and ${processedKeyExchanges.size - 10} more") + } + } + } + } + + /** + * Start periodic cleanup + */ + private fun startPeriodicCleanup() { + managerScope.launch { + while (isActive) { + delay(CLEANUP_INTERVAL) + cleanupOldData() + } + } + } + + /** + * Clean up old processed messages and timestamps + */ + private fun cleanupOldData() { + val cutoffTime = System.currentTimeMillis() - MESSAGE_TIMEOUT + var removedCount = 0 + + // Clean up old message timestamps and corresponding processed messages + val messagesToRemove = messageTimestamps.entries.filter { (_, timestamp) -> + timestamp < cutoffTime + }.map { it.key } + + messagesToRemove.forEach { messageId -> + messageTimestamps.remove(messageId) + if (processedMessages.remove(messageId)) { + removedCount++ + } + } + + // Limit the size of processed messages set + if (processedMessages.size > MAX_PROCESSED_MESSAGES) { + val excess = processedMessages.size - MAX_PROCESSED_MESSAGES + val toRemove = processedMessages.take(excess) + processedMessages.removeAll(toRemove.toSet()) + removeFromMessageTimestamps(toRemove) + removedCount += excess + } + + // Limit the size of processed key exchanges set + if (processedKeyExchanges.size > MAX_PROCESSED_KEY_EXCHANGES) { + val excess = processedKeyExchanges.size - MAX_PROCESSED_KEY_EXCHANGES + val toRemove = processedKeyExchanges.take(excess) + processedKeyExchanges.removeAll(toRemove.toSet()) + } + + if (removedCount > 0) { + Log.d(TAG, "Cleaned up $removedCount old processed messages") + } + } + + /** + * Helper to remove entries from messageTimestamps + */ + private fun removeFromMessageTimestamps(messageIds: List) { + messageIds.forEach { messageId -> + messageTimestamps.remove(messageId) + } + } + + /** + * Clear all security data + */ + fun clearAllData() { + processedMessages.clear() + processedKeyExchanges.clear() + messageTimestamps.clear() + } + + /** + * Shutdown the manager + */ + fun shutdown() { + managerScope.cancel() + clearAllData() + } +} + +/** + * Delegate interface for security manager callbacks + */ +interface SecurityManagerDelegate { + fun onKeyExchangeCompleted(peerID: String) +} diff --git a/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt b/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt new file mode 100644 index 00000000..bb5d868c --- /dev/null +++ b/app/src/main/java/com/bitchat/android/mesh/StoreForwardManager.kt @@ -0,0 +1,315 @@ +package com.bitchat.android.mesh + +import android.util.Log +import com.bitchat.android.protocol.BitchatPacket +import com.bitchat.android.protocol.MessageType +import com.bitchat.android.protocol.SpecialRecipients +import kotlinx.coroutines.* +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +/** + * Manages store-and-forward messaging for offline peers + * Extracted from BluetoothMeshService for better separation of concerns + */ +class StoreForwardManager { + + companion object { + private const val TAG = "StoreForwardManager" + private const val MESSAGE_CACHE_TIMEOUT = 43200000L // 12 hours for regular peers + private const val MAX_CACHED_MESSAGES = 100 // For regular peers + private const val MAX_CACHED_MESSAGES_FAVORITES = 1000 // For favorites + private const val CLEANUP_INTERVAL = 600000L // 10 minutes + } + + /** + * Data class for stored messages + */ + private data class StoredMessage( + val packet: BitchatPacket, + val timestamp: Long, + val messageID: String, + val isForFavorite: Boolean + ) + + // Message storage + private val messageCache = Collections.synchronizedList(mutableListOf()) + private val favoriteMessageQueue = ConcurrentHashMap>() + private val deliveredMessages = Collections.synchronizedSet(mutableSetOf()) + private val cachedMessagesSentToPeer = Collections.synchronizedSet(mutableSetOf()) + + // Delegate for callbacks + var delegate: StoreForwardManagerDelegate? = null + + // Coroutines + private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + init { + startPeriodicCleanup() + } + + /** + * Cache message for offline delivery + */ + fun cacheMessage(packet: BitchatPacket, messageID: String) { + // Skip certain message types (same as iOS) + if (packet.type == MessageType.KEY_EXCHANGE.value || + packet.type == MessageType.ANNOUNCE.value || + packet.type == MessageType.LEAVE.value) { + Log.d(TAG, "Skipping cache for message type: ${packet.type}") + return + } + + // Don't cache broadcast messages + if (packet.recipientID != null && packet.recipientID.contentEquals(SpecialRecipients.BROADCAST)) { + Log.d(TAG, "Skipping cache for broadcast message") + return + } + + // Determine if this is for a favorite peer + val recipientPeerID = packet.recipientID?.let { recipientID -> + String(recipientID).replace("\u0000", "") + } + + if (recipientPeerID.isNullOrEmpty()) { + Log.w(TAG, "Cannot cache message without valid recipient") + return + } + + val isForFavorite = delegate?.isFavorite(recipientPeerID) ?: false + + val storedMessage = StoredMessage( + packet = packet, + timestamp = System.currentTimeMillis(), + messageID = messageID, + isForFavorite = isForFavorite + ) + + if (isForFavorite) { + // Store in favorite queue + if (!favoriteMessageQueue.containsKey(recipientPeerID)) { + favoriteMessageQueue[recipientPeerID] = mutableListOf() + } + favoriteMessageQueue[recipientPeerID]?.add(storedMessage) + + // Limit favorite queue size + if (favoriteMessageQueue[recipientPeerID]?.size ?: 0 > MAX_CACHED_MESSAGES_FAVORITES) { + favoriteMessageQueue[recipientPeerID]?.removeAt(0) + } + + Log.d(TAG, "Cached message for favorite peer $recipientPeerID (${favoriteMessageQueue[recipientPeerID]?.size} total)") + + } else { + // Store in regular cache + cleanupMessageCache() + + messageCache.add(storedMessage) + + // Limit cache size + if (messageCache.size > MAX_CACHED_MESSAGES) { + messageCache.removeAt(0) + } + + Log.d(TAG, "Cached message for peer $recipientPeerID (${messageCache.size} total in cache)") + } + } + + /** + * Send cached messages to peer when they come online + */ + fun sendCachedMessages(peerID: String) { + if (cachedMessagesSentToPeer.contains(peerID)) { + Log.d(TAG, "Already sent cached messages to $peerID") + return // Already sent cached messages to this peer + } + + cachedMessagesSentToPeer.add(peerID) + + managerScope.launch { + cleanupMessageCache() + + val messagesToSend = mutableListOf() + + // Check favorite queue + favoriteMessageQueue[peerID]?.let { favoriteMessages -> + val undeliveredFavorites = favoriteMessages.filter { !deliveredMessages.contains(it.messageID) } + messagesToSend.addAll(undeliveredFavorites) + favoriteMessageQueue.remove(peerID) + Log.d(TAG, "Found ${undeliveredFavorites.size} cached favorite messages for $peerID") + } + + // Filter regular cached messages for this recipient + val recipientMessages = messageCache.filter { storedMessage -> + !deliveredMessages.contains(storedMessage.messageID) && + storedMessage.packet.recipientID?.let { recipientID -> + String(recipientID).replace("\u0000", "") == peerID + } == true + } + messagesToSend.addAll(recipientMessages) + + if (recipientMessages.isNotEmpty()) { + Log.d(TAG, "Found ${recipientMessages.size} cached regular messages for $peerID") + } + + // Sort by timestamp + messagesToSend.sortBy { it.timestamp } + + if (messagesToSend.isNotEmpty()) { + Log.i(TAG, "Sending ${messagesToSend.size} cached messages to $peerID") + } + + // Mark as delivered + val messageIDsToRemove = messagesToSend.map { it.messageID } + deliveredMessages.addAll(messageIDsToRemove) + + // Send with delays to avoid overwhelming the connection + messagesToSend.forEachIndexed { index, storedMessage -> + delay(index * 100L) // 100ms between messages + delegate?.sendPacket(storedMessage.packet) + } + + // Remove sent messages from cache + messageCache.removeAll { messageIDsToRemove.contains(it.messageID) } + + if (messagesToSend.isNotEmpty()) { + Log.d(TAG, "Finished sending ${messagesToSend.size} cached messages to $peerID") + } + } + } + + /** + * Check if message should be cached for peer + */ + fun shouldCacheForPeer(recipientPeerID: String): Boolean { + // Check if recipient is offline and should cache for favorites + val isOffline = !(delegate?.isPeerOnline(recipientPeerID) ?: false) + val isRecipientFavorite = delegate?.isFavorite(recipientPeerID) ?: false + + return isOffline && isRecipientFavorite + } + + /** + * Mark message as delivered + */ + fun markMessageAsDelivered(messageID: String) { + deliveredMessages.add(messageID) + } + + /** + * Get cached message count for peer + */ + fun getCachedMessageCount(peerID: String): Int { + val favoriteCount = favoriteMessageQueue[peerID]?.size ?: 0 + val regularCount = messageCache.count { storedMessage -> + storedMessage.packet.recipientID?.let { recipientID -> + String(recipientID).replace("\u0000", "") == peerID + } == true + } + return favoriteCount + regularCount + } + + /** + * Get debug information + */ + fun getDebugInfo(): String { + return buildString { + appendLine("=== Store-Forward Manager Debug Info ===") + appendLine("Regular Cache: ${messageCache.size}/${MAX_CACHED_MESSAGES}") + appendLine("Favorite Queues: ${favoriteMessageQueue.size}") + + favoriteMessageQueue.forEach { (peerID, messages) -> + appendLine(" - $peerID: ${messages.size} messages") + } + + appendLine("Delivered Messages: ${deliveredMessages.size}") + appendLine("Peers Sent Cache: ${cachedMessagesSentToPeer.size}") + + // Cache age analysis + val now = System.currentTimeMillis() + val regularCacheAges = messageCache.map { (now - it.timestamp) / 1000 } + if (regularCacheAges.isNotEmpty()) { + val avgAge = regularCacheAges.average().toInt() + val maxAge = regularCacheAges.maxOrNull() ?: 0 + appendLine("Regular Cache Age: avg ${avgAge}s, max ${maxAge}s") + } + } + } + + /** + * Start periodic cleanup + */ + private fun startPeriodicCleanup() { + managerScope.launch { + while (isActive) { + delay(CLEANUP_INTERVAL) + cleanupMessageCache() + cleanupDeliveredMessages() + } + } + } + + /** + * Clean up old cached messages (not for favorites) + */ + private fun cleanupMessageCache() { + val cutoffTime = System.currentTimeMillis() - MESSAGE_CACHE_TIMEOUT + val sizeBefore = messageCache.size + val removed = messageCache.removeAll { !it.isForFavorite && it.timestamp < cutoffTime } + + if (removed) { + val removedCount = sizeBefore - messageCache.size + Log.d(TAG, "Cleaned up $removedCount old cached messages") + } + } + + /** + * Clean up delivered messages set (prevent memory leak) + */ + private fun cleanupDeliveredMessages() { + if (deliveredMessages.size > 1000) { + Log.d(TAG, "Clearing delivered messages set (${deliveredMessages.size} entries)") + deliveredMessages.clear() + } + + if (cachedMessagesSentToPeer.size > 200) { + Log.d(TAG, "Clearing cached messages sent tracking (${cachedMessagesSentToPeer.size} entries)") + cachedMessagesSentToPeer.clear() + } + } + + /** + * Clear all cached data + */ + fun clearAllCache() { + messageCache.clear() + favoriteMessageQueue.clear() + deliveredMessages.clear() + cachedMessagesSentToPeer.clear() + Log.d(TAG, "Cleared all cached message data") + } + + /** + * Force cleanup for testing + */ + fun forceCleanup() { + cleanupMessageCache() + cleanupDeliveredMessages() + } + + /** + * Shutdown the manager + */ + fun shutdown() { + managerScope.cancel() + clearAllCache() + } +} + +/** + * Delegate interface for store-forward manager callbacks + */ +interface StoreForwardManagerDelegate { + fun isFavorite(peerID: String): Boolean + fun isPeerOnline(peerID: String): Boolean + fun sendPacket(packet: BitchatPacket) +} diff --git a/demo_colors.md b/demo_colors.md new file mode 100644 index 00000000..c463b41c --- /dev/null +++ b/demo_colors.md @@ -0,0 +1,62 @@ +# Username Color Feature Demo + +The bitchat Android app already has the username color feature fully implemented! Here's how it works: + +## How It Works + +1. **Your username remains green** - Uses `colorScheme.primary` (bright green in dark mode, dark green in light mode) +2. **Other users get unique colors** - Based on their peer ID using the `getUsernameColor()` function +3. **Colors are consistent** - Same user always gets the same color across sessions +4. **Terminal-friendly palette** - 16 colors that work on both black and white backgrounds + +## Color Palette + +The system uses these 16 terminal-friendly colors for other users: + +- 🟢 Bright Green (#00FF00) +- 🔵 Cyan (#00FFFF) +- 🟡 Yellow (#FFFF00) +- 🔴 Magenta (#FF00FF) +- 🟦 Bright Blue (#0080FF) +- 🟠 Orange (#FF8000) +- 🔶 Lime Green (#80FF00) +- 🟣 Purple (#8000FF) +- 🩷 Pink (#FF0080) +- 💚 Spring Green (#00FF80) +- 🟦 Light Cyan (#80FFFF) +- 🩷 Light Red (#FF8080) +- 🟦 Light Blue (#8080FF) +- 🟡 Light Yellow (#FFFF80) +- 🩷 Light Magenta (#FF80FF) +- 🟢 Light Green (#80FF80) + +## Example Chat Display + +``` +[14:23:45] <@you> hello everyone! ← Your message (green) +[14:23:47] <@alice> hey there! ← Alice (cyan) +[14:23:50] <@bob> how's it going? ← Bob (yellow) +[14:23:52] <@charlie> great to see you all ← Charlie (magenta) +[14:23:55] <@you> having a great time ← Your message (green) +[14:23:58] <@alice> same here @you! ← Alice (cyan again) +``` + +## Code Implementation + +The feature is implemented in `/app/src/main/java/com/bitchat/android/ui/ChatScreen.kt`: + +- Line 342-350: Color assignment logic in `formatMessageAsAnnotatedString()` +- Line 820-855: `getUsernameColor()` function that generates consistent colors +- Uses peer ID for consistency (falls back to nickname if no peer ID available) +- Integrates perfectly with the existing IRC-style chat format + +## Testing + +The feature is already working in the app. When you chat with multiple users, you'll see: +- Your messages in green +- Each other user in their own unique color +- Same user always has the same color +- Colors remain consistent across app restarts +- Works in both light and dark themes + +The feature is **complete and ready to use**! 🎉 diff --git a/refactoring_plan.md b/refactoring_plan.md new file mode 100644 index 00000000..fe61e413 --- /dev/null +++ b/refactoring_plan.md @@ -0,0 +1,73 @@ +# BluetoothMeshService Refactoring Plan + +## Current State +- Single file: `BluetoothMeshService.kt` (~1000+ lines) +- Multiple responsibilities mixed together +- Hard to test and maintain + +## Proposed Structure + +### 1. Core Service (BluetoothMeshService.kt) +**Responsibilities:** +- Service lifecycle management +- Coordination between components +- Public API for sending messages +- Delegate management + +### 2. Connection Management (BluetoothConnectionManager.kt) +**Responsibilities:** +- BLE scanning and advertising +- GATT server/client setup and management +- Device connection tracking +- Peer discovery and RSSI tracking + +### 3. Packet Processing (PacketProcessor.kt) +**Responsibilities:** +- Incoming packet handling +- Message type routing +- TTL and duplicate detection +- Timestamp validation + +### 4. Message Handler (MessageHandler.kt) +**Responsibilities:** +- Processing specific message types (ANNOUNCE, MESSAGE, LEAVE, etc.) +- Message parsing and validation +- Relay logic + +### 5. Fragment Manager (FragmentManager.kt) +**Responsibilities:** +- Message fragmentation for large messages +- Fragment reassembly +- Fragment cleanup and timeouts + +### 6. Store-and-Forward Manager (StoreForwardManager.kt) +**Responsibilities:** +- Message caching for offline peers +- Delivering cached messages when peers come online +- Cache cleanup and management + +### 7. Peer Manager (PeerManager.kt) +**Responsibilities:** +- Active peer tracking +- Peer nickname management +- Stale peer cleanup +- Peer list updates + +### 8. Security Manager (SecurityManager.kt) +**Responsibilities:** +- Key exchange handling +- Message signing and verification +- Duplicate detection tracking + +## Refactoring Strategy +1. Extract each component while maintaining exact functionality +2. Use dependency injection for component communication +3. Ensure all existing tests pass +4. Maintain the same public API + +## Benefits +- Easier to test individual components +- Better separation of concerns +- More maintainable code +- Easier to add new features +- Better code reuse diff --git a/test_color.kt b/test_color.kt new file mode 100644 index 00000000..f13cc7d2 --- /dev/null +++ b/test_color.kt @@ -0,0 +1,51 @@ +import androidx.compose.ui.graphics.Color + +/** + * Generate a consistent color for a username based on their peer ID or nickname + * Returns colors that work well on both light and dark backgrounds + */ +fun getUsernameColor(identifier: String): Color { + // Hash the identifier to get a consistent number + val hash = identifier.hashCode().toUInt() + + // Terminal-friendly colors that work on both black and white backgrounds + val colors = listOf( + Color(0xFF00FF00), // Bright Green + Color(0xFF00FFFF), // Cyan + Color(0xFFFFFF00), // Yellow + Color(0xFFFF00FF), // Magenta + Color(0xFF0080FF), // Bright Blue + Color(0xFFFF8000), // Orange + Color(0xFF80FF00), // Lime Green + Color(0xFF8000FF), // Purple + Color(0xFFFF0080), // Pink + Color(0xFF00FF80), // Spring Green + Color(0xFF80FFFF), // Light Cyan + Color(0xFFFF8080), // Light Red + Color(0xFF8080FF), // Light Blue + Color(0xFFFFFF80), // Light Yellow + Color(0xFFFF80FF), // Light Magenta + Color(0xFF80FF80), // Light Green + ) + + // Use modulo to get consistent color for same identifier + return colors[(hash % colors.size.toUInt()).toInt()] +} + +fun main() { + println("Testing username color function:") + + val testUsers = listOf("alice", "bob", "charlie", "diana", "eve") + + testUsers.forEach { user -> + val color = getUsernameColor(user) + println("User '$user' gets color: ${color.value.toString(16).uppercase()}") + } + + // Test consistency - same user should always get same color + println("\nTesting consistency:") + repeat(3) { + val aliceColor = getUsernameColor("alice") + println("Alice color (test ${it + 1}): ${aliceColor.value.toString(16).uppercase()}") + } +}