mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 06:05:21 +00:00
Fix delivery acks and read receipts (#186)
* delivery ack and read receipt * handle deliveryack * fix uppercase id * fix read receipts too
This commit is contained in:
@@ -66,7 +66,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Wire up PacketProcessor reference for recursive handling in MessageHandler
|
// Wire up PacketProcessor reference for recursive handling in MessageHandler
|
||||||
messageHandler.packetProcessor = packetProcessor
|
messageHandler.packetProcessor = packetProcessor
|
||||||
sendPeriodicBroadcastAnnounce()
|
sendPeriodicBroadcastAnnounce()
|
||||||
startPeriodicDebugLogging()
|
// startPeriodicDebugLogging()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,13 +89,13 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send broadcast announcement every 10 seconds
|
* Send broadcast announcement every 30 seconds
|
||||||
*/
|
*/
|
||||||
private fun sendPeriodicBroadcastAnnounce() {
|
private fun sendPeriodicBroadcastAnnounce() {
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
while (isActive) {
|
while (isActive) {
|
||||||
try {
|
try {
|
||||||
delay(10000) // 10 seconds
|
delay(30000) // 30 seconds
|
||||||
sendBroadcastAnnounce()
|
sendBroadcastAnnounce()
|
||||||
} 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}")
|
||||||
@@ -356,9 +356,9 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
return fragmentManager.handleFragment(packet)
|
return fragmentManager.handleFragment(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleDeliveryAck(routed: RoutedPacket) {
|
// override fun handleDeliveryAck(routed: RoutedPacket) {
|
||||||
serviceScope.launch { messageHandler.handleDeliveryAck(routed) }
|
// serviceScope.launch { messageHandler.handleDeliveryAck(routed) }
|
||||||
}
|
// }
|
||||||
|
|
||||||
override fun handleReadReceipt(routed: RoutedPacket) {
|
override fun handleReadReceipt(routed: RoutedPacket) {
|
||||||
serviceScope.launch { messageHandler.handleReadReceipt(routed) }
|
serviceScope.launch { messageHandler.handleReadReceipt(routed) }
|
||||||
@@ -552,7 +552,7 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
originalMessageID = message.id,
|
originalMessageID = message.id,
|
||||||
recipientID = myPeerID,
|
recipientID = myPeerID,
|
||||||
recipientNickname = nickname,
|
recipientNickname = nickname,
|
||||||
hopCount = 0u // Will be calculated during relay
|
hopCount = 0u.toUByte() // Will be calculated during relay
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -586,6 +586,47 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send read receipt for a received private message
|
||||||
|
*/
|
||||||
|
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||||
|
serviceScope.launch {
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Sending read receipt for message $messageID to $recipientPeerID")
|
||||||
|
|
||||||
|
// Create the read receipt
|
||||||
|
val receipt = ReadReceipt(
|
||||||
|
originalMessageID = messageID,
|
||||||
|
readerID = myPeerID,
|
||||||
|
readerNickname = readerNickname
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encode the receipt
|
||||||
|
val receiptData = receipt.encode()
|
||||||
|
|
||||||
|
// Create inner read receipt packet
|
||||||
|
val innerPacket = BitchatPacket(
|
||||||
|
version = 1u,
|
||||||
|
type = MessageType.READ_RECEIPT.value,
|
||||||
|
senderID = hexStringToByteArray(myPeerID),
|
||||||
|
recipientID = hexStringToByteArray(recipientPeerID),
|
||||||
|
timestamp = System.currentTimeMillis().toULong(),
|
||||||
|
payload = receiptData,
|
||||||
|
signature = null,
|
||||||
|
ttl = 3u
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt the entire inner packet and send as NOISE_ENCRYPTED
|
||||||
|
encryptAndBroadcastNoisePacket(innerPacket, recipientPeerID)
|
||||||
|
|
||||||
|
Log.d(TAG, "Sent read receipt for message $messageID to $recipientPeerID")
|
||||||
|
|
||||||
|
} 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
|
* 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
|
* This is the correct protocol implementation - encrypt the entire packet, not just the payload
|
||||||
@@ -644,14 +685,6 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
payload = nickname.toByteArray()
|
payload = nickname.toByteArray()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Send multiple times for reliability
|
|
||||||
delay(Random.nextLong(0, 500))
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
|
||||||
|
|
||||||
delay(500 + Random.nextLong(0, 500))
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
|
||||||
|
|
||||||
delay(1000 + Random.nextLong(0, 500))
|
|
||||||
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
connectionManager.broadcastPacket(RoutedPacket(announcePacket))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,19 +58,12 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
|
|
||||||
// Check if this is a delivery ACK with the new format
|
// Check if this is a delivery ACK with the new format
|
||||||
if (typeMarker == MessageType.DELIVERY_ACK.value) {
|
if (typeMarker == MessageType.DELIVERY_ACK.value) {
|
||||||
// Extract the ACK JSON data (skip the type marker)
|
handleDeliveryAck(decryptedData)
|
||||||
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
|
|
||||||
|
|
||||||
// Decode the delivery ACK
|
|
||||||
val ack = DeliveryAck.decode(ackData)
|
|
||||||
if (ack != null) {
|
|
||||||
delegate?.onDeliveryAckReceived(ack)
|
|
||||||
Log.d(TAG, "Processed delivery ACK from $peerID")
|
Log.d(TAG, "Processed delivery ACK from $peerID")
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for read receipt with type marker
|
// Check for read receipt with type marker
|
||||||
|
// NOTE: THIS DOESN'T WORK WITH IOS, IT SENDS AN INNER PACKET INSTEAD
|
||||||
if (typeMarker == MessageType.READ_RECEIPT.value) {
|
if (typeMarker == MessageType.READ_RECEIPT.value) {
|
||||||
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
|
val receiptData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||||
val receipt = ReadReceipt.decode(receiptData)
|
val receipt = ReadReceipt.decode(receiptData)
|
||||||
@@ -268,7 +261,7 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
delegate?.onMessageReceived(message)
|
delegate?.onMessageReceived(message)
|
||||||
|
|
||||||
// Send delivery ACK
|
// Send delivery ACK
|
||||||
// delegate?.sendDeliveryAck(message, peerID)
|
delegate?.sendDeliveryAck(message, peerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -302,23 +295,13 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
/**
|
/**
|
||||||
* Handle delivery acknowledgment
|
* Handle delivery acknowledgment
|
||||||
*/
|
*/
|
||||||
suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
suspend fun handleDeliveryAck(decryptedData: ByteArray) {
|
||||||
val packet = routed.packet
|
val ackData = decryptedData.sliceArray(1 until decryptedData.size)
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val ack = DeliveryAck.decode(ackData)
|
||||||
if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) {
|
|
||||||
try {
|
|
||||||
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
|
||||||
if (decryptedData != null) {
|
|
||||||
val ack = DeliveryAck.decode(decryptedData)
|
|
||||||
if (ack != null) {
|
if (ack != null) {
|
||||||
delegate?.onDeliveryAckReceived(ack)
|
delegate?.onDeliveryAckReceived(ack)
|
||||||
}
|
}
|
||||||
}
|
return
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to decrypt delivery ACK: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delivery ACK relay is now handled by centralized PacketRelayManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,20 +310,11 @@ class MessageHandler(private val myPeerID: String) {
|
|||||||
suspend fun handleReadReceipt(routed: RoutedPacket) {
|
suspend fun handleReadReceipt(routed: RoutedPacket) {
|
||||||
val packet = routed.packet
|
val packet = routed.packet
|
||||||
val peerID = routed.peerID ?: "unknown"
|
val peerID = routed.peerID ?: "unknown"
|
||||||
if (packet.recipientID != null && String(packet.recipientID).replace("\u0000", "") == myPeerID) {
|
val receipt = ReadReceipt.decode(routed.packet.payload)
|
||||||
try {
|
|
||||||
val decryptedData = delegate?.decryptFromPeer(packet.payload, peerID)
|
|
||||||
if (decryptedData != null) {
|
|
||||||
val receipt = ReadReceipt.decode(decryptedData)
|
|
||||||
if (receipt != null) {
|
if (receipt != null) {
|
||||||
delegate?.onReadReceiptReceived(receipt)
|
delegate?.onReadReceiptReceived(receipt)
|
||||||
}
|
}
|
||||||
}
|
return
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to decrypt read receipt: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Read receipt relay is now handled by centralized PacketRelayManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
|
MessageType.NOISE_HANDSHAKE_INIT -> handleNoiseHandshake(routed, 1)
|
||||||
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
|
MessageType.NOISE_HANDSHAKE_RESP -> handleNoiseHandshake(routed, 2)
|
||||||
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
||||||
MessageType.DELIVERY_ACK -> handleDeliveryAck(routed)
|
//MessageType.DELIVERY_ACK -> handleDeliveryAck(routed) // custom packet type...
|
||||||
MessageType.READ_RECEIPT -> handleReadReceipt(routed)
|
MessageType.READ_RECEIPT -> handleReadReceipt(routed)
|
||||||
else -> {
|
else -> {
|
||||||
validPacket = false
|
validPacket = false
|
||||||
@@ -234,11 +234,11 @@ class PacketProcessor(private val myPeerID: String) {
|
|||||||
/**
|
/**
|
||||||
* Handle delivery acknowledgment
|
* Handle delivery acknowledgment
|
||||||
*/
|
*/
|
||||||
private suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
// private suspend fun handleDeliveryAck(routed: RoutedPacket) {
|
||||||
val peerID = routed.peerID ?: "unknown"
|
// val peerID = routed.peerID ?: "unknown"
|
||||||
Log.d(TAG, "Processing delivery ACK from ${formatPeerForLog(peerID)}")
|
// Log.d(TAG, "Processing delivery ACK from ${formatPeerForLog(peerID)}")
|
||||||
delegate?.handleDeliveryAck(routed)
|
// delegate?.handleDeliveryAck(routed)
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle read receipt
|
* Handle read receipt
|
||||||
@@ -313,7 +313,7 @@ interface PacketProcessorDelegate {
|
|||||||
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 handleDeliveryAck(routed: RoutedPacket)
|
||||||
fun handleReadReceipt(routed: RoutedPacket)
|
fun handleReadReceipt(routed: RoutedPacket)
|
||||||
|
|
||||||
// Communication
|
// Communication
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ sealed class DeliveryStatus : Parcelable {
|
|||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class BitchatMessage(
|
data class BitchatMessage(
|
||||||
val id: String = UUID.randomUUID().toString(),
|
val id: String = UUID.randomUUID().toString().uppercase(),
|
||||||
val sender: String,
|
val sender: String,
|
||||||
val content: String,
|
val content: String,
|
||||||
val timestamp: Date,
|
val timestamp: Date,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package com.bitchat.android.model
|
package com.bitchat.android.model
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.bitchat.android.util.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delivery acknowledgment structure - exact same as iOS version
|
* Delivery acknowledgment structure - exact same as iOS version
|
||||||
|
* Uses binary encoding for efficient protocol communication
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class DeliveryAck(
|
data class DeliveryAck(
|
||||||
@@ -15,29 +16,91 @@ data class DeliveryAck(
|
|||||||
val recipientID: String,
|
val recipientID: String,
|
||||||
val recipientNickname: String,
|
val recipientNickname: String,
|
||||||
val timestamp: Date = Date(),
|
val timestamp: Date = Date(),
|
||||||
val hopCount: UInt
|
val hopCount: UByte
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
private val gson = GsonBuilder()
|
// Primary constructor for creating new acks
|
||||||
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
|
constructor(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UByte) : this(
|
||||||
.create()
|
originalMessageID = originalMessageID,
|
||||||
|
ackID = UUID.randomUUID().toString(),
|
||||||
|
recipientID = recipientID,
|
||||||
|
recipientNickname = recipientNickname,
|
||||||
|
timestamp = Date(),
|
||||||
|
hopCount = hopCount
|
||||||
|
)
|
||||||
|
|
||||||
fun encode(): ByteArray? {
|
/**
|
||||||
return try {
|
* Encode to binary data matching iOS toBinaryData implementation
|
||||||
gson.toJson(this).toByteArray(Charsets.UTF_8)
|
*/
|
||||||
} catch (e: Exception) {
|
fun encode(): ByteArray {
|
||||||
null
|
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 {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Decode from binary data matching iOS fromBinaryData implementation
|
||||||
|
*/
|
||||||
fun decode(data: ByteArray): DeliveryAck? {
|
fun decode(data: ByteArray): DeliveryAck? {
|
||||||
return try {
|
// Create defensive copy
|
||||||
val json = String(data, Charsets.UTF_8)
|
val dataCopy = data.copyOf()
|
||||||
com.google.gson.Gson().fromJson(json, DeliveryAck::class.java)
|
|
||||||
} catch (e: Exception) {
|
// Minimum size: 2 UUIDs (32) + recipientID (8) + hopCount (1) + timestamp (8) + min nickname
|
||||||
null
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ package com.bitchat.android.model
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import com.bitchat.android.util.*
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read receipt structure - exact same as iOS version
|
* Read receipt structure - exact same as iOS version
|
||||||
|
* Uses binary encoding for efficient protocol communication
|
||||||
*/
|
*/
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ReadReceipt(
|
data class ReadReceipt(
|
||||||
@@ -16,22 +18,82 @@ data class ReadReceipt(
|
|||||||
val timestamp: Date = Date()
|
val timestamp: Date = Date()
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
|
|
||||||
fun encode(): ByteArray? {
|
// Primary constructor for creating new read receipts
|
||||||
return try {
|
constructor(originalMessageID: String, readerID: String, readerNickname: String) : this(
|
||||||
com.google.gson.Gson().toJson(this).toByteArray(Charsets.UTF_8)
|
originalMessageID = originalMessageID,
|
||||||
} catch (e: Exception) {
|
receiptID = UUID.randomUUID().toString(),
|
||||||
null
|
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 {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Decode from binary data matching iOS fromBinaryData implementation
|
||||||
|
*/
|
||||||
fun decode(data: ByteArray): ReadReceipt? {
|
fun decode(data: ByteArray): ReadReceipt? {
|
||||||
return try {
|
// Create defensive copy
|
||||||
val json = String(data, Charsets.UTF_8)
|
val dataCopy = data.copyOf()
|
||||||
com.google.gson.Gson().fromJson(json, ReadReceipt::class.java)
|
|
||||||
} catch (e: Exception) {
|
// Minimum size: 2 UUIDs (32) + readerID (8) + timestamp (8) + min nickname
|
||||||
null
|
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import android.os.Parcelable
|
|||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.util.*
|
import android.util.Log
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message types - exact same as iOS version with Noise Protocol support
|
* Message types - exact same as iOS version with Noise Protocol support
|
||||||
@@ -262,6 +262,7 @@ object BinaryProtocol {
|
|||||||
return paddedData
|
return paddedData
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Log.e("BinaryProtocol", "Error encoding packet type ${packet.type}: ${e.message}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class 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
|
||||||
|
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: Any): Boolean {
|
||||||
@@ -46,6 +49,9 @@ class PrivateChatManager(
|
|||||||
// Initialize chat if needed
|
// Initialize chat if needed
|
||||||
messageManager.initializePrivateChat(peerID)
|
messageManager.initializePrivateChat(peerID)
|
||||||
|
|
||||||
|
// Send read receipts for all unread messages from this peer
|
||||||
|
sendReadReceiptsForPeer(peerID, meshService)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,16 +248,56 @@ class PrivateChatManager(
|
|||||||
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
|
||||||
messageManager.addPrivateMessage(senderPeerID, message)
|
messageManager.addPrivateMessage(senderPeerID, message)
|
||||||
|
|
||||||
|
// Track as unread for read receipt purposes
|
||||||
|
val unreadList = unreadReceivedMessages.getOrPut(senderPeerID) { mutableListOf() }
|
||||||
|
unreadList.add(message)
|
||||||
|
|
||||||
|
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
|
||||||
|
* Called when the user focuses on a private chat
|
||||||
|
*/
|
||||||
|
fun sendReadReceiptsForPeer(peerID: String, meshService: Any) {
|
||||||
|
val unreadList = unreadReceivedMessages[peerID]
|
||||||
|
if (unreadList.isNullOrEmpty()) {
|
||||||
|
Log.d(TAG, "No unread messages to send read receipts for peer $peerID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Sending read receipts for ${unreadList.size} unread messages from $peerID")
|
||||||
|
|
||||||
|
// Send read receipt for each unread message
|
||||||
|
unreadList.forEach { message ->
|
||||||
|
try {
|
||||||
|
val method = meshService::class.java.getDeclaredMethod("sendReadReceipt", String::class.java, String::class.java, String::class.java)
|
||||||
|
val myNickname = state.getNicknameValue() ?: "unknown"
|
||||||
|
method.invoke(meshService, message.id, peerID, myNickname)
|
||||||
|
Log.d(TAG, "Sent read receipt for message ${message.id} to $peerID")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "Failed to send read receipt for message ${message.id}: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the unread list since we've sent read receipts
|
||||||
|
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
|
||||||
|
unreadReceivedMessages.remove(peerID)
|
||||||
|
Log.d(TAG, "Cleaned up unread messages for disconnected peer $peerID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Utility Functions
|
// MARK: - Utility Functions
|
||||||
@@ -284,6 +330,9 @@ class PrivateChatManager(
|
|||||||
state.setSelectedPrivateChatPeer(null)
|
state.setSelectedPrivateChatPeer(null)
|
||||||
state.setUnreadPrivateMessages(emptySet())
|
state.setUnreadPrivateMessages(emptySet())
|
||||||
|
|
||||||
|
// Clear unread messages tracking
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user