mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 16:45:20 +00:00
seems to work
This commit is contained in:
@@ -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.
|
||||||
@@ -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<String, MutableMap<Int, ByteArray>>()
|
||||||
|
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // 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<BitchatPacket> {
|
||||||
|
val data = packet.toBinaryData() ?: return emptyList()
|
||||||
|
|
||||||
|
if (data.size <= MAX_FRAGMENT_SIZE) {
|
||||||
|
return listOf(packet) // No fragmentation needed
|
||||||
|
}
|
||||||
|
|
||||||
|
val fragments = mutableListOf<BitchatPacket>()
|
||||||
|
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<Byte>()
|
||||||
|
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<String>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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<String, String>()
|
||||||
|
private val activePeers = ConcurrentHashMap<String, Long>() // peerID -> lastSeen timestamp
|
||||||
|
private val peerRSSI = ConcurrentHashMap<String, Int>()
|
||||||
|
private val announcedPeers = CopyOnWriteArrayList<String>()
|
||||||
|
private val announcedToPeers = CopyOnWriteArrayList<String>()
|
||||||
|
|
||||||
|
// 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<String>()
|
||||||
|
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<String, String> {
|
||||||
|
return peerNicknames.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all peer RSSI values
|
||||||
|
*/
|
||||||
|
fun getAllPeerRSSI(): Map<String, Int> {
|
||||||
|
return peerRSSI.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of active peer IDs
|
||||||
|
*/
|
||||||
|
fun getActivePeerIDs(): List<String> {
|
||||||
|
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<String>)
|
||||||
|
}
|
||||||
@@ -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<String>())
|
||||||
|
private val processedKeyExchanges = Collections.synchronizedSet(mutableSetOf<String>())
|
||||||
|
private val messageTimestamps = Collections.synchronizedMap(mutableMapOf<String, Long>())
|
||||||
|
|
||||||
|
// 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<String>) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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<StoredMessage>())
|
||||||
|
private val favoriteMessageQueue = ConcurrentHashMap<String, MutableList<StoredMessage>>()
|
||||||
|
private val deliveredMessages = Collections.synchronizedSet(mutableSetOf<String>())
|
||||||
|
private val cachedMessagesSentToPeer = Collections.synchronizedSet(mutableSetOf<String>())
|
||||||
|
|
||||||
|
// 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<StoredMessage>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
@@ -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**! 🎉
|
||||||
@@ -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
|
||||||
@@ -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()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user