mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:25:19 +00:00
Changes bitchat protocol (#265)
* create payload * compiles and can send messages * identityannouncement * DMs work, read receipt not sent yet * works * delete old code * simplify * working * fragment wip * compression wip * use zlib compression * clean * nice * mesh * remove comments
This commit is contained in:
@@ -91,10 +91,7 @@ dependencies {
|
|||||||
|
|
||||||
// Bluetooth
|
// Bluetooth
|
||||||
implementation(libs.nordic.ble)
|
implementation(libs.nordic.ble)
|
||||||
|
|
||||||
// Compression
|
|
||||||
implementation(libs.lz4.java)
|
|
||||||
|
|
||||||
// Security preferences
|
// Security preferences
|
||||||
implementation(libs.androidx.security.crypto)
|
implementation(libs.androidx.security.crypto)
|
||||||
|
|
||||||
|
|||||||
@@ -3,13 +3,10 @@ package com.bitchat.android.mesh
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.crypto.EncryptionService
|
import com.bitchat.android.crypto.EncryptionService
|
||||||
import com.bitchat.android.protocol.MessagePadding
|
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.HandshakeRequest
|
import com.bitchat.android.protocol.MessagePadding
|
||||||
import com.bitchat.android.model.RoutedPacket
|
import com.bitchat.android.model.RoutedPacket
|
||||||
import com.bitchat.android.model.DeliveryAck
|
import com.bitchat.android.model.IdentityAnnouncement
|
||||||
import com.bitchat.android.model.ReadReceipt
|
|
||||||
import com.bitchat.android.model.NoiseIdentityAnnouncement
|
|
||||||
import com.bitchat.android.protocol.BitchatPacket
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
import com.bitchat.android.protocol.MessageType
|
import com.bitchat.android.protocol.MessageType
|
||||||
import com.bitchat.android.protocol.SpecialRecipients
|
import com.bitchat.android.protocol.SpecialRecipients
|
||||||
@@ -64,7 +61,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
init {
|
init {
|
||||||
setupDelegates()
|
setupDelegates()
|
||||||
messageHandler.packetProcessor = packetProcessor
|
messageHandler.packetProcessor = packetProcessor
|
||||||
startPeriodicDebugLogging()
|
//startPeriodicDebugLogging()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +92,6 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
delay(30000) // 30 seconds
|
delay(30000) // 30 seconds
|
||||||
sendBroadcastAnnounce()
|
sendBroadcastAnnounce()
|
||||||
broadcastNoiseIdentityAnnouncement()
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
Log.e(TAG, "Error in periodic broadcast announce: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -131,7 +127,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Send Noise handshake response
|
// Send Noise handshake response
|
||||||
val responsePacket = BitchatPacket(
|
val responsePacket = BitchatPacket(
|
||||||
version = 1u,
|
version = 1u,
|
||||||
type = MessageType.NOISE_HANDSHAKE_RESP.value,
|
type = MessageType.NOISE_HANDSHAKE.value,
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
recipientID = hexStringToByteArray(peerID),
|
recipientID = hexStringToByteArray(peerID),
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
@@ -224,18 +220,18 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
// Initiate proper Noise handshake with specific peer
|
// Initiate proper Noise handshake with specific peer
|
||||||
val handshakeData = encryptionService.initiateHandshake(peerID)
|
val handshakeData = encryptionService.initiateHandshake(peerID)
|
||||||
|
|
||||||
if (handshakeData != null) {
|
if (handshakeData != null) {
|
||||||
val packet = BitchatPacket(
|
val packet = BitchatPacket(
|
||||||
version = 1u,
|
version = 1u,
|
||||||
type = MessageType.NOISE_HANDSHAKE_INIT.value,
|
type = MessageType.NOISE_HANDSHAKE.value,
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
recipientID = hexStringToByteArray(peerID),
|
recipientID = hexStringToByteArray(peerID),
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
payload = handshakeData,
|
payload = handshakeData,
|
||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
Log.d(TAG, "Initiated Noise handshake with $peerID (${handshakeData.size} bytes)")
|
||||||
} else {
|
} else {
|
||||||
@@ -247,6 +243,15 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? {
|
||||||
|
return try {
|
||||||
|
encryptionService.processHandshakeMessage(payload, peerID)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to process handshake message from $peerID: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
override fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
||||||
publicKey: ByteArray, previousPeerID: String?) {
|
publicKey: ByteArray, previousPeerID: String?) {
|
||||||
|
|
||||||
@@ -270,10 +275,6 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return delegate?.decryptChannelMessage(encryptedContent, channel)
|
return delegate?.decryptChannelMessage(encryptedContent, channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
|
|
||||||
this@BluetoothMeshService.sendDeliveryAck(message, senderPeerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
override fun onMessageReceived(message: BitchatMessage) {
|
override fun onMessageReceived(message: BitchatMessage) {
|
||||||
delegate?.didReceiveMessage(message)
|
delegate?.didReceiveMessage(message)
|
||||||
@@ -283,12 +284,12 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
delegate?.didReceiveChannelLeave(channel, fromPeer)
|
delegate?.didReceiveChannelLeave(channel, fromPeer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeliveryAckReceived(ack: DeliveryAck) {
|
override fun onDeliveryAckReceived(messageID: String, peerID: String) {
|
||||||
delegate?.didReceiveDeliveryAck(ack)
|
delegate?.didReceiveDeliveryAck(messageID, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReadReceiptReceived(receipt: ReadReceipt) {
|
override fun onReadReceiptReceived(messageID: String, peerID: String) {
|
||||||
delegate?.didReceiveReadReceipt(receipt)
|
delegate?.didReceiveReadReceipt(messageID, peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,18 +316,14 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return SpecialRecipients.BROADCAST
|
return SpecialRecipients.BROADCAST
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean {
|
override fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
||||||
return runBlocking { securityManager.handleNoiseHandshake(routed, step) }
|
return runBlocking { securityManager.handleNoiseHandshake(routed) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleNoiseEncrypted(routed: RoutedPacket) {
|
override fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||||
serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) }
|
serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
|
|
||||||
serviceScope.launch { messageHandler.handleNoiseIdentityAnnouncement(routed) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleAnnounce(routed: RoutedPacket) {
|
override fun handleAnnounce(routed: RoutedPacket) {
|
||||||
serviceScope.launch { messageHandler.handleAnnounce(routed) }
|
serviceScope.launch { messageHandler.handleAnnounce(routed) }
|
||||||
}
|
}
|
||||||
@@ -343,14 +340,6 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return fragmentManager.handleFragment(packet)
|
return fragmentManager.handleFragment(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun handleDeliveryAck(routed: RoutedPacket) {
|
|
||||||
// serviceScope.launch { messageHandler.handleDeliveryAck(routed) }
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun handleReadReceipt(routed: RoutedPacket) {
|
|
||||||
serviceScope.launch { messageHandler.handleReadReceipt(routed) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun sendAnnouncementToPeer(peerID: String) {
|
override fun sendAnnouncementToPeer(peerID: String) {
|
||||||
this@BluetoothMeshService.sendAnnouncementToPeer(peerID)
|
this@BluetoothMeshService.sendAnnouncementToPeer(peerID)
|
||||||
}
|
}
|
||||||
@@ -376,11 +365,6 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
delay(200)
|
delay(200)
|
||||||
sendBroadcastAnnounce()
|
sendBroadcastAnnounce()
|
||||||
}
|
}
|
||||||
// Send key exchange to newly connected device
|
|
||||||
serviceScope.launch {
|
|
||||||
delay(400) // Ensure connection is stable
|
|
||||||
broadcastNoiseIdentityAnnouncement()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRSSIUpdated(deviceAddress: String, rssi: Int) {
|
override fun onRSSIUpdated(deviceAddress: String, rssi: Int) {
|
||||||
@@ -453,371 +437,199 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
if (content.isEmpty()) return
|
if (content.isEmpty()) return
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val nickname = delegate?.getNickname() ?: myPeerID
|
val packet = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
val message = BitchatMessage(
|
type = MessageType.MESSAGE.value,
|
||||||
sender = nickname,
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
content = content,
|
recipientID = SpecialRecipients.BROADCAST,
|
||||||
timestamp = Date(),
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
isRelay = false,
|
payload = content.toByteArray(Charsets.UTF_8),
|
||||||
senderPeerID = myPeerID,
|
signature = null,
|
||||||
mentions = if (mentions.isNotEmpty()) mentions else null,
|
ttl = MAX_TTL
|
||||||
channel = channel
|
|
||||||
)
|
)
|
||||||
|
|
||||||
message.toBinaryPayload()?.let { messageData ->
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
// Sign the message: TODO: NOT SIGNED
|
|
||||||
// val signature = securityManager.signPacket(messageData)
|
|
||||||
|
|
||||||
val packet = BitchatPacket(
|
|
||||||
version = 1u,
|
|
||||||
type = MessageType.MESSAGE.value,
|
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
|
||||||
recipientID = SpecialRecipients.BROADCAST,
|
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
|
||||||
payload = messageData,
|
|
||||||
signature = null,
|
|
||||||
ttl = MAX_TTL
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send with random delay and retry for reliability
|
|
||||||
// delay(Random.nextLong(50, 500))
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send private message
|
* Send private message - SIMPLIFIED iOS-compatible version
|
||||||
|
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
|
||||||
*/
|
*/
|
||||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
||||||
if (content.isEmpty() || recipientPeerID.isEmpty() || recipientNickname.isEmpty()) return
|
if (content.isEmpty() || recipientPeerID.isEmpty() || recipientNickname.isEmpty()) return
|
||||||
|
|
||||||
val nickname = delegate?.getNickname() ?: myPeerID
|
|
||||||
|
|
||||||
val message = BitchatMessage(
|
|
||||||
id = messageID ?: UUID.randomUUID().toString(),
|
|
||||||
sender = nickname,
|
|
||||||
content = content,
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false,
|
|
||||||
isPrivate = true,
|
|
||||||
recipientNickname = recipientNickname,
|
|
||||||
senderPeerID = myPeerID
|
|
||||||
)
|
|
||||||
|
|
||||||
message.toBinaryPayload()?.let { messageData ->
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Create inner packet with the padded message data
|
|
||||||
val innerPacket = BitchatPacket(
|
|
||||||
type = MessageType.MESSAGE.value,
|
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
|
||||||
recipientID = hexStringToByteArray(recipientPeerID),
|
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
|
||||||
payload = messageData,
|
|
||||||
signature = null,
|
|
||||||
ttl = MAX_TTL
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache for offline favorites
|
|
||||||
if (storeForwardManager.shouldCacheForPeer(recipientPeerID)) {
|
|
||||||
storeForwardManager.cacheMessage(innerPacket, messageID ?: message.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the new encrypt and broadcast function
|
|
||||||
encryptAndBroadcastNoisePacket(innerPacket, recipientPeerID)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to send private message: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send delivery acknowledgment for a received private message
|
|
||||||
*/
|
|
||||||
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String) {
|
|
||||||
val nickname = delegate?.getNickname() ?: myPeerID
|
|
||||||
val ack = DeliveryAck(
|
|
||||||
originalMessageID = message.id,
|
|
||||||
recipientID = myPeerID,
|
|
||||||
recipientNickname = nickname,
|
|
||||||
hopCount = 0u.toUByte() // Will be calculated during relay
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
val ackData = ack.encode() ?: return
|
|
||||||
val typeMarker = MessageType.DELIVERY_ACK.value.toByte()
|
|
||||||
val payloadWithMarker = byteArrayOf(typeMarker) + ackData
|
|
||||||
val encryptedPayload = securityManager.encryptForPeer(payloadWithMarker, senderPeerID)
|
|
||||||
|
|
||||||
if (encryptedPayload == null) {
|
|
||||||
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create inner packet with the delivery ACK data
|
|
||||||
val packet = BitchatPacket(
|
|
||||||
type = MessageType.NOISE_ENCRYPTED.value,
|
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
|
||||||
recipientID = hexStringToByteArray(senderPeerID),
|
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
|
||||||
payload = encryptedPayload,
|
|
||||||
signature = null,
|
|
||||||
ttl = 3u
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use the new encrypt and broadcast function
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to send delivery ACK: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send read receipt for a received private message
|
|
||||||
*/
|
|
||||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
// Create the read receipt
|
val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString()
|
||||||
val receipt = ReadReceipt(
|
|
||||||
originalMessageID = messageID,
|
Log.d(TAG, "📨 Sending PM to $recipientPeerID: ${content.take(30)}...")
|
||||||
readerID = myPeerID,
|
|
||||||
readerNickname = readerNickname
|
// Check if we have an established Noise session
|
||||||
)
|
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||||
|
try {
|
||||||
try {
|
// Create TLV-encoded private message exactly like iOS
|
||||||
// Encode the receipt
|
val privateMessage = com.bitchat.android.model.PrivateMessagePacket(
|
||||||
val receiptData = receipt.encode()
|
messageID = finalMessageID,
|
||||||
val typeMarker = MessageType.READ_RECEIPT.value.toByte()
|
content = content
|
||||||
val payloadWithMarker = byteArrayOf(typeMarker) + receiptData
|
)
|
||||||
val encryptedPayload = securityManager.encryptForPeer(payloadWithMarker, recipientPeerID)
|
|
||||||
|
val tlvData = privateMessage.encode()
|
||||||
if (encryptedPayload == null) {
|
if (tlvData == null) {
|
||||||
Log.w(TAG, "Failed to encrypt delivery ACK for $recipientPeerID")
|
Log.e(TAG, "Failed to encode private message with TLV")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create inner packet with the delivery ACK data
|
// Create message payload with NoisePayloadType prefix: [type byte] + [TLV data]
|
||||||
val packet = BitchatPacket(
|
val messagePayload = com.bitchat.android.model.NoisePayload(
|
||||||
type = MessageType.NOISE_ENCRYPTED.value,
|
type = com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE,
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
data = tlvData
|
||||||
recipientID = hexStringToByteArray(recipientPeerID),
|
)
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
|
||||||
payload = encryptedPayload,
|
// Encrypt the payload
|
||||||
signature = null,
|
val encrypted = encryptionService.encrypt(messagePayload.encode(), recipientPeerID)
|
||||||
ttl = 3u
|
|
||||||
)
|
// Create NOISE_ENCRYPTED packet exactly like iOS
|
||||||
|
val packet = BitchatPacket(
|
||||||
Log.d(TAG, "Sending read receipt for message $messageID to $recipientPeerID")
|
version = 1u,
|
||||||
|
|
||||||
// Use the new encrypt and broadcast function
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to send read receipt for message $messageID: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt a BitchatPacket and broadcast it as a NOISE_ENCRYPTED message
|
|
||||||
* This is the correct protocol implementation - encrypt the entire packet, not just the payload
|
|
||||||
*/
|
|
||||||
private fun encryptAndBroadcastNoisePacket(innerPacket: BitchatPacket, recipientPeerID: String) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
// Serialize the inner packet to binary data
|
|
||||||
val innerPacketData = innerPacket.toBinaryData()
|
|
||||||
if (innerPacketData == null) {
|
|
||||||
Log.e(TAG, "Failed to serialize inner packet for encryption")
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt the serialized packet using Noise encryption
|
|
||||||
val encryptedPayload = securityManager.encryptForPeer(innerPacketData, recipientPeerID)
|
|
||||||
|
|
||||||
if (encryptedPayload != null) {
|
|
||||||
// Create the outer NOISE_ENCRYPTED packet
|
|
||||||
val outerPacket = BitchatPacket(
|
|
||||||
type = MessageType.NOISE_ENCRYPTED.value,
|
type = MessageType.NOISE_ENCRYPTED.value,
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
recipientID = hexStringToByteArray(recipientPeerID),
|
recipientID = hexStringToByteArray(recipientPeerID),
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
payload = encryptedPayload,
|
payload = encrypted,
|
||||||
signature = null,
|
signature = null,
|
||||||
ttl = MAX_TTL
|
ttl = MAX_TTL
|
||||||
)
|
)
|
||||||
|
|
||||||
// Broadcast the encrypted packet
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
connectionManager.broadcastPacket(RoutedPacket(outerPacket))
|
Log.d(TAG, "📤 Sent encrypted private message to $recipientPeerID (${encrypted.size} bytes)")
|
||||||
|
|
||||||
Log.d(TAG, "Encrypted and sent packet type ${innerPacket.type} to $recipientPeerID (${encryptedPayload.size} bytes encrypted)")
|
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||||
} else {
|
// This was causing self-notifications - iOS doesn't do this
|
||||||
Log.w(TAG, "Failed to encrypt packet for $recipientPeerID - no session available")
|
// The UI handles showing sent messages through its own message sending logic
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to encrypt private message for $recipientPeerID: ${e.message}")
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fire and forget - initiate handshake but don't queue exactly like iOS
|
||||||
|
Log.d(TAG, "🤝 No session with $recipientPeerID, initiating handshake")
|
||||||
|
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
// FIXED: Don't send didReceiveMessage for our own sent messages
|
||||||
Log.e(TAG, "Failed to encrypt and broadcast Noise packet to $recipientPeerID: ${e.message}")
|
// The UI will handle showing the message in the chat interface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send broadcast announce
|
* Send read receipt for a received private message - NEW NoisePayloadType implementation
|
||||||
|
* Uses same encryption approach as iOS SimplifiedBluetoothService
|
||||||
|
*/
|
||||||
|
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||||
|
serviceScope.launch {
|
||||||
|
Log.d(TAG, "📖 Sending read receipt for message $messageID to $recipientPeerID")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create read receipt payload using NoisePayloadType exactly like iOS
|
||||||
|
val readReceiptPayload = com.bitchat.android.model.NoisePayload(
|
||||||
|
type = com.bitchat.android.model.NoisePayloadType.READ_RECEIPT,
|
||||||
|
data = messageID.toByteArray(Charsets.UTF_8)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt the payload
|
||||||
|
val encrypted = encryptionService.encrypt(readReceiptPayload.encode(), recipientPeerID)
|
||||||
|
|
||||||
|
// Create NOISE_ENCRYPTED packet exactly like iOS
|
||||||
|
val packet = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
|
type = MessageType.NOISE_ENCRYPTED.value,
|
||||||
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = hexStringToByteArray(recipientPeerID),
|
||||||
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
|
payload = encrypted,
|
||||||
|
signature = null,
|
||||||
|
ttl = 7u // Same TTL as iOS messageTTL
|
||||||
|
)
|
||||||
|
|
||||||
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
|
Log.d(TAG, "📤 Sent read receipt to $recipientPeerID for message $messageID")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to send read receipt to $recipientPeerID: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send broadcast announce with TLV-encoded identity announcement - exactly like iOS
|
||||||
*/
|
*/
|
||||||
fun sendBroadcastAnnounce() {
|
fun sendBroadcastAnnounce() {
|
||||||
Log.d(TAG, "Sending broadcast announce")
|
Log.d(TAG, "Sending broadcast announce")
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val nickname = delegate?.getNickname() ?: myPeerID
|
val nickname = delegate?.getNickname() ?: myPeerID
|
||||||
|
|
||||||
|
// Get the static public key for the announcement
|
||||||
|
val staticKey = encryptionService.getStaticPublicKey()
|
||||||
|
if (staticKey == null) {
|
||||||
|
Log.e(TAG, "No static public key available for announcement")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create iOS-compatible IdentityAnnouncement with TLV encoding
|
||||||
|
val announcement = IdentityAnnouncement(nickname, staticKey)
|
||||||
|
val tlvPayload = announcement.encode()
|
||||||
|
if (tlvPayload == null) {
|
||||||
|
Log.e(TAG, "Failed to encode announcement as TLV")
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val announcePacket = BitchatPacket(
|
val announcePacket = BitchatPacket(
|
||||||
type = MessageType.ANNOUNCE.value,
|
type = MessageType.ANNOUNCE.value,
|
||||||
ttl = MAX_TTL,
|
ttl = MAX_TTL,
|
||||||
senderID = myPeerID,
|
senderID = myPeerID,
|
||||||
payload = nickname.toByteArray()
|
payload = tlvPayload
|
||||||
)
|
)
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
||||||
|
Log.d(TAG, "Sent iOS-compatible TLV announce (${tlvPayload.size} bytes)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send announcement to specific peer
|
* Send announcement to specific peer with TLV-encoded identity announcement - exactly like iOS
|
||||||
*/
|
*/
|
||||||
private fun sendAnnouncementToPeer(peerID: String) {
|
fun sendAnnouncementToPeer(peerID: String) {
|
||||||
if (peerManager.hasAnnouncedToPeer(peerID)) return
|
if (peerManager.hasAnnouncedToPeer(peerID)) return
|
||||||
|
|
||||||
val nickname = delegate?.getNickname() ?: myPeerID
|
val nickname = delegate?.getNickname() ?: myPeerID
|
||||||
|
|
||||||
|
// Get the static public key for the announcement
|
||||||
|
val staticKey = encryptionService.getStaticPublicKey()
|
||||||
|
if (staticKey == null) {
|
||||||
|
Log.e(TAG, "No static public key available for peer announcement")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create iOS-compatible IdentityAnnouncement with TLV encoding
|
||||||
|
val announcement = IdentityAnnouncement(nickname, staticKey)
|
||||||
|
val tlvPayload = announcement.encode()
|
||||||
|
if (tlvPayload == null) {
|
||||||
|
Log.e(TAG, "Failed to encode peer announcement as TLV")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val packet = BitchatPacket(
|
val packet = BitchatPacket(
|
||||||
type = MessageType.ANNOUNCE.value,
|
type = MessageType.ANNOUNCE.value,
|
||||||
ttl = MAX_TTL,
|
ttl = MAX_TTL,
|
||||||
senderID = myPeerID,
|
senderID = myPeerID,
|
||||||
payload = nickname.toByteArray()
|
payload = tlvPayload
|
||||||
)
|
)
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||||
|
Log.d(TAG, "Sent iOS-compatible TLV peer announce to $peerID (${tlvPayload.size} bytes)")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send key exchange to newly connected device
|
|
||||||
*/
|
|
||||||
fun broadcastNoiseIdentityAnnouncement() {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
val nickname = delegate?.getNickname() ?: myPeerID
|
|
||||||
|
|
||||||
// Create the identity announcement using proper binary format
|
|
||||||
val announcement = createNoiseIdentityAnnouncement(nickname, null)
|
|
||||||
if (announcement != null) {
|
|
||||||
val announcementData = announcement.toBinaryData()
|
|
||||||
|
|
||||||
val packet = BitchatPacket(
|
|
||||||
type = MessageType.NOISE_IDENTITY_ANNOUNCE.value,
|
|
||||||
ttl = MAX_TTL,
|
|
||||||
senderID = myPeerID,
|
|
||||||
payload = announcementData,
|
|
||||||
)
|
|
||||||
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
|
||||||
Log.d(TAG, "Sent NoiseIdentityAnnouncement (${announcementData.size} bytes)")
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement")
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to send NoiseIdentityAnnouncement: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send handshake request to target peer for pending messages
|
|
||||||
*/
|
|
||||||
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) {
|
|
||||||
serviceScope.launch {
|
|
||||||
try {
|
|
||||||
// Create handshake request
|
|
||||||
val request = HandshakeRequest(
|
|
||||||
requesterID = myPeerID,
|
|
||||||
requesterNickname = delegate?.getNickname() ?: myPeerID,
|
|
||||||
targetID = targetPeerID,
|
|
||||||
pendingMessageCount = pendingCount
|
|
||||||
)
|
|
||||||
|
|
||||||
val requestData = request.toBinaryData()
|
|
||||||
|
|
||||||
// Create packet for handshake request
|
|
||||||
val packet = BitchatPacket(
|
|
||||||
version = 1u,
|
|
||||||
type = MessageType.HANDSHAKE_REQUEST.value,
|
|
||||||
senderID = hexStringToByteArray(myPeerID),
|
|
||||||
recipientID = hexStringToByteArray(targetPeerID),
|
|
||||||
timestamp = System.currentTimeMillis().toULong(),
|
|
||||||
payload = requestData,
|
|
||||||
ttl = 6u
|
|
||||||
)
|
|
||||||
|
|
||||||
// Broadcast the packet (Android equivalent of both direct and relay attempts)
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
|
||||||
Log.d(TAG, "Sent handshake request to $targetPeerID (pending: $pendingCount, ${requestData.size} bytes)")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to send handshake request to $targetPeerID: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a properly formatted NoiseIdentityAnnouncement exactly like iOS
|
|
||||||
*/
|
|
||||||
private fun createNoiseIdentityAnnouncement(nickname: String, previousPeerID: String?): NoiseIdentityAnnouncement? {
|
|
||||||
return try {
|
|
||||||
// Get the static public key for Noise protocol
|
|
||||||
val staticKey = encryptionService.getStaticPublicKey()
|
|
||||||
if (staticKey == null) {
|
|
||||||
Log.e(TAG, "No static public key available for identity announcement")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the signing public key for Ed25519 signatures
|
|
||||||
val signingKey = encryptionService.getSigningPublicKey()
|
|
||||||
if (signingKey == null) {
|
|
||||||
Log.e(TAG, "No signing public key available for identity announcement")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val now = Date()
|
|
||||||
|
|
||||||
// Create the binding data to sign (same format as iOS)
|
|
||||||
val timestampMs = now.time
|
|
||||||
val bindingData = myPeerID.toByteArray(Charsets.UTF_8) +
|
|
||||||
staticKey +
|
|
||||||
timestampMs.toString().toByteArray(Charsets.UTF_8)
|
|
||||||
|
|
||||||
// Sign the binding with our Ed25519 signing key
|
|
||||||
val signature = encryptionService.signData(bindingData) ?: ByteArray(0)
|
|
||||||
|
|
||||||
// Create the identity announcement
|
|
||||||
NoiseIdentityAnnouncement(
|
|
||||||
peerID = myPeerID,
|
|
||||||
publicKey = staticKey,
|
|
||||||
signingPublicKey = signingKey,
|
|
||||||
nickname = nickname,
|
|
||||||
timestamp = now,
|
|
||||||
previousPeerID = previousPeerID,
|
|
||||||
signature = signature
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to create NoiseIdentityAnnouncement: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send leave announcement
|
* Send leave announcement
|
||||||
*/
|
*/
|
||||||
@@ -1014,8 +826,8 @@ interface BluetoothMeshDelegate {
|
|||||||
fun didReceiveMessage(message: BitchatMessage)
|
fun didReceiveMessage(message: BitchatMessage)
|
||||||
fun didUpdatePeerList(peers: List<String>)
|
fun didUpdatePeerList(peers: List<String>)
|
||||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||||
fun didReceiveDeliveryAck(ack: DeliveryAck)
|
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||||
fun didReceiveReadReceipt(receipt: ReadReceipt)
|
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||||
fun getNickname(): String?
|
fun getNickname(): String?
|
||||||
fun isFavorite(peerID: String): Boolean
|
fun isFavorite(peerID: String): Boolean
|
||||||
|
|||||||
@@ -3,25 +3,33 @@ package com.bitchat.android.mesh
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.protocol.BitchatPacket
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
import com.bitchat.android.protocol.MessageType
|
import com.bitchat.android.protocol.MessageType
|
||||||
|
import com.bitchat.android.model.FragmentPayload
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages message fragmentation and reassembly
|
* Manages message fragmentation and reassembly - 100% iOS Compatible
|
||||||
* Extracted from BluetoothMeshService for better separation of concerns
|
*
|
||||||
|
* This implementation exactly matches iOS SimplifiedBluetoothService fragmentation:
|
||||||
|
* - Same fragment payload structure (13-byte header + data)
|
||||||
|
* - Same MTU thresholds and fragment sizes
|
||||||
|
* - Same reassembly logic and timeout handling
|
||||||
|
* - Uses new FragmentPayload model for type safety
|
||||||
*/
|
*/
|
||||||
class FragmentManager {
|
class FragmentManager {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "FragmentManager"
|
private const val TAG = "FragmentManager"
|
||||||
private const val FRAGMENT_SIZE_THRESHOLD = 512 // 512 bytes
|
// iOS values: 512 MTU threshold, 469 max fragment size (512 MTU - headers)
|
||||||
private const val MAX_FRAGMENT_SIZE = 500 // Match iOS/Rust for BLE compatibility (185 byte MTU limit)
|
private const val FRAGMENT_SIZE_THRESHOLD = 512 // Matches iOS: if data.count > 512
|
||||||
private const val FRAGMENT_TIMEOUT = 30000L // 30 seconds
|
private const val MAX_FRAGMENT_SIZE = 469 // Matches iOS: maxFragmentSize = 469
|
||||||
private const val CLEANUP_INTERVAL = 10000L // 10 seconds
|
private const val FRAGMENT_TIMEOUT = 30000L // Matches iOS: 30 seconds cleanup
|
||||||
|
private const val CLEANUP_INTERVAL = 10000L // 10 seconds cleanup check
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fragment storage
|
// Fragment storage - iOS equivalent: incomingFragments: [String: [Int: Data]]
|
||||||
private val incomingFragments = ConcurrentHashMap<String, MutableMap<Int, ByteArray>>()
|
private val incomingFragments = ConcurrentHashMap<String, MutableMap<Int, ByteArray>>()
|
||||||
|
// iOS equivalent: fragmentMetadata: [String: (type: UInt8, total: Int, timestamp: Date)]
|
||||||
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // originalType, totalFragments, timestamp
|
private val fragmentMetadata = ConcurrentHashMap<String, Triple<UByte, Int, Long>>() // originalType, totalFragments, timestamp
|
||||||
|
|
||||||
// Delegate for callbacks
|
// Delegate for callbacks
|
||||||
@@ -35,51 +43,52 @@ class FragmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create fragments from a large packet
|
* Create fragments from a large packet - 100% iOS Compatible
|
||||||
|
* Matches iOS sendFragmentedPacket() implementation exactly
|
||||||
*/
|
*/
|
||||||
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
|
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
|
||||||
val data = packet.toBinaryData() ?: return emptyList()
|
val fullData = packet.toBinaryData() ?: return emptyList()
|
||||||
|
|
||||||
if (data.size <= FRAGMENT_SIZE_THRESHOLD) {
|
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
|
||||||
|
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||||
return listOf(packet) // No fragmentation needed
|
return listOf(packet) // No fragmentation needed
|
||||||
}
|
}
|
||||||
|
|
||||||
val fragments = mutableListOf<BitchatPacket>()
|
val fragments = mutableListOf<BitchatPacket>()
|
||||||
val fragmentID = generateFragmentID()
|
|
||||||
|
|
||||||
// Fragment overhead: 13 bytes (fragment metadata) + 21 bytes (packet header) = 34 bytes total
|
// iOS: let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||||
// With 150 byte fragments, total packet = ~184 bytes (within iOS 185 byte MTU)
|
val fragmentID = FragmentPayload.generateFragmentID()
|
||||||
val totalFragments = (data.size + MAX_FRAGMENT_SIZE - 1) / MAX_FRAGMENT_SIZE
|
|
||||||
|
|
||||||
Log.d(TAG, "Creating ${totalFragments} fragments for ${data.size} byte packet")
|
// iOS: stride(from: 0, to: fullData.count, by: maxFragmentSize)
|
||||||
|
val fragmentChunks = stride(0, fullData.size, MAX_FRAGMENT_SIZE) { offset ->
|
||||||
|
val endOffset = minOf(offset + MAX_FRAGMENT_SIZE, fullData.size)
|
||||||
|
fullData.sliceArray(offset..<endOffset)
|
||||||
|
}
|
||||||
|
|
||||||
for (i in 0 until totalFragments) {
|
Log.d(TAG, "Creating ${fragmentChunks.size} fragments for ${fullData.size} byte packet (iOS compatible)")
|
||||||
val start = i * MAX_FRAGMENT_SIZE
|
|
||||||
val end = minOf(start + MAX_FRAGMENT_SIZE, data.size)
|
// iOS: for (index, fragment) in fragments.enumerated()
|
||||||
val fragmentData = data.sliceArray(start until end)
|
for (index in fragmentChunks.indices) {
|
||||||
|
val fragmentData = fragmentChunks[index]
|
||||||
|
|
||||||
val fragmentPayload = createFragmentPayload(
|
// Create iOS-compatible fragment payload
|
||||||
|
val fragmentPayload = FragmentPayload(
|
||||||
fragmentID = fragmentID,
|
fragmentID = fragmentID,
|
||||||
index = i,
|
index = index,
|
||||||
total = totalFragments,
|
total = fragmentChunks.size,
|
||||||
originalType = packet.type,
|
originalType = packet.type,
|
||||||
data = fragmentData
|
data = fragmentData
|
||||||
)
|
)
|
||||||
|
|
||||||
val fragmentType = when (i) {
|
// iOS: MessageType.fragment.rawValue (single fragment type)
|
||||||
0 -> MessageType.FRAGMENT_START
|
|
||||||
totalFragments - 1 -> MessageType.FRAGMENT_END
|
|
||||||
else -> MessageType.FRAGMENT_CONTINUE
|
|
||||||
}
|
|
||||||
|
|
||||||
val fragmentPacket = BitchatPacket(
|
val fragmentPacket = BitchatPacket(
|
||||||
type = fragmentType.value,
|
type = MessageType.FRAGMENT.value,
|
||||||
ttl = packet.ttl,
|
ttl = packet.ttl,
|
||||||
senderID = packet.senderID,
|
senderID = packet.senderID,
|
||||||
recipientID = packet.recipientID,
|
recipientID = packet.recipientID,
|
||||||
timestamp = packet.timestamp,
|
timestamp = packet.timestamp,
|
||||||
payload = fragmentPayload,
|
payload = fragmentPayload.encode(),
|
||||||
signature = null // Fragments aren't individually signed
|
signature = null // iOS: signature: nil
|
||||||
)
|
)
|
||||||
|
|
||||||
fragments.add(fragmentPacket)
|
fragments.add(fragmentPacket)
|
||||||
@@ -89,62 +98,82 @@ class FragmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming fragment
|
* Handle incoming fragment - 100% iOS Compatible
|
||||||
|
* Matches iOS handleFragment() implementation exactly
|
||||||
*/
|
*/
|
||||||
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||||
if (packet.payload.size < 13) {
|
// iOS: guard packet.payload.count > 13 else { return }
|
||||||
|
if (packet.payload.size < FragmentPayload.HEADER_SIZE) {
|
||||||
Log.w(TAG, "Fragment packet too small: ${packet.payload.size}")
|
Log.w(TAG, "Fragment packet too small: ${packet.payload.size}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't process our own fragments - iOS equivalent check
|
||||||
|
// This would be done at a higher level but we'll include for safety
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract fragment metadata (same format as iOS)
|
// Use FragmentPayload for type-safe decoding
|
||||||
val fragmentIDData = packet.payload.sliceArray(0..7)
|
val fragmentPayload = FragmentPayload.decode(packet.payload)
|
||||||
val fragmentID = fragmentIDData.contentHashCode().toString()
|
if (fragmentPayload == null || !fragmentPayload.isValid()) {
|
||||||
|
Log.w(TAG, "Invalid fragment payload")
|
||||||
val index = ((packet.payload[8].toInt() and 0xFF) shl 8) or (packet.payload[9].toInt() and 0xFF)
|
return null
|
||||||
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)
|
// iOS: let fragmentID = packet.payload[0..<8].map { String(format: "%02x", $0) }.joined()
|
||||||
|
val fragmentIDString = fragmentPayload.getFragmentIDString()
|
||||||
|
|
||||||
// Check if we have all fragments
|
Log.d(TAG, "Received fragment ${fragmentPayload.index}/${fragmentPayload.total} for fragmentID: $fragmentIDString, originalType: ${fragmentPayload.originalType}")
|
||||||
if (incomingFragments[fragmentID]?.size == total) {
|
|
||||||
Log.d(TAG, "All fragments received for $fragmentID, reassembling...")
|
// iOS: if incomingFragments[fragmentID] == nil
|
||||||
|
if (!incomingFragments.containsKey(fragmentIDString)) {
|
||||||
|
incomingFragments[fragmentIDString] = mutableMapOf()
|
||||||
|
fragmentMetadata[fragmentIDString] = Triple(
|
||||||
|
fragmentPayload.originalType,
|
||||||
|
fragmentPayload.total,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS: incomingFragments[fragmentID]?[index] = Data(fragmentData)
|
||||||
|
incomingFragments[fragmentIDString]?.put(fragmentPayload.index, fragmentPayload.data)
|
||||||
|
|
||||||
|
// iOS: if let fragments = incomingFragments[fragmentID], fragments.count == total
|
||||||
|
val fragmentMap = incomingFragments[fragmentIDString]
|
||||||
|
if (fragmentMap != null && fragmentMap.size == fragmentPayload.total) {
|
||||||
|
Log.d(TAG, "All fragments received for $fragmentIDString, reassembling...")
|
||||||
|
|
||||||
// Reassemble message
|
// iOS reassembly logic: for i in 0..<total { if let fragment = fragments[i] { reassembled.append(fragment) } }
|
||||||
val reassembledData = mutableListOf<Byte>()
|
val reassembledData = mutableListOf<Byte>()
|
||||||
for (i in 0 until total) {
|
for (i in 0 until fragmentPayload.total) {
|
||||||
incomingFragments[fragmentID]?.get(i)?.let { data ->
|
fragmentMap[i]?.let { data ->
|
||||||
reassembledData.addAll(data.asIterable())
|
reassembledData.addAll(data.asIterable())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and return reassembled packet
|
// iOS: if let metadata = fragmentMetadata[fragmentID]
|
||||||
val reassembledPacket = BitchatPacket.fromBinaryData(reassembledData.toByteArray())
|
val metadata = fragmentMetadata[fragmentIDString]
|
||||||
|
if (metadata != null) {
|
||||||
// Cleanup
|
// iOS: let reassembledPacket = BitchatPacket(type: metadata.type, ...)
|
||||||
incomingFragments.remove(fragmentID)
|
val reassembledPacket = BitchatPacket(
|
||||||
fragmentMetadata.remove(fragmentID)
|
type = metadata.first,
|
||||||
|
senderID = packet.senderID,
|
||||||
if (reassembledPacket != null) {
|
recipientID = packet.recipientID,
|
||||||
|
timestamp = packet.timestamp,
|
||||||
|
payload = reassembledData.toByteArray(),
|
||||||
|
signature = packet.signature,
|
||||||
|
ttl = if (packet.ttl > 0u) (packet.ttl - 1u).toUByte() else 0u
|
||||||
|
)
|
||||||
|
|
||||||
|
// iOS cleanup: incomingFragments.removeValue(forKey: fragmentID)
|
||||||
|
incomingFragments.remove(fragmentIDString)
|
||||||
|
fragmentMetadata.remove(fragmentIDString)
|
||||||
|
|
||||||
Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes")
|
Log.d(TAG, "Successfully reassembled packet of ${reassembledData.size} bytes")
|
||||||
return reassembledPacket
|
return reassembledPacket
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Failed to parse reassembled packet")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
val received = fragmentMap?.size ?: 0
|
||||||
Log.d(TAG, "Fragment $index stored, have $received/$total fragments for $fragmentID")
|
Log.d(TAG, "Fragment ${fragmentPayload.index} stored, have $received/${fragmentPayload.total} fragments for $fragmentIDString")
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -155,53 +184,51 @@ class FragmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create fragment payload with metadata
|
* Helper function to match iOS stride functionality
|
||||||
|
* stride(from: 0, to: fullData.count, by: maxFragmentSize)
|
||||||
*/
|
*/
|
||||||
private fun createFragmentPayload(
|
private fun <T> stride(from: Int, to: Int, by: Int, transform: (Int) -> T): List<T> {
|
||||||
fragmentID: ByteArray,
|
val result = mutableListOf<T>()
|
||||||
index: Int,
|
var current = from
|
||||||
total: Int,
|
while (current < to) {
|
||||||
originalType: UByte,
|
result.add(transform(current))
|
||||||
data: ByteArray
|
current += by
|
||||||
): ByteArray {
|
}
|
||||||
val payload = ByteArray(13 + data.size)
|
return result
|
||||||
|
|
||||||
// Fragment ID (8 bytes)
|
|
||||||
System.arraycopy(fragmentID, 0, payload, 0, 8)
|
|
||||||
|
|
||||||
// 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 (8 random bytes to match iOS/Rust)
|
* iOS cleanup - exactly matching performCleanup() implementation
|
||||||
|
* Clean old fragments (> 30 seconds old)
|
||||||
*/
|
*/
|
||||||
private fun generateFragmentID(): ByteArray {
|
private fun cleanupOldFragments() {
|
||||||
val fragmentID = ByteArray(8)
|
val now = System.currentTimeMillis()
|
||||||
kotlin.random.Random.nextBytes(fragmentID)
|
val cutoff = now - FRAGMENT_TIMEOUT
|
||||||
return fragmentID
|
|
||||||
|
// iOS: let oldFragments = fragmentMetadata.filter { $0.value.timestamp < cutoff }.map { $0.key }
|
||||||
|
val oldFragments = fragmentMetadata.filter { it.value.third < cutoff }.map { it.key }
|
||||||
|
|
||||||
|
// iOS: for fragmentID in oldFragments { incomingFragments.removeValue(forKey: fragmentID) }
|
||||||
|
for (fragmentID in oldFragments) {
|
||||||
|
incomingFragments.remove(fragmentID)
|
||||||
|
fragmentMetadata.remove(fragmentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldFragments.isNotEmpty()) {
|
||||||
|
Log.d(TAG, "Cleaned up ${oldFragments.size} old fragment sets (iOS compatible)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get debug information
|
* Get debug information - matches iOS debugging
|
||||||
*/
|
*/
|
||||||
fun getDebugInfo(): String {
|
fun getDebugInfo(): String {
|
||||||
return buildString {
|
return buildString {
|
||||||
appendLine("=== Fragment Manager Debug Info ===")
|
appendLine("=== Fragment Manager Debug Info (iOS Compatible) ===")
|
||||||
appendLine("Active Fragment Sets: ${incomingFragments.size}")
|
appendLine("Active Fragment Sets: ${incomingFragments.size}")
|
||||||
|
appendLine("Fragment Size Threshold: $FRAGMENT_SIZE_THRESHOLD bytes")
|
||||||
|
appendLine("Max Fragment Size: $MAX_FRAGMENT_SIZE bytes")
|
||||||
|
|
||||||
fragmentMetadata.forEach { (fragmentID, metadata) ->
|
fragmentMetadata.forEach { (fragmentID, metadata) ->
|
||||||
val (originalType, totalFragments, timestamp) = metadata
|
val (originalType, totalFragments, timestamp) = metadata
|
||||||
val received = incomingFragments[fragmentID]?.size ?: 0
|
val received = incomingFragments[fragmentID]?.size ?: 0
|
||||||
@@ -212,7 +239,7 @@ class FragmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start periodic cleanup of old fragments
|
* Start periodic cleanup of old fragments - matches iOS maintenance timer
|
||||||
*/
|
*/
|
||||||
private fun startPeriodicCleanup() {
|
private fun startPeriodicCleanup() {
|
||||||
managerScope.launch {
|
managerScope.launch {
|
||||||
@@ -223,29 +250,6 @@ class FragmentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* Clear all fragments
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package com.bitchat.android.mesh
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryAck
|
import com.bitchat.android.model.IdentityAnnouncement
|
||||||
import com.bitchat.android.model.NoiseIdentityAnnouncement
|
|
||||||
import com.bitchat.android.model.ReadReceipt
|
|
||||||
import com.bitchat.android.model.RoutedPacket
|
import com.bitchat.android.model.RoutedPacket
|
||||||
import com.bitchat.android.protocol.BitchatPacket
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
import com.bitchat.android.protocol.MessageType
|
import com.bitchat.android.protocol.MessageType
|
||||||
@@ -33,7 +31,8 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val handlerScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Noise encrypted transport message
|
* Handle Noise encrypted transport message - SIMPLIFIED iOS-compatible version
|
||||||
|
* Uses NoisePayloadType system exactly like iOS SimplifiedBluetoothService
|
||||||
*/
|
*/
|
||||||
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
suspend fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
@@ -44,6 +43,13 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
// Skip our own messages
|
// Skip our own messages
|
||||||
if (peerID == myPeerID) return
|
if (peerID == myPeerID) return
|
||||||
|
|
||||||
|
// Check if this message is for us
|
||||||
|
val recipientID = packet.recipientID?.toHexString()
|
||||||
|
if (recipientID != myPeerID) {
|
||||||
|
Log.d(TAG, "🔐 Encrypted message not for me (for $recipientID, I am $myPeerID)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Decrypt the message using the Noise service
|
// Decrypt the message using the Noise service
|
||||||
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
||||||
@@ -52,44 +58,66 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a special format message (type marker + payload)
|
if (decryptedData.isEmpty()) {
|
||||||
if (decryptedData.size > 1) {
|
Log.w(TAG, "Decrypted data is empty from $peerID")
|
||||||
val typeMarker = decryptedData[0].toUByte()
|
return
|
||||||
|
|
||||||
// Check if this is a delivery ACK with the new format
|
|
||||||
if (typeMarker == MessageType.DELIVERY_ACK.value) {
|
|
||||||
handleDeliveryAck(decryptedData)
|
|
||||||
Log.d(TAG, "Processed delivery ACK from $peerID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for read receipt with type marker
|
|
||||||
if (typeMarker == MessageType.READ_RECEIPT.value) {
|
|
||||||
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
|
|
||||||
val receipt = ReadReceipt.decode(receiptData)
|
|
||||||
if (receipt != null) {
|
|
||||||
delegate?.onReadReceiptReceived(receipt)
|
|
||||||
Log.d(TAG, "Processed read receipt from $peerID")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to parse as a full inner packet (for compatibility with other message types)
|
// NEW: Use NoisePayload system exactly like iOS
|
||||||
val innerPacket = BitchatPacket.fromBinaryData(decryptedData)
|
val noisePayload = com.bitchat.android.model.NoisePayload.decode(decryptedData)
|
||||||
if (innerPacket != null) {
|
if (noisePayload == null) {
|
||||||
Log.d(TAG, "Decrypted inner packet type ${innerPacket.type} from $peerID")
|
Log.w(TAG, "Failed to parse NoisePayload from $peerID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "🔓 Decrypted NoisePayload type ${noisePayload.type} from $peerID")
|
||||||
|
|
||||||
|
when (noisePayload.type) {
|
||||||
|
com.bitchat.android.model.NoisePayloadType.PRIVATE_MESSAGE -> {
|
||||||
|
// Decode TLV private message exactly like iOS
|
||||||
|
val privateMessage = com.bitchat.android.model.PrivateMessagePacket.decode(noisePayload.data)
|
||||||
|
if (privateMessage != null) {
|
||||||
|
Log.d(TAG, "🔓 Decrypted TLV PM from $peerID: ${privateMessage.content.take(30)}...")
|
||||||
|
|
||||||
|
// Create BitchatMessage exactly like iOS
|
||||||
|
val message = BitchatMessage(
|
||||||
|
id = privateMessage.messageID,
|
||||||
|
sender = delegate?.getPeerNickname(peerID) ?: "Unknown",
|
||||||
|
content = privateMessage.content,
|
||||||
|
timestamp = java.util.Date(packet.timestamp.toLong()),
|
||||||
|
isRelay = false,
|
||||||
|
originalSender = null,
|
||||||
|
isPrivate = true,
|
||||||
|
recipientNickname = delegate?.getMyNickname(),
|
||||||
|
senderPeerID = peerID,
|
||||||
|
mentions = null // TODO: Parse mentions if needed
|
||||||
|
)
|
||||||
|
|
||||||
|
// Notify delegate
|
||||||
|
delegate?.onMessageReceived(message)
|
||||||
|
|
||||||
|
// Send delivery ACK exactly like iOS
|
||||||
|
sendDeliveryAck(privateMessage.messageID, peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new routed packet with the decrypted inner packet
|
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
||||||
val innerRouted = RoutedPacket(innerPacket, peerID, routed.relayAddress)
|
// Handle delivery ACK exactly like iOS
|
||||||
|
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||||
|
Log.d(TAG, "📬 Delivery ACK received from $peerID for message $messageID")
|
||||||
|
|
||||||
|
// Simplified: Call delegate with messageID and peerID directly
|
||||||
|
delegate?.onDeliveryAckReceived(messageID, peerID)
|
||||||
|
}
|
||||||
|
|
||||||
// Use PacketProcessor to handle the inner packet recursively
|
com.bitchat.android.model.NoisePayloadType.READ_RECEIPT -> {
|
||||||
if (packetProcessor != null) {
|
// Handle read receipt exactly like iOS
|
||||||
packetProcessor!!.processPacket(innerRouted)
|
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||||
} else {
|
Log.d(TAG, "👁️ Read receipt received from $peerID for message $messageID")
|
||||||
Log.w(TAG, "PacketProcessor reference is null; cannot recursively process inner packet.")
|
|
||||||
|
// Simplified: Call delegate with messageID and peerID directly
|
||||||
|
delegate?.onReadReceiptReceived(messageID, peerID)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Failed to parse decrypted data as packet from $peerID")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -98,77 +126,45 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Noise identity announcement - supports peer ID rotation
|
* Send delivery ACK for a received private message - exactly like iOS
|
||||||
*/
|
*/
|
||||||
suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
|
private suspend fun sendDeliveryAck(messageID: String, senderPeerID: String) {
|
||||||
val packet = routed.packet
|
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
|
|
||||||
Log.d(TAG, "Processing Noise identity announcement from $peerID (${packet.payload.size} bytes)")
|
|
||||||
|
|
||||||
// Skip our own announcements
|
|
||||||
if (peerID == myPeerID) return
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse the identity announcement
|
// Create ACK payload: [type byte] + [message ID] - exactly like iOS
|
||||||
val announcement = NoiseIdentityAnnouncement.fromBinaryData(packet.payload)
|
val ackPayload = com.bitchat.android.model.NoisePayload(
|
||||||
if (announcement == null) {
|
type = com.bitchat.android.model.NoisePayloadType.DELIVERED,
|
||||||
Log.w(TAG, "Failed to parse Noise identity announcement from $peerID")
|
data = messageID.toByteArray(Charsets.UTF_8)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Parsed identity announcement: peerID=${announcement.peerID}, " +
|
|
||||||
"nickname=${announcement.nickname}, fingerprint=${announcement.fingerprint?.take(16)}...")
|
|
||||||
|
|
||||||
// Verify the announcement signature using Ed25519 (iOS compatibility)
|
|
||||||
if (announcement.signature.isEmpty()) {
|
|
||||||
Log.w(TAG, "❌ Identity announcement from $peerID has no signature - rejecting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature using the same format as iOS
|
|
||||||
val timestampMs = announcement.timestamp.time
|
|
||||||
val bindingData = announcement.peerID.toByteArray(Charsets.UTF_8) +
|
|
||||||
announcement.publicKey +
|
|
||||||
timestampMs.toString().toByteArray(Charsets.UTF_8)
|
|
||||||
|
|
||||||
val isSignatureValid = delegate?.verifyEd25519Signature(announcement.signature, bindingData, announcement.signingPublicKey) ?: false
|
|
||||||
|
|
||||||
if (!isSignatureValid) {
|
|
||||||
Log.w(TAG, "❌ Signature verification failed for identity announcement from $peerID - rejecting")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "✅ Signature verification successful for identity announcement from $peerID")
|
|
||||||
|
|
||||||
// Update peer binding in the delegate (ChatViewModel/BluetoothMeshService)
|
|
||||||
delegate?.updatePeerIDBinding(
|
|
||||||
newPeerID = announcement.peerID,
|
|
||||||
nickname = announcement.nickname,
|
|
||||||
publicKey = announcement.publicKey,
|
|
||||||
previousPeerID = announcement.previousPeerID
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Check if we need to initiate a handshake with this peer
|
// Encrypt the payload
|
||||||
val hasSession = delegate?.hasNoiseSession(announcement.peerID) ?: false
|
val encryptedPayload = delegate?.encryptForPeer(ackPayload.encode(), senderPeerID)
|
||||||
if (!hasSession) {
|
if (encryptedPayload == null) {
|
||||||
Log.d(TAG, "No session with ${announcement.peerID}, may need handshake")
|
Log.w(TAG, "Failed to encrypt delivery ACK for $senderPeerID")
|
||||||
|
return
|
||||||
// Use lexicographic comparison to decide who initiates (prevents both sides from initiating)
|
|
||||||
if (myPeerID < announcement.peerID) {
|
|
||||||
delegate?.initiateNoiseHandshake(announcement.peerID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Successfully processed identity announcement from $peerID")
|
// Create NOISE_ENCRYPTED packet exactly like iOS
|
||||||
|
val packet = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
|
type = MessageType.NOISE_ENCRYPTED.value,
|
||||||
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = hexStringToByteArray(senderPeerID),
|
||||||
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
|
payload = encryptedPayload,
|
||||||
|
signature = null,
|
||||||
|
ttl = 7u // Same TTL as iOS messageTTL
|
||||||
|
)
|
||||||
|
|
||||||
|
delegate?.sendPacket(packet)
|
||||||
|
Log.d(TAG, "📤 Sent delivery ACK to $senderPeerID for message $messageID")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error processing Noise identity announcement from $peerID: ${e.message}")
|
Log.e(TAG, "Failed to send delivery ACK to $senderPeerID: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle announce message
|
* Handle announce message with TLV decoding support - exactly like iOS
|
||||||
*/
|
*/
|
||||||
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
suspend fun handleAnnounce(routed: RoutedPacket): Boolean {
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
@@ -176,17 +172,90 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
|
|
||||||
if (peerID == myPeerID) return false
|
if (peerID == myPeerID) return false
|
||||||
|
|
||||||
val nickname = String(packet.payload, Charsets.UTF_8)
|
// Try to decode as iOS-compatible IdentityAnnouncement with TLV format
|
||||||
Log.d(TAG, "Received announce from $peerID: $nickname")
|
val announcement = IdentityAnnouncement.decode(packet.payload)
|
||||||
|
if (announcement == null) {
|
||||||
|
Log.w(TAG, "Failed to decode announce from $peerID as iOS-compatible TLV format")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Notify delegate to handle peer management
|
// Successfully decoded TLV format exactly like iOS
|
||||||
|
Log.d(TAG, "Received iOS-compatible announce from $peerID: nickname=${announcement.nickname}, " +
|
||||||
|
"publicKey=${announcement.publicKey.joinToString("") { "%02x".format(it) }.take(16)}...")
|
||||||
|
|
||||||
|
// Extract nickname and public key from TLV data
|
||||||
|
val nickname = announcement.nickname
|
||||||
|
val publicKey = announcement.publicKey
|
||||||
|
|
||||||
|
// Notify delegate to handle peer management with nickname
|
||||||
val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false
|
val isFirstAnnounce = delegate?.addOrUpdatePeer(peerID, nickname) ?: false
|
||||||
|
|
||||||
|
// Update peer ID binding with public key for identity management
|
||||||
|
delegate?.updatePeerIDBinding(
|
||||||
|
newPeerID = peerID,
|
||||||
|
nickname = nickname,
|
||||||
|
publicKey = publicKey,
|
||||||
|
previousPeerID = null
|
||||||
|
)
|
||||||
|
|
||||||
// Announce relay is now handled by centralized PacketRelayManager
|
Log.d(TAG, "Processed iOS-compatible TLV announce: stored public key for $peerID")
|
||||||
|
|
||||||
return isFirstAnnounce
|
return isFirstAnnounce
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Noise handshake - SIMPLIFIED iOS-compatible version
|
||||||
|
* Single handshake type (0x10) with response determined by payload analysis
|
||||||
|
*/
|
||||||
|
suspend fun handleNoiseHandshake(routed: RoutedPacket) {
|
||||||
|
val packet = routed.packet
|
||||||
|
val peerID = routed.peerID ?: "unknown"
|
||||||
|
|
||||||
|
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
||||||
|
|
||||||
|
// Skip our own handshake messages
|
||||||
|
if (peerID == myPeerID) return
|
||||||
|
|
||||||
|
// Check if handshake is addressed to us
|
||||||
|
val recipientID = packet.recipientID?.toHexString()
|
||||||
|
if (recipientID != myPeerID) {
|
||||||
|
Log.d(TAG, "Handshake not for me (for $recipientID, I am $myPeerID)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Process handshake message through delegate (simplified approach)
|
||||||
|
val response = delegate?.processNoiseHandshakeMessage(packet.payload, peerID)
|
||||||
|
|
||||||
|
if (response != null) {
|
||||||
|
Log.d(TAG, "Generated handshake response for $peerID (${response.size} bytes)")
|
||||||
|
|
||||||
|
// Send response using same packet type (simplified iOS approach)
|
||||||
|
val responsePacket = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
|
type = MessageType.NOISE_HANDSHAKE.value,
|
||||||
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = hexStringToByteArray(peerID),
|
||||||
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
|
payload = response,
|
||||||
|
signature = null,
|
||||||
|
ttl = 7u // Same TTL as iOS
|
||||||
|
)
|
||||||
|
|
||||||
|
delegate?.sendPacket(responsePacket)
|
||||||
|
Log.d(TAG, "📤 Sent handshake response to $peerID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if session is now established
|
||||||
|
val hasSession = delegate?.hasNoiseSession(peerID) ?: false
|
||||||
|
if (hasSession) {
|
||||||
|
Log.d(TAG, "✅ Noise session established with $peerID")
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to process Noise handshake from $peerID: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle broadcast or private message
|
* Handle broadcast or private message
|
||||||
*/
|
*/
|
||||||
@@ -194,6 +263,11 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
if (peerID == myPeerID) return
|
if (peerID == myPeerID) return
|
||||||
|
val senderNickname = delegate?.getPeerNickname(peerID)
|
||||||
|
if (senderNickname != null) {
|
||||||
|
Log.d(TAG, "Received message from $senderNickname")
|
||||||
|
delegate?.updatePeerNickname(peerID, senderNickname)
|
||||||
|
}
|
||||||
|
|
||||||
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
val recipientID = packet.recipientID?.takeIf { !it.contentEquals(delegate?.getBroadcastRecipient()) }
|
||||||
|
|
||||||
@@ -215,35 +289,14 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
try {
|
try {
|
||||||
// Parse message
|
// Parse message
|
||||||
val message = BitchatMessage.fromBinaryPayload(packet.payload)
|
val message = BitchatMessage(
|
||||||
if (message != null) {
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||||
// Check for cover traffic (dummy messages)
|
content = String(packet.payload, Charsets.UTF_8),
|
||||||
if (message.content.startsWith("☂DUMMY☂")) {
|
senderPeerID = peerID,
|
||||||
Log.d(TAG, "Discarding cover traffic from $peerID")
|
timestamp = Date()
|
||||||
return // Silently discard
|
)
|
||||||
}
|
|
||||||
|
delegate?.onMessageReceived(message)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Broadcast message relay is now handled by centralized PacketRelayManager
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to process broadcast message: ${e.message}")
|
Log.e(TAG, "Failed to process broadcast message: ${e.message}")
|
||||||
@@ -262,21 +315,14 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse message
|
// Parse message
|
||||||
val message = BitchatMessage.fromBinaryPayload(packet.payload)
|
val message = BitchatMessage(
|
||||||
if (message != null) {
|
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||||
// Check for cover traffic (dummy messages)
|
content = String(packet.payload, Charsets.UTF_8),
|
||||||
if (message.content.startsWith("☂DUMMY☂")) {
|
senderPeerID = peerID,
|
||||||
Log.d(TAG, "Discarding private cover traffic from $peerID")
|
timestamp = Date()
|
||||||
return // Silently discard
|
)
|
||||||
}
|
delegate?.onMessageReceived(message)
|
||||||
|
|
||||||
delegate?.updatePeerNickname(peerID, message.sender)
|
|
||||||
delegate?.onMessageReceived(message)
|
|
||||||
|
|
||||||
// Send delivery ACK
|
|
||||||
delegate?.sendDeliveryAck(message, peerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to process private message from $peerID: ${e.message}")
|
Log.e(TAG, "Failed to process private message from $peerID: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -301,31 +347,6 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
// Leave message relay is now handled by centralized PacketRelayManager
|
// Leave message relay is now handled by centralized PacketRelayManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle delivery acknowledgment
|
|
||||||
*/
|
|
||||||
suspend fun handleDeliveryAck(decryptedData: ByteArray) {
|
|
||||||
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
|
|
||||||
val ack = DeliveryAck.decode(ackData)
|
|
||||||
if (ack != null) {
|
|
||||||
delegate?.onDeliveryAckReceived(ack)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle read receipt
|
|
||||||
*/
|
|
||||||
suspend fun handleReadReceipt(routed: RoutedPacket) {
|
|
||||||
val packet = routed.packet
|
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
val receipt = ReadReceipt.decode(routed.packet.payload)
|
|
||||||
if (receipt != null) {
|
|
||||||
delegate?.onReadReceiptReceived(receipt)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get debug information
|
* Get debug information
|
||||||
*/
|
*/
|
||||||
@@ -392,16 +413,16 @@ interface MessageHandlerDelegate {
|
|||||||
// Noise protocol operations
|
// Noise protocol operations
|
||||||
fun hasNoiseSession(peerID: String): Boolean
|
fun hasNoiseSession(peerID: String): Boolean
|
||||||
fun initiateNoiseHandshake(peerID: String)
|
fun initiateNoiseHandshake(peerID: String)
|
||||||
|
fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray?
|
||||||
fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
fun updatePeerIDBinding(newPeerID: String, nickname: String,
|
||||||
publicKey: ByteArray, previousPeerID: String?)
|
publicKey: ByteArray, previousPeerID: String?)
|
||||||
|
|
||||||
// Message operations
|
// Message operations
|
||||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||||
fun sendDeliveryAck(message: BitchatMessage, senderPeerID: String)
|
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
fun onMessageReceived(message: BitchatMessage)
|
fun onMessageReceived(message: BitchatMessage)
|
||||||
fun onChannelLeave(channel: String, fromPeer: String)
|
fun onChannelLeave(channel: String, fromPeer: String)
|
||||||
fun onDeliveryAckReceived(ack: DeliveryAck)
|
fun onDeliveryAckReceived(messageID: String, peerID: String)
|
||||||
fun onReadReceiptReceived(receipt: ReadReceipt)
|
fun onReadReceiptReceived(messageID: String, peerID: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,22 +133,16 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
|
|
||||||
// Handle public packet types (no address check needed)
|
// Handle public packet types (no address check needed)
|
||||||
when (messageType) {
|
when (messageType) {
|
||||||
MessageType.NOISE_IDENTITY_ANNOUNCE -> handleNoiseIdentityAnnouncement(routed)
|
|
||||||
MessageType.ANNOUNCE -> handleAnnounce(routed)
|
MessageType.ANNOUNCE -> handleAnnounce(routed)
|
||||||
MessageType.MESSAGE -> handleMessage(routed)
|
MessageType.MESSAGE -> handleMessage(routed)
|
||||||
MessageType.LEAVE -> handleLeave(routed)
|
MessageType.LEAVE -> handleLeave(routed)
|
||||||
MessageType.FRAGMENT_START,
|
MessageType.FRAGMENT -> handleFragment(routed)
|
||||||
MessageType.FRAGMENT_CONTINUE,
|
|
||||||
MessageType.FRAGMENT_END -> handleFragment(routed)
|
|
||||||
else -> {
|
else -> {
|
||||||
// Handle private packet types (address check required)
|
// Handle private packet types (address check required)
|
||||||
if (packetRelayManager.isPacketAddressedToMe(packet)) {
|
if (packetRelayManager.isPacketAddressedToMe(packet)) {
|
||||||
when (messageType) {
|
when (messageType) {
|
||||||
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
|
MessageType.NOISE_HANDSHAKE -> handleNoiseHandshake(routed)
|
||||||
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
|
|
||||||
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
||||||
// MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) // custom packet type...
|
|
||||||
// MessageType.READ_RECEIPT -> handleReadReceipt(routed)
|
|
||||||
else -> {
|
else -> {
|
||||||
validPacket = false
|
validPacket = false
|
||||||
Log.w(TAG, "Unknown message type: ${packet.type}")
|
Log.w(TAG, "Unknown message type: ${packet.type}")
|
||||||
@@ -170,12 +164,12 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Noise handshake message
|
* Handle Noise handshake message - SIMPLIFIED iOS-compatible version
|
||||||
*/
|
*/
|
||||||
private fun handleNoiseHandshake(routed: RoutedPacket, step: Int) {
|
private suspend fun handleNoiseHandshake(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
Log.d(TAG, "Processing Noise handshake step $step from ${formatPeerForLog(peerID)}")
|
Log.d(TAG, "Processing Noise handshake from ${formatPeerForLog(peerID)}")
|
||||||
delegate?.handleNoiseHandshake(routed, step)
|
delegate?.handleNoiseHandshake(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,15 +181,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
delegate?.handleNoiseEncrypted(routed)
|
delegate?.handleNoiseEncrypted(routed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle Noise identity announcement (after peer ID rotation)
|
|
||||||
*/
|
|
||||||
private suspend fun handleNoiseIdentityAnnouncement(routed: RoutedPacket) {
|
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing Noise identity announcement from ${formatPeerForLog(peerID)}")
|
|
||||||
delegate?.handleNoiseIdentityAnnouncement(routed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle announce message
|
* Handle announce message
|
||||||
*/
|
*/
|
||||||
@@ -248,15 +233,6 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
// delegate?.handleDeliveryAck(routed)
|
// delegate?.handleDeliveryAck(routed)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle read receipt
|
|
||||||
*/
|
|
||||||
private suspend fun handleReadReceipt(routed: RoutedPacket) {
|
|
||||||
val peerID = routed.peerID ?: "unknown"
|
|
||||||
Log.d(TAG, "Processing read receipt from ${formatPeerForLog(peerID)}")
|
|
||||||
delegate?.handleReadReceipt(routed)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get debug information
|
* Get debug information
|
||||||
*/
|
*/
|
||||||
@@ -314,15 +290,12 @@ interface PacketProcessorDelegate {
|
|||||||
fun getBroadcastRecipient(): ByteArray
|
fun getBroadcastRecipient(): ByteArray
|
||||||
|
|
||||||
// Message type handlers
|
// Message type handlers
|
||||||
fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean
|
fun handleNoiseHandshake(routed: RoutedPacket): Boolean
|
||||||
fun handleNoiseEncrypted(routed: RoutedPacket)
|
fun handleNoiseEncrypted(routed: RoutedPacket)
|
||||||
fun handleNoiseIdentityAnnouncement(routed: RoutedPacket)
|
|
||||||
fun handleAnnounce(routed: RoutedPacket)
|
fun handleAnnounce(routed: RoutedPacket)
|
||||||
fun handleMessage(routed: RoutedPacket)
|
fun handleMessage(routed: RoutedPacket)
|
||||||
fun handleLeave(routed: RoutedPacket)
|
fun handleLeave(routed: RoutedPacket)
|
||||||
fun handleFragment(packet: BitchatPacket): BitchatPacket?
|
fun handleFragment(packet: BitchatPacket): BitchatPacket?
|
||||||
// fun handleDeliveryAck(routed: RoutedPacket)
|
|
||||||
fun handleReadReceipt(routed: RoutedPacket)
|
|
||||||
|
|
||||||
// Communication
|
// Communication
|
||||||
fun sendAnnouncementToPeer(peerID: String)
|
fun sendAnnouncementToPeer(peerID: String)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.bitchat.android.mesh
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.model.BitchatMessage
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
|
|||||||
@@ -88,9 +88,10 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Noise handshake packet
|
* Handle Noise handshake packet - SIMPLIFIED iOS-compatible version
|
||||||
|
* Single handshake type with automatic response handling
|
||||||
*/
|
*/
|
||||||
suspend fun handleNoiseHandshake(routed: RoutedPacket, step: Int): Boolean {
|
suspend fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
Log.d(TAG, "Already processed handshake: $exchangeKey")
|
Log.d(TAG, "Already processed handshake: $exchangeKey")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Processing Noise handshake step $step from $peerID (${packet.payload.size} bytes)")
|
Log.d(TAG, "Processing Noise handshake from $peerID (${packet.payload.size} bytes)")
|
||||||
processedKeyExchanges.add(exchangeKey)
|
processedKeyExchanges.add(exchangeKey)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -128,7 +129,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
val response = encryptionService.processHandshakeMessage(packet.payload, peerID)
|
val response = encryptionService.processHandshakeMessage(packet.payload, peerID)
|
||||||
|
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
Log.d(TAG, "Successfully processed Noise handshake step $step from $peerID, sending response")
|
Log.d(TAG, "Successfully processed Noise handshake from $peerID, sending response")
|
||||||
// Send handshake response through delegate
|
// Send handshake response through delegate
|
||||||
delegate?.sendHandshakeResponse(peerID, response)
|
delegate?.sendHandshakeResponse(peerID, response)
|
||||||
}
|
}
|
||||||
@@ -212,7 +213,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
|||||||
*/
|
*/
|
||||||
private fun generateMessageID(packet: BitchatPacket, peerID: String): String {
|
private fun generateMessageID(packet: BitchatPacket, peerID: String): String {
|
||||||
return when (MessageType.fromValue(packet.type)) {
|
return when (MessageType.fromValue(packet.type)) {
|
||||||
MessageType.FRAGMENT_START, MessageType.FRAGMENT_CONTINUE, MessageType.FRAGMENT_END -> {
|
MessageType.FRAGMENT -> {
|
||||||
// For fragments, include the payload hash to distinguish different fragments
|
// For fragments, include the payload hash to distinguish different fragments
|
||||||
"${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}"
|
"${packet.timestamp}-$peerID-${packet.type}-${packet.payload.contentHashCode()}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,10 +53,8 @@ class StoreForwardManager {
|
|||||||
*/
|
*/
|
||||||
fun cacheMessage(packet: BitchatPacket, messageID: String) {
|
fun cacheMessage(packet: BitchatPacket, messageID: String) {
|
||||||
// Skip certain message types (same as iOS)
|
// Skip certain message types (same as iOS)
|
||||||
if (packet.type == MessageType.NOISE_HANDSHAKE_INIT.value ||
|
if (packet.type == MessageType.NOISE_HANDSHAKE.value ||
|
||||||
packet.type == MessageType.NOISE_HANDSHAKE_RESP.value ||
|
|
||||||
packet.type == MessageType.NOISE_ENCRYPTED.value ||
|
packet.type == MessageType.NOISE_ENCRYPTED.value ||
|
||||||
packet.type == MessageType.NOISE_IDENTITY_ANNOUNCE.value ||
|
|
||||||
packet.type == MessageType.ANNOUNCE.value ||
|
packet.type == MessageType.ANNOUNCE.value ||
|
||||||
packet.type == MessageType.LEAVE.value) {
|
packet.type == MessageType.LEAVE.value) {
|
||||||
Log.d(TAG, "Skipping cache for message type: ${packet.type}")
|
Log.d(TAG, "Skipping cache for message type: ${packet.type}")
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ import java.util.*
|
|||||||
sealed class DeliveryStatus : Parcelable {
|
sealed class DeliveryStatus : Parcelable {
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Sending : DeliveryStatus()
|
object Sending : DeliveryStatus()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object Sent : DeliveryStatus()
|
object Sent : DeliveryStatus()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Delivered(val to: String, val at: Date) : DeliveryStatus()
|
data class Delivered(val to: String, val at: Date) : DeliveryStatus()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Read(val by: String, val at: Date) : DeliveryStatus()
|
data class Read(val by: String, val at: Date) : DeliveryStatus()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Failed(val reason: String) : DeliveryStatus()
|
data class Failed(val reason: String) : DeliveryStatus()
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class PartiallyDelivered(val reached: Int, val total: Int) : DeliveryStatus()
|
data class PartiallyDelivered(val reached: Int, val total: Int) : DeliveryStatus()
|
||||||
|
|
||||||
fun getDisplayText(): String {
|
fun getDisplayText(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is Sending -> "Sending..."
|
is Sending -> "Sending..."
|
||||||
@@ -68,7 +68,7 @@ data class BitchatMessage(
|
|||||||
fun toBinaryPayload(): ByteArray? {
|
fun toBinaryPayload(): ByteArray? {
|
||||||
try {
|
try {
|
||||||
val buffer = ByteBuffer.allocate(4096).apply { order(ByteOrder.BIG_ENDIAN) }
|
val buffer = ByteBuffer.allocate(4096).apply { order(ByteOrder.BIG_ENDIAN) }
|
||||||
|
|
||||||
// Message format:
|
// Message format:
|
||||||
// - Flags: 1 byte (bit flags for optional fields)
|
// - Flags: 1 byte (bit flags for optional fields)
|
||||||
// - Timestamp: 8 bytes (milliseconds since epoch, big-endian)
|
// - Timestamp: 8 bytes (milliseconds since epoch, big-endian)
|
||||||
@@ -76,7 +76,7 @@ data class BitchatMessage(
|
|||||||
// - Sender length: 1 byte + sender data
|
// - Sender length: 1 byte + sender data
|
||||||
// - Content length: 2 bytes + content data (or encrypted content)
|
// - Content length: 2 bytes + content data (or encrypted content)
|
||||||
// Optional fields based on flags...
|
// Optional fields based on flags...
|
||||||
|
|
||||||
var flags: UByte = 0u
|
var flags: UByte = 0u
|
||||||
if (isRelay) flags = flags or 0x01u
|
if (isRelay) flags = flags or 0x01u
|
||||||
if (isPrivate) flags = flags or 0x02u
|
if (isPrivate) flags = flags or 0x02u
|
||||||
@@ -86,23 +86,23 @@ data class BitchatMessage(
|
|||||||
if (mentions != null && mentions.isNotEmpty()) flags = flags or 0x20u
|
if (mentions != null && mentions.isNotEmpty()) flags = flags or 0x20u
|
||||||
if (channel != null) flags = flags or 0x40u
|
if (channel != null) flags = flags or 0x40u
|
||||||
if (isEncrypted) flags = flags or 0x80u
|
if (isEncrypted) flags = flags or 0x80u
|
||||||
|
|
||||||
buffer.put(flags.toByte())
|
buffer.put(flags.toByte())
|
||||||
|
|
||||||
// Timestamp (in milliseconds, 8 bytes big-endian)
|
// Timestamp (in milliseconds, 8 bytes big-endian)
|
||||||
val timestampMillis = timestamp.time
|
val timestampMillis = timestamp.time
|
||||||
buffer.putLong(timestampMillis)
|
buffer.putLong(timestampMillis)
|
||||||
|
|
||||||
// ID
|
// ID
|
||||||
val idBytes = id.toByteArray(Charsets.UTF_8)
|
val idBytes = id.toByteArray(Charsets.UTF_8)
|
||||||
buffer.put(minOf(idBytes.size, 255).toByte())
|
buffer.put(minOf(idBytes.size, 255).toByte())
|
||||||
buffer.put(idBytes.take(255).toByteArray())
|
buffer.put(idBytes.take(255).toByteArray())
|
||||||
|
|
||||||
// Sender
|
// Sender
|
||||||
val senderBytes = sender.toByteArray(Charsets.UTF_8)
|
val senderBytes = sender.toByteArray(Charsets.UTF_8)
|
||||||
buffer.put(minOf(senderBytes.size, 255).toByte())
|
buffer.put(minOf(senderBytes.size, 255).toByte())
|
||||||
buffer.put(senderBytes.take(255).toByteArray())
|
buffer.put(senderBytes.take(255).toByteArray())
|
||||||
|
|
||||||
// Content or encrypted content
|
// Content or encrypted content
|
||||||
if (isEncrypted && encryptedContent != null) {
|
if (isEncrypted && encryptedContent != null) {
|
||||||
val length = minOf(encryptedContent.size, 65535)
|
val length = minOf(encryptedContent.size, 65535)
|
||||||
@@ -114,26 +114,26 @@ data class BitchatMessage(
|
|||||||
buffer.putShort(length.toShort())
|
buffer.putShort(length.toShort())
|
||||||
buffer.put(contentBytes.take(length).toByteArray())
|
buffer.put(contentBytes.take(length).toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional fields
|
// Optional fields
|
||||||
originalSender?.let { origSender ->
|
originalSender?.let { origSender ->
|
||||||
val origBytes = origSender.toByteArray(Charsets.UTF_8)
|
val origBytes = origSender.toByteArray(Charsets.UTF_8)
|
||||||
buffer.put(minOf(origBytes.size, 255).toByte())
|
buffer.put(minOf(origBytes.size, 255).toByte())
|
||||||
buffer.put(origBytes.take(255).toByteArray())
|
buffer.put(origBytes.take(255).toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
recipientNickname?.let { recipient ->
|
recipientNickname?.let { recipient ->
|
||||||
val recipBytes = recipient.toByteArray(Charsets.UTF_8)
|
val recipBytes = recipient.toByteArray(Charsets.UTF_8)
|
||||||
buffer.put(minOf(recipBytes.size, 255).toByte())
|
buffer.put(minOf(recipBytes.size, 255).toByte())
|
||||||
buffer.put(recipBytes.take(255).toByteArray())
|
buffer.put(recipBytes.take(255).toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
senderPeerID?.let { peerID ->
|
senderPeerID?.let { peerID ->
|
||||||
val peerBytes = peerID.toByteArray(Charsets.UTF_8)
|
val peerBytes = peerID.toByteArray(Charsets.UTF_8)
|
||||||
buffer.put(minOf(peerBytes.size, 255).toByte())
|
buffer.put(minOf(peerBytes.size, 255).toByte())
|
||||||
buffer.put(peerBytes.take(255).toByteArray())
|
buffer.put(peerBytes.take(255).toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mentions array
|
// Mentions array
|
||||||
mentions?.let { mentionList ->
|
mentions?.let { mentionList ->
|
||||||
buffer.put(minOf(mentionList.size, 255).toByte())
|
buffer.put(minOf(mentionList.size, 255).toByte())
|
||||||
@@ -143,24 +143,24 @@ data class BitchatMessage(
|
|||||||
buffer.put(mentionBytes.take(255).toByteArray())
|
buffer.put(mentionBytes.take(255).toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel hashtag
|
// Channel hashtag
|
||||||
channel?.let { channelName ->
|
channel?.let { channelName ->
|
||||||
val channelBytes = channelName.toByteArray(Charsets.UTF_8)
|
val channelBytes = channelName.toByteArray(Charsets.UTF_8)
|
||||||
buffer.put(minOf(channelBytes.size, 255).toByte())
|
buffer.put(minOf(channelBytes.size, 255).toByte())
|
||||||
buffer.put(channelBytes.take(255).toByteArray())
|
buffer.put(channelBytes.take(255).toByteArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
val result = ByteArray(buffer.position())
|
val result = ByteArray(buffer.position())
|
||||||
buffer.rewind()
|
buffer.rewind()
|
||||||
buffer.get(result)
|
buffer.get(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Parse message from binary payload - exactly same logic as iOS version
|
* Parse message from binary payload - exactly same logic as iOS version
|
||||||
@@ -168,9 +168,9 @@ data class BitchatMessage(
|
|||||||
fun fromBinaryPayload(data: ByteArray): BitchatMessage? {
|
fun fromBinaryPayload(data: ByteArray): BitchatMessage? {
|
||||||
try {
|
try {
|
||||||
if (data.size < 13) return null
|
if (data.size < 13) return null
|
||||||
|
|
||||||
val buffer = ByteBuffer.wrap(data).apply { order(ByteOrder.BIG_ENDIAN) }
|
val buffer = ByteBuffer.wrap(data).apply { order(ByteOrder.BIG_ENDIAN) }
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
val flags = buffer.get().toUByte()
|
val flags = buffer.get().toUByte()
|
||||||
val isRelay = (flags and 0x01u) != 0u.toUByte()
|
val isRelay = (flags and 0x01u) != 0u.toUByte()
|
||||||
@@ -181,32 +181,32 @@ data class BitchatMessage(
|
|||||||
val hasMentions = (flags and 0x20u) != 0u.toUByte()
|
val hasMentions = (flags and 0x20u) != 0u.toUByte()
|
||||||
val hasChannel = (flags and 0x40u) != 0u.toUByte()
|
val hasChannel = (flags and 0x40u) != 0u.toUByte()
|
||||||
val isEncrypted = (flags and 0x80u) != 0u.toUByte()
|
val isEncrypted = (flags and 0x80u) != 0u.toUByte()
|
||||||
|
|
||||||
// Timestamp
|
// Timestamp
|
||||||
val timestampMillis = buffer.getLong()
|
val timestampMillis = buffer.getLong()
|
||||||
val timestamp = Date(timestampMillis)
|
val timestamp = Date(timestampMillis)
|
||||||
|
|
||||||
// ID
|
// ID
|
||||||
val idLength = buffer.get().toInt() and 0xFF
|
val idLength = buffer.get().toInt() and 0xFF
|
||||||
if (buffer.remaining() < idLength) return null
|
if (buffer.remaining() < idLength) return null
|
||||||
val idBytes = ByteArray(idLength)
|
val idBytes = ByteArray(idLength)
|
||||||
buffer.get(idBytes)
|
buffer.get(idBytes)
|
||||||
val id = String(idBytes, Charsets.UTF_8)
|
val id = String(idBytes, Charsets.UTF_8)
|
||||||
|
|
||||||
// Sender
|
// Sender
|
||||||
val senderLength = buffer.get().toInt() and 0xFF
|
val senderLength = buffer.get().toInt() and 0xFF
|
||||||
if (buffer.remaining() < senderLength) return null
|
if (buffer.remaining() < senderLength) return null
|
||||||
val senderBytes = ByteArray(senderLength)
|
val senderBytes = ByteArray(senderLength)
|
||||||
buffer.get(senderBytes)
|
buffer.get(senderBytes)
|
||||||
val sender = String(senderBytes, Charsets.UTF_8)
|
val sender = String(senderBytes, Charsets.UTF_8)
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
val contentLength = buffer.getShort().toInt() and 0xFFFF
|
val contentLength = buffer.getShort().toInt() and 0xFFFF
|
||||||
if (buffer.remaining() < contentLength) return null
|
if (buffer.remaining() < contentLength) return null
|
||||||
|
|
||||||
val content: String
|
val content: String
|
||||||
val encryptedContent: ByteArray?
|
val encryptedContent: ByteArray?
|
||||||
|
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
val encryptedBytes = ByteArray(contentLength)
|
val encryptedBytes = ByteArray(contentLength)
|
||||||
buffer.get(encryptedBytes)
|
buffer.get(encryptedBytes)
|
||||||
@@ -218,7 +218,7 @@ data class BitchatMessage(
|
|||||||
content = String(contentBytes, Charsets.UTF_8)
|
content = String(contentBytes, Charsets.UTF_8)
|
||||||
encryptedContent = null
|
encryptedContent = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional fields
|
// Optional fields
|
||||||
val originalSender = if (hasOriginalSender && buffer.hasRemaining()) {
|
val originalSender = if (hasOriginalSender && buffer.hasRemaining()) {
|
||||||
val length = buffer.get().toInt() and 0xFF
|
val length = buffer.get().toInt() and 0xFF
|
||||||
@@ -228,7 +228,7 @@ data class BitchatMessage(
|
|||||||
String(bytes, Charsets.UTF_8)
|
String(bytes, Charsets.UTF_8)
|
||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
val recipientNickname = if (hasRecipientNickname && buffer.hasRemaining()) {
|
val recipientNickname = if (hasRecipientNickname && buffer.hasRemaining()) {
|
||||||
val length = buffer.get().toInt() and 0xFF
|
val length = buffer.get().toInt() and 0xFF
|
||||||
if (buffer.remaining() >= length) {
|
if (buffer.remaining() >= length) {
|
||||||
@@ -237,7 +237,7 @@ data class BitchatMessage(
|
|||||||
String(bytes, Charsets.UTF_8)
|
String(bytes, Charsets.UTF_8)
|
||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
val senderPeerID = if (hasSenderPeerID && buffer.hasRemaining()) {
|
val senderPeerID = if (hasSenderPeerID && buffer.hasRemaining()) {
|
||||||
val length = buffer.get().toInt() and 0xFF
|
val length = buffer.get().toInt() and 0xFF
|
||||||
if (buffer.remaining() >= length) {
|
if (buffer.remaining() >= length) {
|
||||||
@@ -246,7 +246,7 @@ data class BitchatMessage(
|
|||||||
String(bytes, Charsets.UTF_8)
|
String(bytes, Charsets.UTF_8)
|
||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
// Mentions array
|
// Mentions array
|
||||||
val mentions = if (hasMentions && buffer.hasRemaining()) {
|
val mentions = if (hasMentions && buffer.hasRemaining()) {
|
||||||
val mentionCount = buffer.get().toInt() and 0xFF
|
val mentionCount = buffer.get().toInt() and 0xFF
|
||||||
@@ -263,7 +263,7 @@ data class BitchatMessage(
|
|||||||
}
|
}
|
||||||
if (mentionList.isNotEmpty()) mentionList else null
|
if (mentionList.isNotEmpty()) mentionList else null
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
val channel = if (hasChannel && buffer.hasRemaining()) {
|
val channel = if (hasChannel && buffer.hasRemaining()) {
|
||||||
val length = buffer.get().toInt() and 0xFF
|
val length = buffer.get().toInt() and 0xFF
|
||||||
@@ -273,7 +273,7 @@ data class BitchatMessage(
|
|||||||
String(bytes, Charsets.UTF_8)
|
String(bytes, Charsets.UTF_8)
|
||||||
} else null
|
} else null
|
||||||
} else null
|
} else null
|
||||||
|
|
||||||
return BitchatMessage(
|
return BitchatMessage(
|
||||||
id = id,
|
id = id,
|
||||||
sender = sender,
|
sender = sender,
|
||||||
@@ -289,19 +289,19 @@ data class BitchatMessage(
|
|||||||
encryptedContent = encryptedContent,
|
encryptedContent = encryptedContent,
|
||||||
isEncrypted = isEncrypted
|
isEncrypted = isEncrypted
|
||||||
)
|
)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
other as BitchatMessage
|
other as BitchatMessage
|
||||||
|
|
||||||
if (id != other.id) return false
|
if (id != other.id) return false
|
||||||
if (sender != other.sender) return false
|
if (sender != other.sender) return false
|
||||||
if (content != other.content) return false
|
if (content != other.content) return false
|
||||||
@@ -319,10 +319,10 @@ data class BitchatMessage(
|
|||||||
} else if (other.encryptedContent != null) return false
|
} else if (other.encryptedContent != null) return false
|
||||||
if (isEncrypted != other.isEncrypted) return false
|
if (isEncrypted != other.isEncrypted) return false
|
||||||
if (deliveryStatus != other.deliveryStatus) return false
|
if (deliveryStatus != other.deliveryStatus) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
var result = id.hashCode()
|
var result = id.hashCode()
|
||||||
result = 31 * result + sender.hashCode()
|
result = 31 * result + sender.hashCode()
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
package com.bitchat.android.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import com.bitchat.android.util.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delivery acknowledgment structure - exact same as iOS version
|
|
||||||
* Uses binary encoding for efficient protocol communication
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data class DeliveryAck(
|
|
||||||
val originalMessageID: String,
|
|
||||||
val ackID: String = UUID.randomUUID().toString(),
|
|
||||||
val recipientID: String,
|
|
||||||
val recipientNickname: String,
|
|
||||||
val timestamp: Date = Date(),
|
|
||||||
val hopCount: UByte
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
// Primary constructor for creating new acks
|
|
||||||
constructor(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UByte) : this(
|
|
||||||
originalMessageID = originalMessageID,
|
|
||||||
ackID = UUID.randomUUID().toString(),
|
|
||||||
recipientID = recipientID,
|
|
||||||
recipientNickname = recipientNickname,
|
|
||||||
timestamp = Date(),
|
|
||||||
hopCount = hopCount
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode to binary data matching iOS toBinaryData implementation
|
|
||||||
*/
|
|
||||||
fun encode(): ByteArray {
|
|
||||||
val builder = BinaryDataBuilder()
|
|
||||||
|
|
||||||
// Append original message UUID
|
|
||||||
builder.appendUUID(originalMessageID)
|
|
||||||
|
|
||||||
// Append ack ID UUID
|
|
||||||
builder.appendUUID(ackID)
|
|
||||||
|
|
||||||
// Append recipient ID as 8-byte hex string
|
|
||||||
val recipientData = ByteArray(8) { 0 }
|
|
||||||
var tempID = recipientID
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
while (tempID.length >= 2 && index < 8) {
|
|
||||||
val hexByte = tempID.substring(0, 2)
|
|
||||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
|
||||||
if (byte != null) {
|
|
||||||
recipientData[index] = byte
|
|
||||||
}
|
|
||||||
tempID = tempID.substring(2)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.buffer.addAll(recipientData.toList())
|
|
||||||
|
|
||||||
// Append hop count (UInt8)
|
|
||||||
builder.appendUInt8(hopCount)
|
|
||||||
|
|
||||||
// Append timestamp
|
|
||||||
builder.appendDate(timestamp)
|
|
||||||
|
|
||||||
// Append recipient nickname as string
|
|
||||||
builder.appendString(recipientNickname)
|
|
||||||
|
|
||||||
return builder.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Decode from binary data matching iOS fromBinaryData implementation
|
|
||||||
*/
|
|
||||||
fun decode(data: ByteArray): DeliveryAck? {
|
|
||||||
// Create defensive copy
|
|
||||||
val dataCopy = data.copyOf()
|
|
||||||
|
|
||||||
// Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname
|
|
||||||
if (dataCopy.size < 50) return null
|
|
||||||
|
|
||||||
val offset = intArrayOf(0)
|
|
||||||
|
|
||||||
val originalMessageID = dataCopy.readUUID(offset) ?: return null
|
|
||||||
val ackID = dataCopy.readUUID(offset) ?: return null
|
|
||||||
|
|
||||||
val recipientIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
|
||||||
val recipientID = recipientIDData.hexEncodedString()
|
|
||||||
|
|
||||||
val hopCount = dataCopy.readUInt8(offset) ?: return null
|
|
||||||
val timestamp = dataCopy.readDate(offset) ?: return null
|
|
||||||
val recipientNickname = dataCopy.readString(offset) ?: return null
|
|
||||||
|
|
||||||
return DeliveryAck(
|
|
||||||
originalMessageID = originalMessageID,
|
|
||||||
ackID = ackID,
|
|
||||||
recipientID = recipientID,
|
|
||||||
recipientNickname = recipientNickname,
|
|
||||||
timestamp = timestamp,
|
|
||||||
hopCount = hopCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package com.bitchat.android.model
|
||||||
|
|
||||||
|
import com.bitchat.android.protocol.MessageType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FragmentPayload - 100% iOS-compatible fragment payload structure
|
||||||
|
*
|
||||||
|
* This class handles the encoding and decoding of fragment payloads exactly
|
||||||
|
* as implemented in iOS bitchat SimplifiedBluetoothService.
|
||||||
|
*
|
||||||
|
* Fragment payload structure (matching iOS):
|
||||||
|
* - 8 bytes: Fragment ID (random bytes)
|
||||||
|
* - 2 bytes: Index (big-endian)
|
||||||
|
* - 2 bytes: Total count (big-endian)
|
||||||
|
* - 1 byte: Original message type
|
||||||
|
* - Variable: Fragment data
|
||||||
|
*
|
||||||
|
* Total header size: 13 bytes
|
||||||
|
*/
|
||||||
|
data class FragmentPayload(
|
||||||
|
val fragmentID: ByteArray, // 8 bytes - random fragment identifier
|
||||||
|
val index: Int, // Fragment index (0-based)
|
||||||
|
val total: Int, // Total number of fragments
|
||||||
|
val originalType: UByte, // Original message type before fragmentation
|
||||||
|
val data: ByteArray // Fragment data
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val HEADER_SIZE = 13
|
||||||
|
const val FRAGMENT_ID_SIZE = 8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode fragment payload from binary data
|
||||||
|
* Matches iOS implementation exactly
|
||||||
|
*/
|
||||||
|
fun decode(payloadData: ByteArray): FragmentPayload? {
|
||||||
|
if (payloadData.size < HEADER_SIZE) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract fragment ID (8 bytes)
|
||||||
|
val fragmentID = payloadData.sliceArray(0..<FRAGMENT_ID_SIZE)
|
||||||
|
|
||||||
|
// Extract index (2 bytes, big-endian) - matching iOS
|
||||||
|
val index = ((payloadData[8].toInt() and 0xFF) shl 8) or
|
||||||
|
(payloadData[9].toInt() and 0xFF)
|
||||||
|
|
||||||
|
// Extract total (2 bytes, big-endian) - matching iOS
|
||||||
|
val total = ((payloadData[10].toInt() and 0xFF) shl 8) or
|
||||||
|
(payloadData[11].toInt() and 0xFF)
|
||||||
|
|
||||||
|
// Extract original type (1 byte)
|
||||||
|
val originalType = payloadData[12].toUByte()
|
||||||
|
|
||||||
|
// Extract fragment data (remaining bytes)
|
||||||
|
val data = if (payloadData.size > HEADER_SIZE) {
|
||||||
|
payloadData.sliceArray(HEADER_SIZE..<payloadData.size)
|
||||||
|
} else {
|
||||||
|
ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FragmentPayload(fragmentID, index, total, originalType, data)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random fragment ID (8 bytes)
|
||||||
|
* Matches iOS implementation: Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||||
|
*/
|
||||||
|
fun generateFragmentID(): ByteArray {
|
||||||
|
val fragmentID = ByteArray(FRAGMENT_ID_SIZE)
|
||||||
|
kotlin.random.Random.nextBytes(fragmentID)
|
||||||
|
return fragmentID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode fragment payload to binary data
|
||||||
|
* Matches iOS implementation exactly
|
||||||
|
*/
|
||||||
|
fun encode(): ByteArray {
|
||||||
|
val payload = ByteArray(HEADER_SIZE + data.size)
|
||||||
|
|
||||||
|
// Fragment ID (8 bytes)
|
||||||
|
System.arraycopy(fragmentID, 0, payload, 0, FRAGMENT_ID_SIZE)
|
||||||
|
|
||||||
|
// Index (2 bytes, big-endian) - matching iOS withUnsafeBytes(of: UInt16(index).bigEndian)
|
||||||
|
payload[8] = ((index shr 8) and 0xFF).toByte()
|
||||||
|
payload[9] = (index and 0xFF).toByte()
|
||||||
|
|
||||||
|
// Total (2 bytes, big-endian) - matching iOS withUnsafeBytes(of: UInt16(fragments.count).bigEndian)
|
||||||
|
payload[10] = ((total shr 8) and 0xFF).toByte()
|
||||||
|
payload[11] = (total and 0xFF).toByte()
|
||||||
|
|
||||||
|
// Original type (1 byte)
|
||||||
|
payload[12] = originalType.toByte()
|
||||||
|
|
||||||
|
// Fragment data
|
||||||
|
if (data.isNotEmpty()) {
|
||||||
|
System.arraycopy(data, 0, payload, HEADER_SIZE, data.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get fragment ID as hex string for logging/debugging
|
||||||
|
*/
|
||||||
|
fun getFragmentIDString(): String {
|
||||||
|
return fragmentID.joinToString("") { "%02x".format(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate fragment payload constraints
|
||||||
|
*/
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
return fragmentID.size == FRAGMENT_ID_SIZE &&
|
||||||
|
index >= 0 &&
|
||||||
|
total > 0 &&
|
||||||
|
index < total &&
|
||||||
|
data.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as FragmentPayload
|
||||||
|
|
||||||
|
if (!fragmentID.contentEquals(other.fragmentID)) return false
|
||||||
|
if (index != other.index) return false
|
||||||
|
if (total != other.total) return false
|
||||||
|
if (originalType != other.originalType) return false
|
||||||
|
if (!data.contentEquals(other.data)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = fragmentID.contentHashCode()
|
||||||
|
result = 31 * result + index
|
||||||
|
result = 31 * result + total
|
||||||
|
result = 31 * result + originalType.hashCode()
|
||||||
|
result = 31 * result + data.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "FragmentPayload(fragmentID=${getFragmentIDString()}, index=$index, total=$total, originalType=$originalType, dataSize=${data.size})"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package com.bitchat.android.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import com.bitchat.android.util.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handshake request for pending messages - exact same as iOS version
|
|
||||||
* Uses binary encoding for efficient protocol communication
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data class HandshakeRequest(
|
|
||||||
val requestID: String,
|
|
||||||
val requesterID: String, // Who needs the handshake
|
|
||||||
val requesterNickname: String, // Nickname of requester
|
|
||||||
val targetID: String, // Who should initiate handshake
|
|
||||||
val pendingMessageCount: UByte, // Number of messages queued
|
|
||||||
val timestamp: Date
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
// Primary constructor for creating new requests
|
|
||||||
constructor(requesterID: String, requesterNickname: String, targetID: String, pendingMessageCount: UByte) : this(
|
|
||||||
requestID = UUID.randomUUID().toString(),
|
|
||||||
requesterID = requesterID,
|
|
||||||
requesterNickname = requesterNickname,
|
|
||||||
targetID = targetID,
|
|
||||||
pendingMessageCount = pendingMessageCount,
|
|
||||||
timestamp = Date()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode to binary data matching iOS toBinaryData implementation
|
|
||||||
*/
|
|
||||||
fun toBinaryData(): ByteArray {
|
|
||||||
val builder = BinaryDataBuilder()
|
|
||||||
|
|
||||||
// Append request ID UUID
|
|
||||||
builder.appendUUID(requestID)
|
|
||||||
|
|
||||||
// RequesterID as 8-byte hex string
|
|
||||||
val requesterData = ByteArray(8) { 0 }
|
|
||||||
var tempID = requesterID
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
while (tempID.length >= 2 && index < 8) {
|
|
||||||
val hexByte = tempID.substring(0, 2)
|
|
||||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
|
||||||
if (byte != null) {
|
|
||||||
requesterData[index] = byte
|
|
||||||
}
|
|
||||||
tempID = tempID.substring(2)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.buffer.addAll(requesterData.toList())
|
|
||||||
|
|
||||||
// TargetID as 8-byte hex string
|
|
||||||
val targetData = ByteArray(8) { 0 }
|
|
||||||
tempID = targetID
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
while (tempID.length >= 2 && index < 8) {
|
|
||||||
val hexByte = tempID.substring(0, 2)
|
|
||||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
|
||||||
if (byte != null) {
|
|
||||||
targetData[index] = byte
|
|
||||||
}
|
|
||||||
tempID = tempID.substring(2)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.buffer.addAll(targetData.toList())
|
|
||||||
|
|
||||||
// Append pending message count (UInt8)
|
|
||||||
builder.appendUInt8(pendingMessageCount)
|
|
||||||
|
|
||||||
// Append timestamp
|
|
||||||
builder.appendDate(timestamp)
|
|
||||||
|
|
||||||
// Append requester nickname as string
|
|
||||||
builder.appendString(requesterNickname)
|
|
||||||
|
|
||||||
return builder.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Decode from binary data matching iOS fromBinaryData implementation
|
|
||||||
*/
|
|
||||||
fun fromBinaryData(data: ByteArray): HandshakeRequest? {
|
|
||||||
// Create defensive copy
|
|
||||||
val dataCopy = data.copyOf()
|
|
||||||
|
|
||||||
// Minimum size: UUID (16) + requesterID (8) + targetID (8) + count (1) + timestamp (8) + min nickname
|
|
||||||
if (dataCopy.size < 42) return null
|
|
||||||
|
|
||||||
val offset = intArrayOf(0)
|
|
||||||
|
|
||||||
val requestID = dataCopy.readUUID(offset) ?: return null
|
|
||||||
|
|
||||||
val requesterIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
|
||||||
val requesterID = requesterIDData.hexEncodedString()
|
|
||||||
|
|
||||||
val targetIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
|
||||||
val targetID = targetIDData.hexEncodedString()
|
|
||||||
|
|
||||||
val pendingMessageCount = dataCopy.readUInt8(offset) ?: return null
|
|
||||||
val timestamp = dataCopy.readDate(offset) ?: return null
|
|
||||||
val requesterNickname = dataCopy.readString(offset) ?: return null
|
|
||||||
|
|
||||||
return HandshakeRequest(
|
|
||||||
requestID = requestID,
|
|
||||||
requesterID = requesterID,
|
|
||||||
requesterNickname = requesterNickname,
|
|
||||||
targetID = targetID,
|
|
||||||
pendingMessageCount = pendingMessageCount,
|
|
||||||
timestamp = timestamp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package com.bitchat.android.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.bitchat.android.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identity announcement structure with TLV encoding
|
||||||
|
* Compatible with iOS AnnouncementPacket TLV format
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class IdentityAnnouncement(
|
||||||
|
val nickname: String,
|
||||||
|
val publicKey: ByteArray // FIXED: Made non-nullable to match iOS and AnnouncementPacket
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TLV types matching iOS implementation
|
||||||
|
*/
|
||||||
|
private enum class TLVType(val value: UByte) {
|
||||||
|
NICKNAME(0x01u),
|
||||||
|
NOISE_PUBLIC_KEY(0x02u);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: UByte): TLVType? {
|
||||||
|
return values().find { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode to TLV binary data matching iOS implementation
|
||||||
|
*/
|
||||||
|
fun encode(): ByteArray? {
|
||||||
|
val nicknameData = nickname.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
// Check size limits
|
||||||
|
if (nicknameData.size > 255 || publicKey.size > 255) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = mutableListOf<Byte>()
|
||||||
|
|
||||||
|
// TLV for nickname
|
||||||
|
result.add(TLVType.NICKNAME.value.toByte())
|
||||||
|
result.add(nicknameData.size.toByte())
|
||||||
|
result.addAll(nicknameData.toList())
|
||||||
|
|
||||||
|
// TLV for public key
|
||||||
|
result.add(TLVType.NOISE_PUBLIC_KEY.value.toByte())
|
||||||
|
result.add(publicKey.size.toByte())
|
||||||
|
result.addAll(publicKey.toList())
|
||||||
|
|
||||||
|
return result.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Decode from TLV binary data matching iOS implementation
|
||||||
|
*/
|
||||||
|
fun decode(data: ByteArray): IdentityAnnouncement? {
|
||||||
|
// Create defensive copy
|
||||||
|
val dataCopy = data.copyOf()
|
||||||
|
|
||||||
|
var offset = 0
|
||||||
|
var nickname: String? = null
|
||||||
|
var publicKey: ByteArray? = null
|
||||||
|
|
||||||
|
while (offset + 2 <= dataCopy.size) {
|
||||||
|
// Read TLV type
|
||||||
|
val typeValue = dataCopy[offset].toUByte()
|
||||||
|
val type = TLVType.fromValue(typeValue) ?: return null
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
// Read TLV length
|
||||||
|
val length = dataCopy[offset].toUByte().toInt()
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (offset + length > dataCopy.size) return null
|
||||||
|
|
||||||
|
// Read TLV value
|
||||||
|
val value = dataCopy.sliceArray(offset until offset + length)
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
TLVType.NICKNAME -> {
|
||||||
|
nickname = String(value, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
TLVType.NOISE_PUBLIC_KEY -> {
|
||||||
|
publicKey = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both fields are required
|
||||||
|
return if (nickname != null && publicKey != null) {
|
||||||
|
IdentityAnnouncement(nickname, publicKey)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override equals and hashCode since we use ByteArray
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as IdentityAnnouncement
|
||||||
|
|
||||||
|
if (nickname != other.nickname) return false
|
||||||
|
if (!publicKey.contentEquals(other.publicKey)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = nickname.hashCode()
|
||||||
|
result = 31 * result + publicKey.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "IdentityAnnouncement(nickname='$nickname', publicKey=${publicKey.joinToString("") { "%02x".format(it) }.take(16)}...)"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
package com.bitchat.android.model
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Noise encrypted payload types and handling - 100% compatible with iOS SimplifiedBluetoothService
|
||||||
|
*
|
||||||
|
* This handles all encrypted content that goes through noiseEncrypted packets:
|
||||||
|
* - Private messages with TLV encoding
|
||||||
|
* - Delivery acknowledgments
|
||||||
|
* - Read receipts
|
||||||
|
* - Future encrypted payload types
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types of payloads embedded within noiseEncrypted messages
|
||||||
|
* Matches iOS NoisePayloadType exactly
|
||||||
|
*/
|
||||||
|
enum class NoisePayloadType(val value: UByte) {
|
||||||
|
PRIVATE_MESSAGE(0x01u), // Private chat message with TLV encoding
|
||||||
|
READ_RECEIPT(0x02u), // Message was read
|
||||||
|
DELIVERED(0x03u); // Message was delivered
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: UByte): NoisePayloadType? {
|
||||||
|
return values().find { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for creating and parsing Noise payloads
|
||||||
|
* Matches iOS NoisePayload helper exactly
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class NoisePayload(
|
||||||
|
val type: NoisePayloadType,
|
||||||
|
val data: ByteArray
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode payload with type prefix - exactly like iOS
|
||||||
|
* Format: [type_byte][payload_data]
|
||||||
|
*/
|
||||||
|
fun encode(): ByteArray {
|
||||||
|
val result = ByteArray(1 + data.size)
|
||||||
|
result[0] = type.value.toByte()
|
||||||
|
data.copyInto(result, destinationOffset = 1)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Decode payload from data - exactly like iOS
|
||||||
|
* Expects: [type_byte][payload_data]
|
||||||
|
*/
|
||||||
|
fun decode(data: ByteArray): NoisePayload? {
|
||||||
|
if (data.isEmpty()) return null
|
||||||
|
|
||||||
|
val typeValue = data[0].toUByte()
|
||||||
|
val type = NoisePayloadType.fromValue(typeValue) ?: return null
|
||||||
|
|
||||||
|
// Extract payload data (remaining bytes after type byte)
|
||||||
|
val payloadData = if (data.size > 1) {
|
||||||
|
data.copyOfRange(1, data.size)
|
||||||
|
} else {
|
||||||
|
ByteArray(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NoisePayload(type, payloadData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override equals and hashCode since we use ByteArray
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as NoisePayload
|
||||||
|
|
||||||
|
if (type != other.type) return false
|
||||||
|
if (!data.contentEquals(other.data)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = type.hashCode()
|
||||||
|
result = 31 * result + data.contentHashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private message packet with TLV encoding - matches iOS PrivateMessagePacket exactly
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class PrivateMessagePacket(
|
||||||
|
val messageID: String,
|
||||||
|
val content: String
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TLV types matching iOS implementation exactly
|
||||||
|
*/
|
||||||
|
private enum class TLVType(val value: UByte) {
|
||||||
|
MESSAGE_ID(0x00u),
|
||||||
|
CONTENT(0x01u);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromValue(value: UByte): TLVType? {
|
||||||
|
return values().find { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode to TLV binary data - exactly like iOS
|
||||||
|
* Format: [type][length][value] for each field
|
||||||
|
*/
|
||||||
|
fun encode(): ByteArray? {
|
||||||
|
val messageIDData = messageID.toByteArray(Charsets.UTF_8)
|
||||||
|
val contentData = content.toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
// Check size limits (TLV length field is 1 byte = max 255)
|
||||||
|
if (messageIDData.size > 255 || contentData.size > 255) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = mutableListOf<Byte>()
|
||||||
|
|
||||||
|
// TLV for messageID
|
||||||
|
result.add(TLVType.MESSAGE_ID.value.toByte())
|
||||||
|
result.add(messageIDData.size.toByte())
|
||||||
|
result.addAll(messageIDData.toList())
|
||||||
|
|
||||||
|
// TLV for content
|
||||||
|
result.add(TLVType.CONTENT.value.toByte())
|
||||||
|
result.add(contentData.size.toByte())
|
||||||
|
result.addAll(contentData.toList())
|
||||||
|
|
||||||
|
return result.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Decode from TLV binary data - exactly like iOS
|
||||||
|
*/
|
||||||
|
fun decode(data: ByteArray): PrivateMessagePacket? {
|
||||||
|
var offset = 0
|
||||||
|
var messageID: String? = null
|
||||||
|
var content: String? = null
|
||||||
|
|
||||||
|
while (offset + 2 <= data.size) {
|
||||||
|
// Read TLV type
|
||||||
|
val typeValue = data[offset].toUByte()
|
||||||
|
val type = TLVType.fromValue(typeValue) ?: return null
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
// Read TLV length
|
||||||
|
val length = data[offset].toUByte().toInt()
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
// Check bounds
|
||||||
|
if (offset + length > data.size) return null
|
||||||
|
|
||||||
|
// Read TLV value
|
||||||
|
val value = data.copyOfRange(offset, offset + length)
|
||||||
|
offset += length
|
||||||
|
|
||||||
|
when (type) {
|
||||||
|
TLVType.MESSAGE_ID -> {
|
||||||
|
messageID = String(value, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
TLVType.CONTENT -> {
|
||||||
|
content = String(value, Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (messageID != null && content != null) {
|
||||||
|
PrivateMessagePacket(messageID, content)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "PrivateMessagePacket(messageID='$messageID', content='${content.take(50)}${if (content.length > 50) "..." else ""}')"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
package com.bitchat.android.model
|
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import com.bitchat.android.util.*
|
|
||||||
import java.security.MessageDigest
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Noise Identity Announcement data class (compatible with iOS version)
|
|
||||||
* Enhanced identity announcement with rotation support and binary protocol
|
|
||||||
*/
|
|
||||||
data class NoiseIdentityAnnouncement(
|
|
||||||
val peerID: String, // Current ephemeral peer ID
|
|
||||||
val publicKey: ByteArray, // Noise static public key
|
|
||||||
val signingPublicKey: ByteArray, // Ed25519 signing public key
|
|
||||||
val nickname: String, // Current nickname
|
|
||||||
val timestamp: Date, // When this binding was created
|
|
||||||
val previousPeerID: String?, // Previous peer ID (for smooth transition)
|
|
||||||
val signature: ByteArray // Signature proving ownership
|
|
||||||
) {
|
|
||||||
|
|
||||||
// Computed fingerprint from public key
|
|
||||||
val fingerprint: String by lazy {
|
|
||||||
val digest = MessageDigest.getInstance("SHA-256")
|
|
||||||
val hash = digest.digest(publicKey)
|
|
||||||
hash.joinToString("") { "%02x".format(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as NoiseIdentityAnnouncement
|
|
||||||
|
|
||||||
if (peerID != other.peerID) return false
|
|
||||||
if (nickname != other.nickname) return false
|
|
||||||
if (!publicKey.contentEquals(other.publicKey)) return false
|
|
||||||
if (!signingPublicKey.contentEquals(other.signingPublicKey)) return false
|
|
||||||
if (timestamp != other.timestamp) return false
|
|
||||||
if (!signature.contentEquals(other.signature)) return false
|
|
||||||
if (previousPeerID != other.previousPeerID) return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
var result = peerID.hashCode()
|
|
||||||
result = 31 * result + nickname.hashCode()
|
|
||||||
result = 31 * result + publicKey.contentHashCode()
|
|
||||||
result = 31 * result + signingPublicKey.contentHashCode()
|
|
||||||
result = 31 * result + timestamp.hashCode()
|
|
||||||
result = 31 * result + signature.contentHashCode()
|
|
||||||
result = 31 * result + (previousPeerID?.hashCode() ?: 0)
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Binary Encoding
|
|
||||||
|
|
||||||
fun toBinaryData(): ByteArray {
|
|
||||||
val builder = BinaryDataBuilder()
|
|
||||||
|
|
||||||
// Flags byte: bit 0 = hasPreviousPeerID
|
|
||||||
var flags: UByte = 0u
|
|
||||||
if (previousPeerID != null) flags = flags or 0x01u
|
|
||||||
builder.appendUInt8(flags)
|
|
||||||
|
|
||||||
// PeerID as 8-byte hex string
|
|
||||||
val peerData = hexStringToByteArray(peerID)
|
|
||||||
// Directly append the 8 bytes without length prefix since this is a fixed field
|
|
||||||
for (byte in peerData) {
|
|
||||||
builder.buffer.add(byte)
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.appendData(publicKey)
|
|
||||||
builder.appendData(signingPublicKey)
|
|
||||||
builder.appendString(nickname)
|
|
||||||
builder.appendDate(timestamp)
|
|
||||||
|
|
||||||
if (previousPeerID != null) {
|
|
||||||
// Previous PeerID as 8-byte hex string
|
|
||||||
val prevData = hexStringToByteArray(previousPeerID)
|
|
||||||
// Directly append the 8 bytes without length prefix since this is a fixed field
|
|
||||||
for (byte in prevData) {
|
|
||||||
builder.buffer.add(byte)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.appendData(signature)
|
|
||||||
|
|
||||||
return builder.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "NoiseIdentityAnnouncement"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse Noise identity announcement from binary payload with proper iOS compatibility
|
|
||||||
*/
|
|
||||||
fun fromBinaryData(data: ByteArray): NoiseIdentityAnnouncement? {
|
|
||||||
return try {
|
|
||||||
// Create defensive copy
|
|
||||||
val dataCopy = data.copyOf()
|
|
||||||
|
|
||||||
// Minimum size check: flags(1) + peerID(8) + min data lengths
|
|
||||||
if (dataCopy.size < 20) {
|
|
||||||
Log.w(TAG, "Data too small for NoiseIdentityAnnouncement: ${dataCopy.size} bytes")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val offsetArray = intArrayOf(0)
|
|
||||||
|
|
||||||
val flags = dataCopy.readUInt8(offsetArray) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read flags")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val hasPreviousPeerID = (flags.toInt() and 0x01) != 0
|
|
||||||
|
|
||||||
// Read peerID using safe method
|
|
||||||
val peerIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read peerID bytes")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val peerID = peerIDBytes.hexEncodedString()
|
|
||||||
|
|
||||||
val publicKey = dataCopy.readData(offsetArray) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read public key")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val signingPublicKey = dataCopy.readData(offsetArray) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read signing public key")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val nickname = dataCopy.readString(offsetArray) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read nickname")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val timestamp = dataCopy.readDate(offsetArray) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read timestamp")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousPeerID: String? = null
|
|
||||||
if (hasPreviousPeerID) {
|
|
||||||
// Read previousPeerID using safe method
|
|
||||||
val prevIDBytes = dataCopy.readFixedBytes(offsetArray, 8) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read previousPeerID bytes")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
previousPeerID = prevIDBytes.hexEncodedString()
|
|
||||||
}
|
|
||||||
|
|
||||||
val signature = dataCopy.readData(offsetArray) ?: run {
|
|
||||||
Log.w(TAG, "Failed to read signature")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "Successfully parsed NoiseIdentityAnnouncement: peerID=$peerID, nickname=$nickname")
|
|
||||||
|
|
||||||
return NoiseIdentityAnnouncement(
|
|
||||||
peerID = peerID,
|
|
||||||
publicKey = publicKey,
|
|
||||||
signingPublicKey = signingPublicKey,
|
|
||||||
nickname = nickname,
|
|
||||||
timestamp = timestamp,
|
|
||||||
previousPeerID = previousPeerID,
|
|
||||||
signature = signature
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to parse Noise identity announcement: ${e.message}", e)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
|
|
||||||
*/
|
|
||||||
private fun hexStringToByteArray(hexString: String): ByteArray {
|
|
||||||
val result = ByteArray(8) { 0 } // Initialize with zeros, exactly 8 bytes
|
|
||||||
var tempID = hexString
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
while (tempID.length >= 2 && index < 8) {
|
|
||||||
val hexByte = tempID.substring(0, 2)
|
|
||||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
|
||||||
if (byte != null) {
|
|
||||||
result[index] = byte
|
|
||||||
}
|
|
||||||
tempID = tempID.substring(2)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package com.bitchat.android.model
|
|
||||||
|
|
||||||
import android.os.Parcelable
|
|
||||||
import kotlinx.parcelize.Parcelize
|
|
||||||
import com.bitchat.android.util.*
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read receipt structure - exact same as iOS version
|
|
||||||
* Uses binary encoding for efficient protocol communication
|
|
||||||
*/
|
|
||||||
@Parcelize
|
|
||||||
data class ReadReceipt(
|
|
||||||
val originalMessageID: String,
|
|
||||||
val receiptID: String = UUID.randomUUID().toString(),
|
|
||||||
val readerID: String,
|
|
||||||
val readerNickname: String,
|
|
||||||
val timestamp: Date = Date()
|
|
||||||
) : Parcelable {
|
|
||||||
|
|
||||||
// Primary constructor for creating new read receipts
|
|
||||||
constructor(originalMessageID: String, readerID: String, readerNickname: String) : this(
|
|
||||||
originalMessageID = originalMessageID,
|
|
||||||
receiptID = UUID.randomUUID().toString(),
|
|
||||||
readerID = readerID,
|
|
||||||
readerNickname = readerNickname,
|
|
||||||
timestamp = Date()
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode to binary data matching iOS toBinaryData implementation
|
|
||||||
*/
|
|
||||||
fun encode(): ByteArray {
|
|
||||||
val builder = BinaryDataBuilder()
|
|
||||||
|
|
||||||
// Append original message UUID
|
|
||||||
builder.appendUUID(originalMessageID)
|
|
||||||
|
|
||||||
// Append receipt ID UUID
|
|
||||||
builder.appendUUID(receiptID)
|
|
||||||
|
|
||||||
// Append reader ID as 8-byte hex string
|
|
||||||
val readerData = ByteArray(8) { 0 }
|
|
||||||
var tempID = readerID
|
|
||||||
var index = 0
|
|
||||||
|
|
||||||
while (tempID.length >= 2 && index < 8) {
|
|
||||||
val hexByte = tempID.substring(0, 2)
|
|
||||||
val byte = hexByte.toIntOrNull(16)?.toByte()
|
|
||||||
if (byte != null) {
|
|
||||||
readerData[index] = byte
|
|
||||||
}
|
|
||||||
tempID = tempID.substring(2)
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.buffer.addAll(readerData.toList())
|
|
||||||
|
|
||||||
// Append timestamp
|
|
||||||
builder.appendDate(timestamp)
|
|
||||||
|
|
||||||
// Append reader nickname as string
|
|
||||||
builder.appendString(readerNickname)
|
|
||||||
|
|
||||||
return builder.toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Decode from binary data matching iOS fromBinaryData implementation
|
|
||||||
*/
|
|
||||||
fun decode(data: ByteArray): ReadReceipt? {
|
|
||||||
// Create defensive copy
|
|
||||||
val dataCopy = data.copyOf()
|
|
||||||
|
|
||||||
// Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname
|
|
||||||
if (dataCopy.size < 49) return null
|
|
||||||
|
|
||||||
val offset = intArrayOf(0)
|
|
||||||
|
|
||||||
val originalMessageID = dataCopy.readUUID(offset) ?: return null
|
|
||||||
val receiptID = dataCopy.readUUID(offset) ?: return null
|
|
||||||
|
|
||||||
val readerIDData = dataCopy.readFixedBytes(offset, 8) ?: return null
|
|
||||||
val readerID = readerIDData.hexEncodedString()
|
|
||||||
|
|
||||||
val timestamp = dataCopy.readDate(offset) ?: return null
|
|
||||||
val readerNickname = dataCopy.readString(offset) ?: return null
|
|
||||||
|
|
||||||
return ReadReceipt(
|
|
||||||
originalMessageID = originalMessageID,
|
|
||||||
receiptID = receiptID,
|
|
||||||
readerID = readerID,
|
|
||||||
readerNickname = readerNickname,
|
|
||||||
timestamp = timestamp
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,32 +11,11 @@ import android.util.Log
|
|||||||
*/
|
*/
|
||||||
enum class MessageType(val value: UByte) {
|
enum class MessageType(val value: UByte) {
|
||||||
ANNOUNCE(0x01u),
|
ANNOUNCE(0x01u),
|
||||||
// 0x02 was legacy keyExchange - removed
|
MESSAGE(0x02u), // All user messages (private and broadcast)
|
||||||
LEAVE(0x03u),
|
LEAVE(0x03u),
|
||||||
MESSAGE(0x04u), // All user messages (private and broadcast)
|
NOISE_HANDSHAKE(0x10u), // Noise handshake
|
||||||
FRAGMENT_START(0x05u),
|
NOISE_ENCRYPTED(0x11u), // Noise encrypted transport message
|
||||||
FRAGMENT_CONTINUE(0x06u),
|
FRAGMENT(0x20u); // Fragmentation for large packets
|
||||||
FRAGMENT_END(0x07u),
|
|
||||||
CHANNEL_ANNOUNCE(0x08u), // Announce password-protected channel status
|
|
||||||
CHANNEL_RETENTION(0x09u), // Announce channel retention status
|
|
||||||
DELIVERY_ACK(0x0Au), // Acknowledge message received
|
|
||||||
DELIVERY_STATUS_REQUEST(0x0Bu), // Request delivery status update
|
|
||||||
READ_RECEIPT(0x0Cu), // Message has been read/viewed
|
|
||||||
|
|
||||||
// Noise Protocol messages - exact same as iOS
|
|
||||||
NOISE_HANDSHAKE_INIT(0x10u), // Noise handshake initiation
|
|
||||||
NOISE_HANDSHAKE_RESP(0x11u), // Noise handshake response
|
|
||||||
NOISE_ENCRYPTED(0x12u), // Noise encrypted transport message
|
|
||||||
NOISE_IDENTITY_ANNOUNCE(0x13u), // Announce static public key for discovery
|
|
||||||
CHANNEL_KEY_VERIFY_REQUEST(0x14u), // Request key verification for a channel
|
|
||||||
CHANNEL_KEY_VERIFY_RESPONSE(0x15u), // Response to key verification request
|
|
||||||
CHANNEL_PASSWORD_UPDATE(0x16u), // Distribute new password to channel members
|
|
||||||
CHANNEL_METADATA(0x17u), // Announce channel creator and metadata
|
|
||||||
HANDSHAKE_REQUEST(0x25u), // Request handshake initiation for pending messages
|
|
||||||
|
|
||||||
// Protocol version negotiation
|
|
||||||
VERSION_HELLO(0x20u), // Initial version announcement
|
|
||||||
VERSION_ACK(0x21u); // Version acknowledgment
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromValue(value: UByte): MessageType? {
|
fun fromValue(value: UByte): MessageType? {
|
||||||
@@ -351,6 +330,7 @@ object BinaryProtocol {
|
|||||||
)
|
)
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("BinaryProtocol", "Error decoding packet: ${e.message}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package com.bitchat.android.protocol
|
package com.bitchat.android.protocol
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.util.zip.Deflater
|
import java.util.zip.Deflater
|
||||||
import java.util.zip.Inflater
|
import java.util.zip.Inflater
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compression utilities - LZ4-like functionality using Deflater/Inflater
|
* Compression utilities - 100% iOS-compatible zlib implementation
|
||||||
* Android doesn't have native LZ4, so we use Java's built-in compression
|
* Uses the same zlib algorithm as iOS CompressionUtil.swift
|
||||||
*/
|
*/
|
||||||
object CompressionUtil {
|
object CompressionUtil {
|
||||||
private const val COMPRESSION_THRESHOLD = 100 // bytes - same as iOS
|
private const val COMPRESSION_THRESHOLD = 100 // bytes - same as iOS
|
||||||
@@ -15,14 +16,12 @@ object CompressionUtil {
|
|||||||
* Helper to check if compression is worth it - exact same logic as iOS
|
* Helper to check if compression is worth it - exact same logic as iOS
|
||||||
*/
|
*/
|
||||||
fun shouldCompress(data: ByteArray): Boolean {
|
fun shouldCompress(data: ByteArray): Boolean {
|
||||||
// TODO: COMPRESSION DOESN'T WORK WITH IOS YET
|
|
||||||
return false
|
|
||||||
// Don't compress if:
|
// Don't compress if:
|
||||||
// 1. Data is too small
|
// 1. Data is too small
|
||||||
// 2. Data appears to be already compressed (high entropy)
|
// 2. Data appears to be already compressed (high entropy)
|
||||||
if (data.size < COMPRESSION_THRESHOLD) return false
|
if (data.size < COMPRESSION_THRESHOLD) return false
|
||||||
|
|
||||||
// Simple entropy check - count unique bytes
|
// Simple entropy check - count unique bytes (exact same as iOS)
|
||||||
val byteFrequency = mutableMapOf<Byte, Int>()
|
val byteFrequency = mutableMapOf<Byte, Int>()
|
||||||
for (byte in data) {
|
for (byte in data) {
|
||||||
byteFrequency[byte] = (byteFrequency[byte] ?: 0) + 1
|
byteFrequency[byte] = (byteFrequency[byte] ?: 0) + 1
|
||||||
@@ -34,55 +33,142 @@ object CompressionUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compress data using Deflater (closest to LZ4 available on Android)
|
* Compress data using deflate algorithm - exact same as iOS
|
||||||
|
* iOS COMPRESSION_ZLIB actually produces raw deflate data (no zlib headers)
|
||||||
*/
|
*/
|
||||||
fun compress(data: ByteArray): ByteArray? {
|
fun compress(data: ByteArray): ByteArray? {
|
||||||
// Skip compression for small data
|
// Skip compression for small data
|
||||||
if (data.size < COMPRESSION_THRESHOLD) return null
|
if (data.size < COMPRESSION_THRESHOLD) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val deflater = Deflater(Deflater.BEST_SPEED) // Fast compression like LZ4
|
// Use raw deflate format (no headers) to match iOS COMPRESSION_ZLIB behavior
|
||||||
|
val deflater = Deflater(Deflater.DEFAULT_COMPRESSION, true) // true = raw deflate, no headers
|
||||||
deflater.setInput(data)
|
deflater.setInput(data)
|
||||||
deflater.finish()
|
deflater.finish()
|
||||||
|
|
||||||
val buffer = ByteArray(data.size + 16) // Some overhead space
|
val outputStream = ByteArrayOutputStream(data.size)
|
||||||
val compressedSize = deflater.deflate(buffer)
|
val buffer = ByteArray(1024)
|
||||||
|
|
||||||
|
while (!deflater.finished()) {
|
||||||
|
val count = deflater.deflate(buffer)
|
||||||
|
outputStream.write(buffer, 0, count)
|
||||||
|
}
|
||||||
deflater.end()
|
deflater.end()
|
||||||
|
|
||||||
// Only return if compression was beneficial
|
val compressedData = outputStream.toByteArray()
|
||||||
if (compressedSize > 0 && compressedSize < data.size) {
|
|
||||||
return buffer.copyOfRange(0, compressedSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
// Only return if compression was beneficial (same logic as iOS)
|
||||||
|
return if (compressedData.size > 0 && compressedData.size < data.size) {
|
||||||
|
compressedData
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decompress data using Inflater
|
* Decompress deflate compressed data - exact same as iOS
|
||||||
|
* iOS COMPRESSION_ZLIB produces raw deflate data (no headers)
|
||||||
*/
|
*/
|
||||||
fun decompress(compressedData: ByteArray, originalSize: Int): ByteArray? {
|
fun decompress(compressedData: ByteArray, originalSize: Int): ByteArray? {
|
||||||
|
// iOS COMPRESSION_ZLIB produces raw deflate format (no headers)
|
||||||
try {
|
try {
|
||||||
val inflater = Inflater()
|
val inflater = Inflater(true) // true = raw deflate, no headers
|
||||||
inflater.setInput(compressedData)
|
inflater.setInput(compressedData)
|
||||||
|
|
||||||
val result = ByteArray(originalSize)
|
val decompressedBuffer = ByteArray(originalSize)
|
||||||
val decompressedSize = inflater.inflate(result)
|
val actualSize = inflater.inflate(decompressedBuffer)
|
||||||
inflater.end()
|
inflater.end()
|
||||||
|
|
||||||
if (decompressedSize > 0) {
|
// Verify decompressed size matches expected (same validation as iOS)
|
||||||
return if (decompressedSize == originalSize) {
|
return if (actualSize == originalSize) {
|
||||||
result
|
decompressedBuffer
|
||||||
|
} else if (actualSize > 0) {
|
||||||
|
// Handle case where actual size is different
|
||||||
|
decompressedBuffer.copyOfRange(0, actualSize)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d("CompressionUtil", "Raw deflate decompression failed: ${e.message}, trying with zlib headers...")
|
||||||
|
|
||||||
|
// Fallback: try with zlib headers in case of mixed usage
|
||||||
|
try {
|
||||||
|
val inflater = Inflater(false) // false = expect zlib headers
|
||||||
|
inflater.setInput(compressedData)
|
||||||
|
|
||||||
|
val decompressedBuffer = ByteArray(originalSize)
|
||||||
|
val actualSize = inflater.inflate(decompressedBuffer)
|
||||||
|
inflater.end()
|
||||||
|
|
||||||
|
return if (actualSize == originalSize) {
|
||||||
|
decompressedBuffer
|
||||||
|
} else if (actualSize > 0) {
|
||||||
|
decompressedBuffer.copyOfRange(0, actualSize)
|
||||||
} else {
|
} else {
|
||||||
result.copyOfRange(0, decompressedSize)
|
null
|
||||||
}
|
}
|
||||||
|
} catch (fallbackException: Exception) {
|
||||||
|
Log.e("CompressionUtil", "Both raw deflate and zlib decompression failed: ${fallbackException.message}")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test function to verify deflate compression works correctly
|
||||||
|
* This can be called during app initialization to ensure compatibility
|
||||||
|
*/
|
||||||
|
fun testCompression(): Boolean {
|
||||||
|
try {
|
||||||
|
// Create test data that should compress well (repeating pattern like iOS would use)
|
||||||
|
val testMessage = "This is a test message that should compress well. ".repeat(10)
|
||||||
|
val originalData = testMessage.toByteArray()
|
||||||
|
|
||||||
|
Log.d("CompressionUtil", "Testing deflate compression with ${originalData.size} bytes")
|
||||||
|
|
||||||
|
// Test shouldCompress
|
||||||
|
val shouldCompress = shouldCompress(originalData)
|
||||||
|
Log.d("CompressionUtil", "shouldCompress() returned: $shouldCompress")
|
||||||
|
|
||||||
|
if (!shouldCompress) {
|
||||||
|
Log.e("CompressionUtil", "shouldCompress failed for test data")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
// Test compression
|
||||||
|
val compressed = compress(originalData)
|
||||||
|
if (compressed == null) {
|
||||||
|
Log.e("CompressionUtil", "Compression failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d("CompressionUtil", "Compressed ${originalData.size} bytes to ${compressed.size} bytes (${(compressed.size.toDouble() / originalData.size * 100).toInt()}%)")
|
||||||
|
|
||||||
|
// Test decompression
|
||||||
|
val decompressed = decompress(compressed, originalData.size)
|
||||||
|
if (decompressed == null) {
|
||||||
|
Log.e("CompressionUtil", "Decompression failed")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data integrity
|
||||||
|
val isIdentical = originalData.contentEquals(decompressed)
|
||||||
|
Log.d("CompressionUtil", "Data integrity check: $isIdentical")
|
||||||
|
|
||||||
|
if (!isIdentical) {
|
||||||
|
Log.e("CompressionUtil", "Decompressed data doesn't match original")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i("CompressionUtil", "✅ deflate compression test PASSED - ready for iOS compatibility")
|
||||||
|
return true
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return null
|
Log.e("CompressionUtil", "deflate compression test failed: ${e.message}")
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,219 +1,218 @@
|
|||||||
package com.bitchat.android.services
|
//package com.bitchat.android.services
|
||||||
|
//
|
||||||
import android.content.Context
|
//import android.content.Context
|
||||||
import android.content.SharedPreferences
|
//import android.content.SharedPreferences
|
||||||
import android.util.Log
|
//import android.util.Log
|
||||||
import com.bitchat.android.model.BitchatMessage
|
//import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Dispatchers
|
//import kotlinx.coroutines.withContext
|
||||||
import kotlinx.coroutines.withContext
|
//import java.io.File
|
||||||
import java.io.File
|
//import java.io.FileInputStream
|
||||||
import java.io.FileInputStream
|
//import java.io.FileOutputStream
|
||||||
import java.io.FileOutputStream
|
//import java.io.ObjectInputStream
|
||||||
import java.io.ObjectInputStream
|
//import java.io.ObjectOutputStream
|
||||||
import java.io.ObjectOutputStream
|
//import java.util.*
|
||||||
import java.util.*
|
//
|
||||||
|
///**
|
||||||
/**
|
// * Message retention service for saving channel messages locally
|
||||||
* Message retention service for saving channel messages locally
|
// * Matches iOS MessageRetentionService functionality
|
||||||
* Matches iOS MessageRetentionService functionality
|
// */
|
||||||
*/
|
//class MessageRetentionService private constructor(private val context: Context) {
|
||||||
class MessageRetentionService private constructor(private val context: Context) {
|
//
|
||||||
|
// companion object {
|
||||||
companion object {
|
// private const val TAG = "MessageRetentionService"
|
||||||
private const val TAG = "MessageRetentionService"
|
// private const val PREF_NAME = "message_retention"
|
||||||
private const val PREF_NAME = "message_retention"
|
// private const val KEY_FAVORITE_CHANNELS = "favorite_channels"
|
||||||
private const val KEY_FAVORITE_CHANNELS = "favorite_channels"
|
//
|
||||||
|
// @Volatile
|
||||||
@Volatile
|
// private var INSTANCE: MessageRetentionService? = null
|
||||||
private var INSTANCE: MessageRetentionService? = null
|
//
|
||||||
|
// fun getInstance(context: Context): MessageRetentionService {
|
||||||
fun getInstance(context: Context): MessageRetentionService {
|
// return INSTANCE ?: synchronized(this) {
|
||||||
return INSTANCE ?: synchronized(this) {
|
// INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it }
|
||||||
INSTANCE ?: MessageRetentionService(context.applicationContext).also { INSTANCE = it }
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
// private val retentionDir = File(context.filesDir, "retained_messages")
|
||||||
private val retentionDir = File(context.filesDir, "retained_messages")
|
//
|
||||||
|
// init {
|
||||||
init {
|
// if (!retentionDir.exists()) {
|
||||||
if (!retentionDir.exists()) {
|
// retentionDir.mkdirs()
|
||||||
retentionDir.mkdirs()
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// // MARK: - Channel Bookmarking (Favorites)
|
||||||
// MARK: - Channel Bookmarking (Favorites)
|
//
|
||||||
|
// fun getFavoriteChannels(): Set<String> {
|
||||||
fun getFavoriteChannels(): Set<String> {
|
// return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet()
|
||||||
return prefs.getStringSet(KEY_FAVORITE_CHANNELS, emptySet()) ?: emptySet()
|
// }
|
||||||
}
|
//
|
||||||
|
// fun toggleFavoriteChannel(channel: String): Boolean {
|
||||||
fun toggleFavoriteChannel(channel: String): Boolean {
|
// val currentFavorites = getFavoriteChannels().toMutableSet()
|
||||||
val currentFavorites = getFavoriteChannels().toMutableSet()
|
// val wasAdded = if (currentFavorites.contains(channel)) {
|
||||||
val wasAdded = if (currentFavorites.contains(channel)) {
|
// currentFavorites.remove(channel)
|
||||||
currentFavorites.remove(channel)
|
// false
|
||||||
false
|
// } else {
|
||||||
} else {
|
// currentFavorites.add(channel)
|
||||||
currentFavorites.add(channel)
|
// true
|
||||||
true
|
// }
|
||||||
}
|
//
|
||||||
|
// prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply()
|
||||||
prefs.edit().putStringSet(KEY_FAVORITE_CHANNELS, currentFavorites).apply()
|
//
|
||||||
|
// if (!wasAdded) {
|
||||||
if (!wasAdded) {
|
// // Channel removed from favorites - delete saved messages in background
|
||||||
// Channel removed from favorites - delete saved messages in background
|
// Thread {
|
||||||
Thread {
|
// try {
|
||||||
try {
|
// val channelFile = getChannelFile(channel)
|
||||||
val channelFile = getChannelFile(channel)
|
// if (channelFile.exists()) {
|
||||||
if (channelFile.exists()) {
|
// channelFile.delete()
|
||||||
channelFile.delete()
|
// Log.d(TAG, "Deleted saved messages for channel $channel")
|
||||||
Log.d(TAG, "Deleted saved messages for channel $channel")
|
// }
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.e(TAG, "Failed to delete messages for channel $channel", e)
|
||||||
Log.e(TAG, "Failed to delete messages for channel $channel", e)
|
// }
|
||||||
}
|
// }.start()
|
||||||
}.start()
|
// }
|
||||||
}
|
//
|
||||||
|
// Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}")
|
||||||
Log.d(TAG, "Channel $channel ${if (wasAdded) "bookmarked" else "unbookmarked"}")
|
// return wasAdded
|
||||||
return wasAdded
|
// }
|
||||||
}
|
//
|
||||||
|
// fun isChannelBookmarked(channel: String): Boolean {
|
||||||
fun isChannelBookmarked(channel: String): Boolean {
|
// return getFavoriteChannels().contains(channel)
|
||||||
return getFavoriteChannels().contains(channel)
|
// }
|
||||||
}
|
//
|
||||||
|
// // MARK: - Message Storage
|
||||||
// MARK: - Message Storage
|
//
|
||||||
|
// suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) {
|
||||||
suspend fun saveMessage(message: BitchatMessage, forChannel: String) = withContext(Dispatchers.IO) {
|
// if (!isChannelBookmarked(forChannel)) {
|
||||||
if (!isChannelBookmarked(forChannel)) {
|
// Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel")
|
||||||
Log.w(TAG, "Attempted to save message for non-bookmarked channel: $forChannel")
|
// return@withContext
|
||||||
return@withContext
|
// }
|
||||||
}
|
//
|
||||||
|
// try {
|
||||||
try {
|
// val channelFile = getChannelFile(forChannel)
|
||||||
val channelFile = getChannelFile(forChannel)
|
// val existingMessages = loadMessagesFromFile(channelFile).toMutableList()
|
||||||
val existingMessages = loadMessagesFromFile(channelFile).toMutableList()
|
//
|
||||||
|
// // Check if message already exists (by ID)
|
||||||
// Check if message already exists (by ID)
|
// if (existingMessages.any { it.id == message.id }) {
|
||||||
if (existingMessages.any { it.id == message.id }) {
|
// Log.d(TAG, "Message ${message.id} already saved for channel $forChannel")
|
||||||
Log.d(TAG, "Message ${message.id} already saved for channel $forChannel")
|
// return@withContext
|
||||||
return@withContext
|
// }
|
||||||
}
|
//
|
||||||
|
// // Add new message
|
||||||
// Add new message
|
// existingMessages.add(message)
|
||||||
existingMessages.add(message)
|
//
|
||||||
|
// // Sort by timestamp
|
||||||
// Sort by timestamp
|
// existingMessages.sortBy { it.timestamp }
|
||||||
existingMessages.sortBy { it.timestamp }
|
//
|
||||||
|
// // Save back to file
|
||||||
// Save back to file
|
// saveMessagesToFile(channelFile, existingMessages)
|
||||||
saveMessagesToFile(channelFile, existingMessages)
|
//
|
||||||
|
// Log.d(TAG, "Saved message ${message.id} for channel $forChannel")
|
||||||
Log.d(TAG, "Saved message ${message.id} for channel $forChannel")
|
//
|
||||||
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.e(TAG, "Failed to save message for channel $forChannel", e)
|
||||||
Log.e(TAG, "Failed to save message for channel $forChannel", e)
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// suspend fun loadMessagesForChannel(channel: String): List<BitchatMessage> = withContext(Dispatchers.IO) {
|
||||||
suspend fun loadMessagesForChannel(channel: String): List<BitchatMessage> = withContext(Dispatchers.IO) {
|
// if (!isChannelBookmarked(channel)) {
|
||||||
if (!isChannelBookmarked(channel)) {
|
// Log.d(TAG, "Channel $channel not bookmarked, returning empty list")
|
||||||
Log.d(TAG, "Channel $channel not bookmarked, returning empty list")
|
// return@withContext emptyList()
|
||||||
return@withContext emptyList()
|
// }
|
||||||
}
|
//
|
||||||
|
// try {
|
||||||
try {
|
// val channelFile = getChannelFile(channel)
|
||||||
val channelFile = getChannelFile(channel)
|
// val messages = loadMessagesFromFile(channelFile)
|
||||||
val messages = loadMessagesFromFile(channelFile)
|
// Log.d(TAG, "Loaded ${messages.size} messages for channel $channel")
|
||||||
Log.d(TAG, "Loaded ${messages.size} messages for channel $channel")
|
// return@withContext messages
|
||||||
return@withContext messages
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.e(TAG, "Failed to load messages for channel $channel", e)
|
||||||
Log.e(TAG, "Failed to load messages for channel $channel", e)
|
// return@withContext emptyList()
|
||||||
return@withContext emptyList()
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) {
|
||||||
suspend fun deleteMessagesForChannel(channel: String): Unit = withContext(Dispatchers.IO) {
|
// try {
|
||||||
try {
|
// val channelFile = getChannelFile(channel)
|
||||||
val channelFile = getChannelFile(channel)
|
// if (channelFile.exists()) {
|
||||||
if (channelFile.exists()) {
|
// channelFile.delete()
|
||||||
channelFile.delete()
|
// Log.d(TAG, "Deleted saved messages for channel $channel")
|
||||||
Log.d(TAG, "Deleted saved messages for channel $channel")
|
// }
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.e(TAG, "Failed to delete messages for channel $channel", e)
|
||||||
Log.e(TAG, "Failed to delete messages for channel $channel", e)
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) {
|
||||||
suspend fun deleteAllStoredMessages(): Unit = withContext(Dispatchers.IO) {
|
// try {
|
||||||
try {
|
// if (retentionDir.exists()) {
|
||||||
if (retentionDir.exists()) {
|
// retentionDir.listFiles()?.forEach { file ->
|
||||||
retentionDir.listFiles()?.forEach { file ->
|
// file.delete()
|
||||||
file.delete()
|
// }
|
||||||
}
|
// Log.d(TAG, "Deleted all stored messages")
|
||||||
Log.d(TAG, "Deleted all stored messages")
|
// }
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.e(TAG, "Failed to delete all stored messages", e)
|
||||||
Log.e(TAG, "Failed to delete all stored messages", e)
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// // MARK: - File Operations
|
||||||
// MARK: - File Operations
|
//
|
||||||
|
// private fun getChannelFile(channel: String): File {
|
||||||
private fun getChannelFile(channel: String): File {
|
// // Sanitize channel name for filename
|
||||||
// Sanitize channel name for filename
|
// val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_")
|
||||||
val sanitizedChannel = channel.replace("[^a-zA-Z0-9_-]".toRegex(), "_")
|
// return File(retentionDir, "channel_${sanitizedChannel}.dat")
|
||||||
return File(retentionDir, "channel_${sanitizedChannel}.dat")
|
// }
|
||||||
}
|
//
|
||||||
|
// private fun loadMessagesFromFile(file: File): List<BitchatMessage> {
|
||||||
private fun loadMessagesFromFile(file: File): List<BitchatMessage> {
|
// if (!file.exists()) {
|
||||||
if (!file.exists()) {
|
// return emptyList()
|
||||||
return emptyList()
|
// }
|
||||||
}
|
//
|
||||||
|
// return try {
|
||||||
return try {
|
// FileInputStream(file).use { fis ->
|
||||||
FileInputStream(file).use { fis ->
|
// ObjectInputStream(fis).use { ois ->
|
||||||
ObjectInputStream(fis).use { ois ->
|
// @Suppress("UNCHECKED_CAST")
|
||||||
@Suppress("UNCHECKED_CAST")
|
// ois.readObject() as List<BitchatMessage>
|
||||||
ois.readObject() as List<BitchatMessage>
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e)
|
||||||
Log.w(TAG, "Failed to load messages from ${file.name}, returning empty list", e)
|
// emptyList()
|
||||||
emptyList()
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// private fun saveMessagesToFile(file: File, messages: List<BitchatMessage>) {
|
||||||
private fun saveMessagesToFile(file: File, messages: List<BitchatMessage>) {
|
// FileOutputStream(file).use { fos ->
|
||||||
FileOutputStream(file).use { fos ->
|
// ObjectOutputStream(fos).use { oos ->
|
||||||
ObjectOutputStream(fos).use { oos ->
|
// oos.writeObject(messages)
|
||||||
oos.writeObject(messages)
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//
|
||||||
|
// // MARK: - Statistics
|
||||||
// MARK: - Statistics
|
//
|
||||||
|
// fun getBookmarkedChannelsCount(): Int {
|
||||||
fun getBookmarkedChannelsCount(): Int {
|
// return getFavoriteChannels().size
|
||||||
return getFavoriteChannels().size
|
// }
|
||||||
}
|
//
|
||||||
|
// suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) {
|
||||||
suspend fun getTotalStoredMessagesCount(): Int = withContext(Dispatchers.IO) {
|
// var totalCount = 0
|
||||||
var totalCount = 0
|
//
|
||||||
|
// try {
|
||||||
try {
|
// retentionDir.listFiles()?.forEach { file ->
|
||||||
retentionDir.listFiles()?.forEach { file ->
|
// if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) {
|
||||||
if (file.name.startsWith("channel_") && file.name.endsWith(".dat")) {
|
// val messages = loadMessagesFromFile(file)
|
||||||
val messages = loadMessagesFromFile(file)
|
// totalCount += messages.size
|
||||||
totalCount += messages.size
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// } catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
// Log.e(TAG, "Failed to count stored messages", e)
|
||||||
Log.e(TAG, "Failed to count stored messages", e)
|
// }
|
||||||
}
|
//
|
||||||
|
// totalCount
|
||||||
totalCount
|
// }
|
||||||
}
|
//}
|
||||||
}
|
|
||||||
|
|||||||
@@ -119,20 +119,7 @@ class ChannelManager(
|
|||||||
// MARK: - Channel Password and Encryption
|
// MARK: - Channel Password and Encryption
|
||||||
|
|
||||||
private fun verifyChannelPassword(channel: String, password: String): Boolean {
|
private fun verifyChannelPassword(channel: String, password: String): Boolean {
|
||||||
val key = deriveChannelKey(password, channel)
|
// TODO: REMOVE THIS - FOR TESTING ONLY
|
||||||
|
|
||||||
// Verify against existing messages if available
|
|
||||||
val existingMessages = state.getChannelMessagesValue()[channel]?.filter { it.isEncrypted }
|
|
||||||
if (!existingMessages.isNullOrEmpty()) {
|
|
||||||
val testMessage = existingMessages.first()
|
|
||||||
val decryptedContent = decryptChannelMessage(testMessage.encryptedContent ?: byteArrayOf(), channel, key)
|
|
||||||
if (decryptedContent == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
channelKeys[channel] = key
|
|
||||||
channelPasswords[channel] = password
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,48 +170,8 @@ class ChannelManager(
|
|||||||
onEncryptedPayload: (ByteArray) -> Unit,
|
onEncryptedPayload: (ByteArray) -> Unit,
|
||||||
onFallback: () -> Unit
|
onFallback: () -> Unit
|
||||||
) {
|
) {
|
||||||
val key = channelKeys[channel]
|
// TODO: REIMPLEMENT – REMOVED FOR NOW
|
||||||
if (key == null) {
|
return
|
||||||
onFallback()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
coroutineScope.launch {
|
|
||||||
try {
|
|
||||||
val contentBytes = content.toByteArray(Charsets.UTF_8)
|
|
||||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, key)
|
|
||||||
|
|
||||||
val iv = cipher.iv
|
|
||||||
val encryptedData = cipher.doFinal(contentBytes)
|
|
||||||
|
|
||||||
// Combine IV and encrypted data
|
|
||||||
val combined = ByteArray(iv.size + encryptedData.size)
|
|
||||||
System.arraycopy(iv, 0, combined, 0, iv.size)
|
|
||||||
System.arraycopy(encryptedData, 0, combined, iv.size, encryptedData.size)
|
|
||||||
|
|
||||||
val encryptedMessage = BitchatMessage(
|
|
||||||
sender = senderNickname ?: myPeerID,
|
|
||||||
content = "",
|
|
||||||
timestamp = Date(),
|
|
||||||
isRelay = false,
|
|
||||||
senderPeerID = myPeerID,
|
|
||||||
mentions = if (mentions.isNotEmpty()) mentions else null,
|
|
||||||
channel = channel,
|
|
||||||
encryptedContent = combined,
|
|
||||||
isEncrypted = true
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send encrypted message via mesh
|
|
||||||
encryptedMessage.toBinaryPayload()?.let { messageData ->
|
|
||||||
onEncryptedPayload(messageData)
|
|
||||||
} ?: onFallback()
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Fallback to unencrypted
|
|
||||||
onFallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Channel Management
|
// MARK: - Channel Management
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryAck
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
import com.bitchat.android.model.ReadReceipt
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -41,8 +40,6 @@ class ChatViewModel(
|
|||||||
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
||||||
override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
|
override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
|
||||||
override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
|
override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
|
||||||
override fun broadcastNoiseIdentityAnnouncement() = meshService.broadcastNoiseIdentityAnnouncement()
|
|
||||||
override fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte) = meshService.sendHandshakeRequest(targetPeerID, pendingCount)
|
|
||||||
override fun getMyPeerID(): String = meshService.myPeerID
|
override fun getMyPeerID(): String = meshService.myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,12 +381,12 @@ class ChatViewModel(
|
|||||||
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
meshDelegateHandler.didReceiveChannelLeave(channel, fromPeer)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) {
|
||||||
meshDelegateHandler.didReceiveDeliveryAck(ack)
|
meshDelegateHandler.didReceiveDeliveryAck(messageID, recipientPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
|
||||||
meshDelegateHandler.didReceiveReadReceipt(receipt)
|
meshDelegateHandler.didReceiveReadReceipt(messageID, recipientPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bitchat.android.ui
|
package com.bitchat.android.ui
|
||||||
|
|
||||||
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ class CommandProcessor(
|
|||||||
|
|
||||||
// MARK: - Command Processing
|
// MARK: - Command Processing
|
||||||
|
|
||||||
fun processCommand(command: String, meshService: Any, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit): Boolean {
|
fun processCommand(command: String, meshService: BluetoothMeshService, myPeerID: String, onSendMessage: (String, List<String>, String?) -> Unit): Boolean {
|
||||||
if (!command.startsWith("/")) return false
|
if (!command.startsWith("/")) return false
|
||||||
|
|
||||||
val parts = command.split(" ")
|
val parts = command.split(" ")
|
||||||
@@ -77,7 +78,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMessageCommand(parts: List<String>, meshService: Any) {
|
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
@@ -129,7 +130,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleWhoCommand(meshService: Any) {
|
private fun handleWhoCommand(meshService: BluetoothMeshService) {
|
||||||
val connectedPeers = state.getConnectedPeersValue()
|
val connectedPeers = state.getConnectedPeersValue()
|
||||||
val peerList = connectedPeers.joinToString(", ") { peerID ->
|
val peerList = connectedPeers.joinToString(", ") { peerID ->
|
||||||
// Convert peerID to nickname using the mesh service
|
// Convert peerID to nickname using the mesh service
|
||||||
@@ -214,7 +215,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleBlockCommand(parts: List<String>, meshService: Any) {
|
private fun handleBlockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
privateChatManager.blockPeerByNickname(targetName, meshService)
|
privateChatManager.blockPeerByNickname(targetName, meshService)
|
||||||
@@ -231,7 +232,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleUnblockCommand(parts: List<String>, meshService: Any) {
|
private fun handleUnblockCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
privateChatManager.unblockPeerByNickname(targetName, meshService)
|
privateChatManager.unblockPeerByNickname(targetName, meshService)
|
||||||
@@ -250,7 +251,7 @@ class CommandProcessor(
|
|||||||
parts: List<String>,
|
parts: List<String>,
|
||||||
verb: String,
|
verb: String,
|
||||||
object_: String,
|
object_: String,
|
||||||
meshService: Any,
|
meshService: BluetoothMeshService,
|
||||||
myPeerID: String,
|
myPeerID: String,
|
||||||
onSendMessage: (String, List<String>, String?) -> Unit
|
onSendMessage: (String, List<String>, String?) -> Unit
|
||||||
) {
|
) {
|
||||||
@@ -382,7 +383,7 @@ class CommandProcessor(
|
|||||||
|
|
||||||
// MARK: - Mention Autocomplete
|
// MARK: - Mention Autocomplete
|
||||||
|
|
||||||
fun updateMentionSuggestions(input: String, meshService: Any) {
|
fun updateMentionSuggestions(input: String, meshService: BluetoothMeshService) {
|
||||||
// Check if input contains @ and we're at the end of a word or at the end of input
|
// Check if input contains @ and we're at the end of a word or at the end of input
|
||||||
val atIndex = input.lastIndexOf('@')
|
val atIndex = input.lastIndexOf('@')
|
||||||
if (atIndex == -1) {
|
if (atIndex == -1) {
|
||||||
@@ -401,14 +402,8 @@ class CommandProcessor(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all connected peer nicknames
|
// Get all connected peer nicknames - now using direct access instead of reflection
|
||||||
val peerNicknames = try {
|
val peerNicknames = meshService.getPeerNicknames().values.toList()
|
||||||
val method = meshService::class.java.getDeclaredMethod("getPeerNicknames")
|
|
||||||
val peerNicknamesMap = method.invoke(meshService) as? Map<String, String>
|
|
||||||
peerNicknamesMap?.values?.toList() ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter nicknames based on the text after @
|
// Filter nicknames based on the text after @
|
||||||
val filteredNicknames = peerNicknames.filter { nickname ->
|
val filteredNicknames = peerNicknames.filter { nickname ->
|
||||||
@@ -439,50 +434,21 @@ class CommandProcessor(
|
|||||||
return "$textBeforeAt@$nickname "
|
return "$textBeforeAt@$nickname "
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Utility Functions (would access mesh service)
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: Any): String? {
|
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
||||||
return try {
|
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
||||||
val method = meshService::class.java.getDeclaredMethod("getPeerNicknames")
|
|
||||||
val peerNicknames = method.invoke(meshService) as? Map<String, String>
|
|
||||||
peerNicknames?.entries?.find { it.value == nickname }?.key
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: Any): String {
|
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
||||||
return try {
|
return meshService.getPeerNicknames()[peerID] ?: peerID
|
||||||
val method = meshService::class.java.getDeclaredMethod("getPeerNicknames")
|
|
||||||
val peerNicknames = method.invoke(meshService) as? Map<String, String>
|
|
||||||
peerNicknames?.get(peerID) ?: peerID
|
|
||||||
} catch (e: Exception) {
|
|
||||||
peerID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMyPeerID(meshService: Any): String {
|
private fun getMyPeerID(meshService: BluetoothMeshService): String {
|
||||||
return try {
|
return meshService.myPeerID
|
||||||
val field = meshService::class.java.getDeclaredField("myPeerID")
|
|
||||||
field.isAccessible = true
|
|
||||||
field.get(meshService) as? String ?: "unknown"
|
|
||||||
} catch (e: Exception) {
|
|
||||||
"unknown"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendPrivateMessageVia(meshService: Any, content: String, peerID: String, recipientNickname: String, messageId: String) {
|
private fun sendPrivateMessageVia(meshService: BluetoothMeshService, content: String, peerID: String, recipientNickname: String, messageId: String) {
|
||||||
try {
|
meshService.sendPrivateMessage(content, peerID, recipientNickname, messageId)
|
||||||
val method = meshService::class.java.getDeclaredMethod(
|
|
||||||
"sendPrivateMessage",
|
|
||||||
String::class.java,
|
|
||||||
String::class.java,
|
|
||||||
String::class.java,
|
|
||||||
String::class.java
|
|
||||||
)
|
|
||||||
method.invoke(meshService, content, peerID, recipientNickname, messageId)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ package com.bitchat.android.ui
|
|||||||
|
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
import com.bitchat.android.mesh.BluetoothMeshDelegate
|
||||||
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryAck
|
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import com.bitchat.android.model.ReadReceipt
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -22,7 +21,7 @@ class MeshDelegateHandler(
|
|||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
private val onHapticFeedback: () -> Unit,
|
private val onHapticFeedback: () -> Unit,
|
||||||
private val getMyPeerID: () -> String,
|
private val getMyPeerID: () -> String,
|
||||||
private val getMeshService: () -> Any
|
private val getMeshService: () -> BluetoothMeshService
|
||||||
) : BluetoothMeshDelegate {
|
) : BluetoothMeshDelegate {
|
||||||
|
|
||||||
override fun didReceiveMessage(message: BitchatMessage) {
|
override fun didReceiveMessage(message: BitchatMessage) {
|
||||||
@@ -103,15 +102,15 @@ class MeshDelegateHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveDeliveryAck(ack: DeliveryAck) {
|
override fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
messageManager.updateMessageDeliveryStatus(ack.originalMessageID, DeliveryStatus.Delivered(ack.recipientNickname, ack.timestamp))
|
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Delivered(recipientPeerID, Date()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun didReceiveReadReceipt(receipt: ReadReceipt) {
|
override fun didReceiveReadReceipt(messageID: String, recipientPeerID: String) {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
messageManager.updateMessageDeliveryStatus(receipt.originalMessageID, DeliveryStatus.Read(receipt.readerNickname, receipt.timestamp))
|
messageManager.updateMessageDeliveryStatus(messageID, DeliveryStatus.Read(recipientPeerID, Date()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.bitchat.android.ui
|
|||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import com.bitchat.android.mesh.PeerFingerprintManager
|
import com.bitchat.android.mesh.PeerFingerprintManager
|
||||||
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
|
||||||
@@ -13,8 +14,6 @@ import android.util.Log
|
|||||||
interface NoiseSessionDelegate {
|
interface NoiseSessionDelegate {
|
||||||
fun hasEstablishedSession(peerID: String): Boolean
|
fun hasEstablishedSession(peerID: String): Boolean
|
||||||
fun initiateHandshake(peerID: String)
|
fun initiateHandshake(peerID: String)
|
||||||
fun broadcastNoiseIdentityAnnouncement()
|
|
||||||
fun sendHandshakeRequest(targetPeerID: String, pendingCount: UByte)
|
|
||||||
fun getMyPeerID(): String
|
fun getMyPeerID(): String
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,22 +25,22 @@ class PrivateChatManager(
|
|||||||
private val state: ChatState,
|
private val state: ChatState,
|
||||||
private val messageManager: MessageManager,
|
private val messageManager: MessageManager,
|
||||||
private val dataManager: DataManager,
|
private val dataManager: DataManager,
|
||||||
private val noiseSessionDelegate: NoiseSessionDelegate? = null
|
private val noiseSessionDelegate: NoiseSessionDelegate
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "PrivateChatManager"
|
private const val TAG = "PrivateChatManager"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use centralized fingerprint management - NO LOCAL STORAGE
|
// Use centralized fingerprint management - NO LOCAL STORAGE
|
||||||
private val fingerprintManager = PeerFingerprintManager.getInstance()
|
private val fingerprintManager = PeerFingerprintManager.getInstance()
|
||||||
|
|
||||||
// Track received private messages that need read receipts
|
// Track received private messages that need read receipts
|
||||||
private val unreadReceivedMessages = mutableMapOf<String, MutableList<BitchatMessage>>()
|
private val unreadReceivedMessages = mutableMapOf<String, MutableList<BitchatMessage>>()
|
||||||
|
|
||||||
// MARK: - Private Chat Lifecycle
|
// MARK: - Private Chat Lifecycle
|
||||||
|
|
||||||
fun startPrivateChat(peerID: String, meshService: Any): Boolean {
|
fun startPrivateChat(peerID: String, meshService: BluetoothMeshService): Boolean {
|
||||||
if (isPeerBlocked(peerID)) {
|
if (isPeerBlocked(peerID)) {
|
||||||
val peerNickname = getPeerNickname(peerID, meshService)
|
val peerNickname = getPeerNickname(peerID, meshService)
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
@@ -53,28 +52,28 @@ class PrivateChatManager(
|
|||||||
messageManager.addMessage(systemMessage)
|
messageManager.addMessage(systemMessage)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establish Noise session if needed before starting the chat
|
// Establish Noise session if needed before starting the chat
|
||||||
establishNoiseSessionIfNeeded(peerID, meshService)
|
establishNoiseSessionIfNeeded(peerID, meshService)
|
||||||
|
|
||||||
state.setSelectedPrivateChatPeer(peerID)
|
state.setSelectedPrivateChatPeer(peerID)
|
||||||
|
|
||||||
// Clear unread
|
// Clear unread
|
||||||
messageManager.clearPrivateUnreadMessages(peerID)
|
messageManager.clearPrivateUnreadMessages(peerID)
|
||||||
|
|
||||||
// Initialize chat if needed
|
// Initialize chat if needed
|
||||||
messageManager.initializePrivateChat(peerID)
|
messageManager.initializePrivateChat(peerID)
|
||||||
|
|
||||||
// Send read receipts for all unread messages from this peer
|
// Send read receipts for all unread messages from this peer
|
||||||
sendReadReceiptsForPeer(peerID, meshService)
|
sendReadReceiptsForPeer(peerID, meshService)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun endPrivateChat() {
|
fun endPrivateChat() {
|
||||||
state.setSelectedPrivateChatPeer(null)
|
state.setSelectedPrivateChatPeer(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPrivateMessage(
|
fun sendPrivateMessage(
|
||||||
content: String,
|
content: String,
|
||||||
peerID: String,
|
peerID: String,
|
||||||
@@ -93,7 +92,7 @@ class PrivateChatManager(
|
|||||||
messageManager.addMessage(systemMessage)
|
messageManager.addMessage(systemMessage)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val message = BitchatMessage(
|
val message = BitchatMessage(
|
||||||
sender = senderNickname ?: myPeerID,
|
sender = senderNickname ?: myPeerID,
|
||||||
content = content,
|
content = content,
|
||||||
@@ -104,31 +103,31 @@ class PrivateChatManager(
|
|||||||
senderPeerID = myPeerID,
|
senderPeerID = myPeerID,
|
||||||
deliveryStatus = DeliveryStatus.Sending
|
deliveryStatus = DeliveryStatus.Sending
|
||||||
)
|
)
|
||||||
|
|
||||||
messageManager.addPrivateMessage(peerID, message)
|
messageManager.addPrivateMessage(peerID, message)
|
||||||
onSendMessage(content, peerID, recipientNickname ?: "", message.id)
|
onSendMessage(content, peerID, recipientNickname ?: "", message.id)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Peer Management
|
// MARK: - Peer Management
|
||||||
|
|
||||||
fun isPeerBlocked(peerID: String): Boolean {
|
fun isPeerBlocked(peerID: String): Boolean {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
return fingerprint != null && dataManager.isUserBlocked(fingerprint)
|
return fingerprint != null && dataManager.isUserBlocked(fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleFavorite(peerID: String) {
|
fun toggleFavorite(peerID: String) {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) ?: return
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) ?: return
|
||||||
|
|
||||||
Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint")
|
Log.d(TAG, "toggleFavorite called for peerID: $peerID, fingerprint: $fingerprint")
|
||||||
|
|
||||||
val wasFavorite = dataManager.isFavorite(fingerprint)
|
val wasFavorite = dataManager.isFavorite(fingerprint)
|
||||||
Log.d(TAG, "Current favorite status: $wasFavorite")
|
Log.d(TAG, "Current favorite status: $wasFavorite")
|
||||||
|
|
||||||
val currentFavorites = state.getFavoritePeersValue()
|
val currentFavorites = state.getFavoritePeersValue()
|
||||||
Log.d(TAG, "Current UI state favorites: $currentFavorites")
|
Log.d(TAG, "Current UI state favorites: $currentFavorites")
|
||||||
|
|
||||||
if (wasFavorite) {
|
if (wasFavorite) {
|
||||||
dataManager.removeFavorite(fingerprint)
|
dataManager.removeFavorite(fingerprint)
|
||||||
Log.d(TAG, "Removed from favorites: $fingerprint")
|
Log.d(TAG, "Removed from favorites: $fingerprint")
|
||||||
@@ -136,37 +135,37 @@ class PrivateChatManager(
|
|||||||
dataManager.addFavorite(fingerprint)
|
dataManager.addFavorite(fingerprint)
|
||||||
Log.d(TAG, "Added to favorites: $fingerprint")
|
Log.d(TAG, "Added to favorites: $fingerprint")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always update state to trigger UI refresh - create new set to ensure change detection
|
// Always update state to trigger UI refresh - create new set to ensure change detection
|
||||||
val newFavorites = dataManager.favoritePeers.toSet()
|
val newFavorites = dataManager.favoritePeers.toSet()
|
||||||
state.setFavoritePeers(newFavorites)
|
state.setFavoritePeers(newFavorites)
|
||||||
|
|
||||||
Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites")
|
Log.d(TAG, "Force updated favorite peers state. New favorites: $newFavorites")
|
||||||
Log.d(TAG, "All peer fingerprints: ${fingerprintManager.getAllPeerFingerprints()}")
|
Log.d(TAG, "All peer fingerprints: ${fingerprintManager.getAllPeerFingerprints()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isFavorite(peerID: String): Boolean {
|
fun isFavorite(peerID: String): Boolean {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) ?: return false
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID) ?: return false
|
||||||
val isFav = dataManager.isFavorite(fingerprint)
|
val isFav = dataManager.isFavorite(fingerprint)
|
||||||
Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav")
|
Log.d(TAG, "isFavorite check: peerID=$peerID, fingerprint=$fingerprint, result=$isFav")
|
||||||
return isFav
|
return isFav
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPeerFingerprint(peerID: String): String? {
|
fun getPeerFingerprint(peerID: String): String? {
|
||||||
return fingerprintManager.getFingerprintForPeer(peerID)
|
return fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPeerFingerprints(): Map<String, String> {
|
fun getPeerFingerprints(): Map<String, String> {
|
||||||
return fingerprintManager.getAllPeerFingerprints()
|
return fingerprintManager.getAllPeerFingerprints()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Block/Unblock Operations
|
// MARK: - Block/Unblock Operations
|
||||||
|
|
||||||
fun blockPeer(peerID: String, meshService: Any): Boolean {
|
fun blockPeer(peerID: String, meshService: BluetoothMeshService): Boolean {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
if (fingerprint != null) {
|
if (fingerprint != null) {
|
||||||
dataManager.addBlockedUser(fingerprint)
|
dataManager.addBlockedUser(fingerprint)
|
||||||
|
|
||||||
val peerNickname = getPeerNickname(peerID, meshService)
|
val peerNickname = getPeerNickname(peerID, meshService)
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
@@ -175,22 +174,22 @@ class PrivateChatManager(
|
|||||||
isRelay = false
|
isRelay = false
|
||||||
)
|
)
|
||||||
messageManager.addMessage(systemMessage)
|
messageManager.addMessage(systemMessage)
|
||||||
|
|
||||||
// End private chat if currently in one with this peer
|
// End private chat if currently in one with this peer
|
||||||
if (state.getSelectedPrivateChatPeerValue() == peerID) {
|
if (state.getSelectedPrivateChatPeerValue() == peerID) {
|
||||||
endPrivateChat()
|
endPrivateChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unblockPeer(peerID: String, meshService: Any): Boolean {
|
fun unblockPeer(peerID: String, meshService: BluetoothMeshService): Boolean {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
||||||
dataManager.removeBlockedUser(fingerprint)
|
dataManager.removeBlockedUser(fingerprint)
|
||||||
|
|
||||||
val peerNickname = getPeerNickname(peerID, meshService)
|
val peerNickname = getPeerNickname(peerID, meshService)
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
sender = "system",
|
sender = "system",
|
||||||
@@ -203,10 +202,10 @@ class PrivateChatManager(
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun blockPeerByNickname(targetName: String, meshService: Any): Boolean {
|
fun blockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
return blockPeer(peerID, meshService)
|
return blockPeer(peerID, meshService)
|
||||||
} else {
|
} else {
|
||||||
@@ -220,10 +219,10 @@ class PrivateChatManager(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unblockPeerByNickname(targetName: String, meshService: Any): Boolean {
|
fun unblockPeerByNickname(targetName: String, meshService: BluetoothMeshService): Boolean {
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
|
|
||||||
if (peerID != null) {
|
if (peerID != null) {
|
||||||
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
val fingerprint = fingerprintManager.getFingerprintForPeer(peerID)
|
||||||
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
if (fingerprint != null && dataManager.isUserBlocked(fingerprint)) {
|
||||||
@@ -249,7 +248,7 @@ class PrivateChatManager(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun listBlockedUsers(): String {
|
fun listBlockedUsers(): String {
|
||||||
val blockedCount = dataManager.blockedUsers.size
|
val blockedCount = dataManager.blockedUsers.size
|
||||||
return if (blockedCount == 0) {
|
return if (blockedCount == 0) {
|
||||||
@@ -258,136 +257,137 @@ class PrivateChatManager(
|
|||||||
"blocked users: $blockedCount fingerprints"
|
"blocked users: $blockedCount fingerprints"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Message Handling
|
// MARK: - Message Handling
|
||||||
|
|
||||||
fun handleIncomingPrivateMessage(message: BitchatMessage) {
|
fun handleIncomingPrivateMessage(message: BitchatMessage) {
|
||||||
message.senderPeerID?.let { senderPeerID ->
|
message.senderPeerID?.let { senderPeerID ->
|
||||||
if (!isPeerBlocked(senderPeerID)) {
|
if (!isPeerBlocked(senderPeerID)) {
|
||||||
// Add to private messages
|
// Add to private messages
|
||||||
messageManager.addPrivateMessage(senderPeerID, message)
|
messageManager.addPrivateMessage(senderPeerID, message)
|
||||||
|
|
||||||
// Track as unread for read receipt purposes
|
// Track as unread for read receipt purposes
|
||||||
val unreadList = unreadReceivedMessages.getOrPut(senderPeerID) { mutableListOf() }
|
val unreadList = unreadReceivedMessages.getOrPut(senderPeerID) { mutableListOf() }
|
||||||
unreadList.add(message)
|
unreadList.add(message)
|
||||||
|
|
||||||
Log.d(TAG, "Added received message ${message.id} from $senderPeerID to unread list (${unreadList.size} unread)")
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Added received message ${message.id} from $senderPeerID to unread list (${unreadList.size} unread)"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send read receipts for all unread messages from a specific peer
|
* Send read receipts for all unread messages from a specific peer
|
||||||
* Called when the user focuses on a private chat
|
* Called when the user focuses on a private chat
|
||||||
*/
|
*/
|
||||||
fun sendReadReceiptsForPeer(peerID: String, meshService: Any) {
|
fun sendReadReceiptsForPeer(peerID: String, meshService: BluetoothMeshService) {
|
||||||
val unreadList = unreadReceivedMessages[peerID]
|
val unreadList = unreadReceivedMessages[peerID]
|
||||||
if (unreadList.isNullOrEmpty()) {
|
if (unreadList.isNullOrEmpty()) {
|
||||||
Log.d(TAG, "No unread messages to send read receipts for peer $peerID")
|
Log.d(TAG, "No unread messages to send read receipts for peer $peerID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Sending read receipts for ${unreadList.size} unread messages from $peerID")
|
Log.d(TAG, "Sending read receipts for ${unreadList.size} unread messages from $peerID")
|
||||||
|
|
||||||
// Send read receipt for each unread message
|
// Send read receipt for each unread message - now using direct method call
|
||||||
unreadList.forEach { message ->
|
unreadList.forEach { message ->
|
||||||
try {
|
try {
|
||||||
val method = meshService::class.java.getDeclaredMethod("sendReadReceipt", String::class.java, String::class.java, String::class.java)
|
|
||||||
val myNickname = state.getNicknameValue() ?: "unknown"
|
val myNickname = state.getNicknameValue() ?: "unknown"
|
||||||
method.invoke(meshService, message.id, peerID, myNickname)
|
meshService.sendReadReceipt(message.id, peerID, myNickname)
|
||||||
Log.d(TAG, "Sent read receipt for message ${message.id} to $peerID")
|
Log.d(TAG, "Sent read receipt for message ${message.id} to $peerID")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to send read receipt for message ${message.id}: ${e.message}")
|
Log.w(TAG, "Failed to send read receipt for message ${message.id}: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the unread list since we've sent read receipts
|
// Clear the unread list since we've sent read receipts
|
||||||
unreadReceivedMessages.remove(peerID)
|
unreadReceivedMessages.remove(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cleanupDisconnectedPeer(peerID: String) {
|
fun cleanupDisconnectedPeer(peerID: String) {
|
||||||
// End private chat if peer disconnected
|
// End private chat if peer disconnected
|
||||||
if (state.getSelectedPrivateChatPeerValue() == peerID) {
|
if (state.getSelectedPrivateChatPeerValue() == peerID) {
|
||||||
endPrivateChat()
|
endPrivateChat()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up unread messages for disconnected peer
|
// Clean up unread messages for disconnected peer
|
||||||
unreadReceivedMessages.remove(peerID)
|
unreadReceivedMessages.remove(peerID)
|
||||||
Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID")
|
Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Noise Session Management
|
// MARK: - Noise Session Management
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Establish Noise session if needed before starting private chat
|
* Establish Noise session if needed before starting private chat
|
||||||
* Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement
|
* Uses same lexicographical logic as MessageHandler.handleNoiseIdentityAnnouncement
|
||||||
*/
|
*/
|
||||||
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: Any) {
|
private fun establishNoiseSessionIfNeeded(peerID: String, meshService: BluetoothMeshService) {
|
||||||
// If we have a clean delegate, use it; otherwise fall back to reflection for backward compatibility
|
if (noiseSessionDelegate.hasEstablishedSession(peerID)) {
|
||||||
if (noiseSessionDelegate != null) {
|
Log.d(TAG, "Noise session already established with $peerID")
|
||||||
if (noiseSessionDelegate.hasEstablishedSession(peerID)) {
|
return
|
||||||
Log.d(TAG, "Noise session already established with $peerID")
|
}
|
||||||
return
|
|
||||||
}
|
Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
|
||||||
|
|
||||||
Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
|
val myPeerID = noiseSessionDelegate.getMyPeerID()
|
||||||
|
|
||||||
val myPeerID = noiseSessionDelegate.getMyPeerID()
|
// Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
|
||||||
|
if (myPeerID < peerID) {
|
||||||
// Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
|
// We should initiate the handshake
|
||||||
if (myPeerID < peerID) {
|
Log.d(
|
||||||
// We should initiate the handshake
|
TAG,
|
||||||
Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID")
|
"Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID"
|
||||||
noiseSessionDelegate.initiateHandshake(peerID)
|
)
|
||||||
} else {
|
noiseSessionDelegate.initiateHandshake(peerID)
|
||||||
// They should initiate, we send a Noise identity announcement
|
|
||||||
Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
|
|
||||||
noiseSessionDelegate.broadcastNoiseIdentityAnnouncement()
|
|
||||||
// Also send handshake request to this peer
|
|
||||||
noiseSessionDelegate.sendHandshakeRequest(peerID, 1u) // 1 pending message (the chat we're trying to start)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to reflection-based approach for backward compatibility
|
// They should initiate, we send identity announcement through standard announce
|
||||||
establishNoiseSessionIfNeededLegacy(peerID, meshService)
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"Our peer ID lexicographically >= target peer ID, sending identity announcement to prompt handshake from $peerID"
|
||||||
|
)
|
||||||
|
meshService.sendAnnouncementToPeer(peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Legacy reflection-based implementation for backward compatibility
|
// * Legacy reflection-based implementation for backward compatibility
|
||||||
*/
|
// */
|
||||||
private fun establishNoiseSessionIfNeededLegacy(peerID: String, meshService: Any) {
|
// private fun establishNoiseSessionIfNeededLegacy(peerID: String, meshService: Any) {
|
||||||
try {
|
// try {
|
||||||
// Check if we already have an established Noise session with this peer
|
// // Check if we already have an established Noise session with this peer
|
||||||
val hasSessionMethod = meshService::class.java.getDeclaredMethod("hasEstablishedSession", String::class.java)
|
// val hasSessionMethod = meshService::class.java.getDeclaredMethod("hasEstablishedSession", String::class.java)
|
||||||
val hasSession = hasSessionMethod.invoke(meshService, peerID) as Boolean
|
// val hasSession = hasSessionMethod.invoke(meshService, peerID) as Boolean
|
||||||
|
//
|
||||||
if (hasSession) {
|
// if (hasSession) {
|
||||||
Log.d(TAG, "Noise session already established with $peerID")
|
// Log.d(TAG, "Noise session already established with $peerID")
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
|
// Log.d(TAG, "No Noise session with $peerID, determining who should initiate handshake")
|
||||||
|
//
|
||||||
// Get our peer ID from mesh service for lexicographical comparison
|
// // Get our peer ID from mesh service for lexicographical comparison
|
||||||
val myPeerIDField = meshService::class.java.getField("myPeerID")
|
// val myPeerIDField = meshService::class.java.getField("myPeerID")
|
||||||
val myPeerID = myPeerIDField.get(meshService) as String
|
// val myPeerID = myPeerIDField.get(meshService) as String
|
||||||
|
//
|
||||||
// Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
|
// // Use lexicographical comparison to decide who initiates (same logic as MessageHandler)
|
||||||
if (myPeerID < peerID) {
|
// if (myPeerID < peerID) {
|
||||||
// We should initiate the handshake
|
// // We should initiate the handshake
|
||||||
Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID")
|
// Log.d(TAG, "Our peer ID lexicographically < target peer ID, initiating Noise handshake with $peerID")
|
||||||
initiateHandshakeWithPeer(peerID, meshService)
|
// initiateHandshakeWithPeer(peerID, meshService)
|
||||||
} else {
|
// } else {
|
||||||
// They should initiate, we send a Noise identity announcement
|
// // They should initiate, we send a Noise identity announcement
|
||||||
Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
|
// Log.d(TAG, "Our peer ID lexicographically >= target peer ID, sending Noise identity announcement to prompt handshake from $peerID")
|
||||||
sendNoiseIdentityAnnouncement(meshService)
|
// sendNoiseIdentityAnnouncement(meshService)
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
} catch (e: Exception) {
|
// } catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to establish Noise session with $peerID: ${e.message}")
|
// Log.e(TAG, "Failed to establish Noise session with $peerID: ${e.message}")
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate handshake with specific peer using the existing delegate pattern
|
* Initiate handshake with specific peer using the existing delegate pattern
|
||||||
*/
|
*/
|
||||||
@@ -398,20 +398,21 @@ class PrivateChatManager(
|
|||||||
val messageHandler = meshService::class.java.getDeclaredField("messageHandler")
|
val messageHandler = meshService::class.java.getDeclaredField("messageHandler")
|
||||||
messageHandler.isAccessible = true
|
messageHandler.isAccessible = true
|
||||||
val handler = messageHandler.get(meshService)
|
val handler = messageHandler.get(meshService)
|
||||||
|
|
||||||
val delegate = handler::class.java.getDeclaredField("delegate")
|
val delegate = handler::class.java.getDeclaredField("delegate")
|
||||||
delegate.isAccessible = true
|
delegate.isAccessible = true
|
||||||
val handlerDelegate = delegate.get(handler)
|
val handlerDelegate = delegate.get(handler)
|
||||||
|
|
||||||
val method = handlerDelegate::class.java.getMethod("initiateNoiseHandshake", String::class.java)
|
val method =
|
||||||
|
handlerDelegate::class.java.getMethod("initiateNoiseHandshake", String::class.java)
|
||||||
method.invoke(handlerDelegate, peerID)
|
method.invoke(handlerDelegate, peerID)
|
||||||
|
|
||||||
Log.d(TAG, "Successfully initiated Noise handshake with $peerID using delegate pattern")
|
Log.d(TAG, "Successfully initiated Noise handshake with $peerID using delegate pattern")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}")
|
Log.e(TAG, "Failed to initiate Noise handshake with $peerID: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Noise identity announcement to prompt other peer to initiate handshake
|
* Send Noise identity announcement to prompt other peer to initiate handshake
|
||||||
* This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService
|
* This follows the same pattern as broadcastNoiseIdentityAnnouncement() in BluetoothMeshService
|
||||||
@@ -419,53 +420,40 @@ class PrivateChatManager(
|
|||||||
private fun sendNoiseIdentityAnnouncement(meshService: Any) {
|
private fun sendNoiseIdentityAnnouncement(meshService: Any) {
|
||||||
try {
|
try {
|
||||||
// Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement
|
// Call broadcastNoiseIdentityAnnouncement which sends a NoiseIdentityAnnouncement
|
||||||
val method = meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement")
|
val method =
|
||||||
|
meshService::class.java.getDeclaredMethod("broadcastNoiseIdentityAnnouncement")
|
||||||
method.invoke(meshService)
|
method.invoke(meshService)
|
||||||
Log.d(TAG, "Successfully sent Noise identity announcement")
|
Log.d(TAG, "Successfully sent Noise identity announcement")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to send Noise identity announcement: ${e.message}")
|
Log.e(TAG, "Failed to send Noise identity announcement: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: Any): String? {
|
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
||||||
// This would need to access the mesh service to get peer nicknames
|
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
||||||
// For now, we'll assume the mesh service provides a way to get this mapping
|
|
||||||
return try {
|
|
||||||
val method = meshService::class.java.getDeclaredMethod("getPeerNicknames")
|
|
||||||
val peerNicknames = method.invoke(meshService) as? Map<String, String>
|
|
||||||
peerNicknames?.entries?.find { it.value == nickname }?.key
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: Any): String {
|
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
||||||
return try {
|
return meshService.getPeerNicknames()[peerID] ?: peerID
|
||||||
val method = meshService::class.java.getDeclaredMethod("getPeerNicknames")
|
|
||||||
val peerNicknames = method.invoke(meshService) as? Map<String, String>
|
|
||||||
peerNicknames?.get(peerID) ?: peerID
|
|
||||||
} catch (e: Exception) {
|
|
||||||
peerID
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Emergency Clear
|
// MARK: - Emergency Clear
|
||||||
|
|
||||||
fun clearAllPrivateChats() {
|
fun clearAllPrivateChats() {
|
||||||
state.setSelectedPrivateChatPeer(null)
|
state.setSelectedPrivateChatPeer(null)
|
||||||
state.setUnreadPrivateMessages(emptySet())
|
state.setUnreadPrivateMessages(emptySet())
|
||||||
|
|
||||||
// Clear unread messages tracking
|
// Clear unread messages tracking
|
||||||
unreadReceivedMessages.clear()
|
unreadReceivedMessages.clear()
|
||||||
|
|
||||||
// Clear fingerprints via centralized manager (only if needed for emergency clear)
|
// Clear fingerprints via centralized manager (only if needed for emergency clear)
|
||||||
// Note: This will be handled by the parent PeerManager.clearAllPeers()
|
// Note: This will be handled by the parent PeerManager.clearAllPeers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public Getters
|
// MARK: - Public Getters
|
||||||
|
|
||||||
fun getAllPeerFingerprints(): Map<String, String> {
|
fun getAllPeerFingerprints(): Map<String, String> {
|
||||||
return fingerprintManager.getAllPeerFingerprints()
|
return fingerprintManager.getAllPeerFingerprints()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ kotlinx-coroutines = "1.10.2"
|
|||||||
# Bluetooth
|
# Bluetooth
|
||||||
nordic-ble = "2.6.1"
|
nordic-ble = "2.6.1"
|
||||||
|
|
||||||
# Compression
|
|
||||||
lz4-java = "1.8.0"
|
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
security-crypto = "1.1.0-beta01"
|
security-crypto = "1.1.0-beta01"
|
||||||
@@ -85,8 +84,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine
|
|||||||
# Bluetooth
|
# Bluetooth
|
||||||
nordic-ble = { module = "no.nordicsemi.android:ble", version.ref = "nordic-ble" }
|
nordic-ble = { module = "no.nordicsemi.android:ble", version.ref = "nordic-ble" }
|
||||||
|
|
||||||
# Compression
|
|
||||||
lz4-java = { module = "org.lz4:lz4-java", version.ref = "lz4-java" }
|
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" }
|
androidx-security-crypto = { module = "androidx.security:security-crypto", version.ref = "security-crypto" }
|
||||||
|
|||||||
Reference in New Issue
Block a user