mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 01:45:22 +00:00
refactor mesh core
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,716 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.BitchatFilePacket
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.model.NoisePayload
|
||||
import com.bitchat.android.model.NoisePayloadType
|
||||
import com.bitchat.android.model.PrivateMessagePacket
|
||||
import com.bitchat.android.model.RequestSyncPacket
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.service.TransportBridgeService
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* Shared mesh coordinator that wires all mesh-layer components and provides common APIs
|
||||
* for send/receive operations across transports.
|
||||
*/
|
||||
class MeshCore(
|
||||
private val context: Context,
|
||||
private val scope: CoroutineScope,
|
||||
private val transport: MeshTransport,
|
||||
private val encryptionService: EncryptionService,
|
||||
val myPeerID: String,
|
||||
private val maxTtl: UByte,
|
||||
sharedGossipManager: GossipSyncManager?,
|
||||
gossipConfigProvider: GossipSyncManager.ConfigProvider,
|
||||
private val hooks: Hooks = Hooks()
|
||||
) {
|
||||
data class Hooks(
|
||||
val onMessageReceived: ((BitchatMessage) -> Unit)? = null,
|
||||
val onPeerIdBindingUpdated: ((String, String, ByteArray, String?) -> Unit)? = null,
|
||||
val onAnnounceProcessed: ((RoutedPacket, Boolean) -> Unit)? = null,
|
||||
val readReceiptInterceptor: ((String, String) -> Boolean)? = null,
|
||||
val onReadReceiptSent: ((String) -> Unit)? = null,
|
||||
val announcementNicknameProvider: (() -> String?)? = null,
|
||||
val leavePayloadProvider: (() -> ByteArray)? = null
|
||||
)
|
||||
|
||||
private val peerManager = PeerManager()
|
||||
val fragmentManager = FragmentManager()
|
||||
private val securityManager = SecurityManager(encryptionService, myPeerID)
|
||||
private val storeForwardManager = StoreForwardManager()
|
||||
private val messageHandler = MessageHandler(myPeerID, context.applicationContext)
|
||||
private val packetProcessor = PacketProcessor(myPeerID)
|
||||
|
||||
val gossipSyncManager: GossipSyncManager =
|
||||
sharedGossipManager ?: GossipSyncManager(myPeerID = myPeerID, scope = scope, configProvider = gossipConfigProvider)
|
||||
|
||||
var delegate: MeshDelegate? = null
|
||||
|
||||
private var announceJob: Job? = null
|
||||
private var isActive = false
|
||||
|
||||
init {
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
setupDelegates()
|
||||
|
||||
if (sharedGossipManager == null) {
|
||||
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
dispatchGlobal(RoutedPacket(packet))
|
||||
}
|
||||
|
||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||
transport.sendPacketToPeer(peerID, packet)
|
||||
}
|
||||
|
||||
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||
return signPacketBeforeBroadcast(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startCore() {
|
||||
if (isActive) return
|
||||
isActive = true
|
||||
startPeriodicBroadcastAnnounce()
|
||||
gossipSyncManager.start()
|
||||
}
|
||||
|
||||
fun stopCore() {
|
||||
if (!isActive) return
|
||||
isActive = false
|
||||
announceJob?.cancel()
|
||||
announceJob = null
|
||||
gossipSyncManager.stop()
|
||||
}
|
||||
|
||||
fun shutdown() {
|
||||
peerManager.shutdown()
|
||||
fragmentManager.shutdown()
|
||||
securityManager.shutdown()
|
||||
storeForwardManager.shutdown()
|
||||
messageHandler.shutdown()
|
||||
packetProcessor.shutdown()
|
||||
}
|
||||
|
||||
fun processIncoming(packet: BitchatPacket, peerID: String?, relayAddress: String?) {
|
||||
packetProcessor.processPacket(RoutedPacket(packet, peerID, relayAddress))
|
||||
}
|
||||
|
||||
fun sendFromBridge(packet: RoutedPacket) {
|
||||
transport.broadcastPacket(packet)
|
||||
}
|
||||
|
||||
private fun dispatchGlobal(routed: RoutedPacket) {
|
||||
transport.broadcastPacket(routed)
|
||||
TransportBridgeService.broadcast(transport.id, routed)
|
||||
}
|
||||
|
||||
private fun startPeriodicBroadcastAnnounce() {
|
||||
announceJob?.cancel()
|
||||
announceJob = scope.launch {
|
||||
while (isActive) {
|
||||
try {
|
||||
delay(30_000)
|
||||
sendBroadcastAnnounce()
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDelegates() {
|
||||
peerManager.delegate = object : PeerManagerDelegate {
|
||||
override fun onPeerListUpdated(peerIDs: List<String>) {
|
||||
delegate?.didUpdatePeerList(peerIDs)
|
||||
}
|
||||
|
||||
override fun onPeerRemoved(peerID: String) {
|
||||
try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { }
|
||||
try { encryptionService.removePeer(peerID) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
securityManager.delegate = object : SecurityManagerDelegate {
|
||||
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) {
|
||||
scope.launch {
|
||||
delay(100)
|
||||
sendAnnouncementToPeer(peerID)
|
||||
delay(1000)
|
||||
storeForwardManager.sendCachedMessages(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
override fun sendHandshakeResponse(peerID: String, response: ByteArray) {
|
||||
val responsePacket = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = MeshPacketUtils.hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = response,
|
||||
ttl = maxTtl
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(responsePacket)))
|
||||
}
|
||||
|
||||
override fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID)
|
||||
}
|
||||
|
||||
storeForwardManager.delegate = object : StoreForwardManagerDelegate {
|
||||
override fun isFavorite(peerID: String): Boolean {
|
||||
return delegate?.isFavorite(peerID) ?: false
|
||||
}
|
||||
|
||||
override fun isPeerOnline(peerID: String): Boolean {
|
||||
return peerManager.isPeerActive(peerID)
|
||||
}
|
||||
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
dispatchGlobal(RoutedPacket(packet))
|
||||
}
|
||||
}
|
||||
|
||||
messageHandler.delegate = object : MessageHandlerDelegate {
|
||||
override fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
|
||||
return peerManager.addOrUpdatePeer(peerID, nickname)
|
||||
}
|
||||
|
||||
override fun removePeer(peerID: String) {
|
||||
peerManager.removePeer(peerID)
|
||||
}
|
||||
|
||||
override fun updatePeerNickname(peerID: String, nickname: String) {
|
||||
peerManager.addOrUpdatePeer(peerID, nickname)
|
||||
}
|
||||
|
||||
override fun getPeerNickname(peerID: String): String? {
|
||||
return peerManager.getPeerNickname(peerID)
|
||||
}
|
||||
|
||||
override fun getNetworkSize(): Int {
|
||||
return peerManager.getActivePeerCount()
|
||||
}
|
||||
|
||||
override fun getMyNickname(): String? {
|
||||
return delegate?.getNickname()
|
||||
}
|
||||
|
||||
override fun getPeerInfo(peerID: String): PeerInfo? {
|
||||
return peerManager.getPeerInfo(peerID)
|
||||
}
|
||||
|
||||
override fun updatePeerInfo(
|
||||
peerID: String,
|
||||
nickname: String,
|
||||
noisePublicKey: ByteArray,
|
||||
signingPublicKey: ByteArray,
|
||||
isVerified: Boolean
|
||||
): Boolean {
|
||||
return peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
||||
}
|
||||
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
}
|
||||
|
||||
override fun relayPacket(routed: RoutedPacket) {
|
||||
dispatchGlobal(routed)
|
||||
}
|
||||
|
||||
override fun getBroadcastRecipient(): ByteArray {
|
||||
return SpecialRecipients.BROADCAST
|
||||
}
|
||||
|
||||
override fun verifySignature(packet: BitchatPacket, peerID: String): Boolean {
|
||||
return securityManager.verifySignature(packet, peerID)
|
||||
}
|
||||
|
||||
override fun encryptForPeer(data: ByteArray, recipientPeerID: String): ByteArray? {
|
||||
return securityManager.encryptForPeer(data, recipientPeerID)
|
||||
}
|
||||
|
||||
override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String): ByteArray? {
|
||||
return securityManager.decryptFromPeer(encryptedData, senderPeerID)
|
||||
}
|
||||
|
||||
override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean {
|
||||
return encryptionService.verifyEd25519Signature(signature, data, publicKey)
|
||||
}
|
||||
|
||||
override fun hasNoiseSession(peerID: String): Boolean {
|
||||
return encryptionService.hasEstablishedSession(peerID)
|
||||
}
|
||||
|
||||
override fun initiateNoiseHandshake(peerID: String) {
|
||||
this@MeshCore.initiateNoiseHandshake(peerID)
|
||||
}
|
||||
|
||||
override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? {
|
||||
return try {
|
||||
encryptionService.processHandshakeMessage(payload, peerID)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePeerIDBinding(
|
||||
newPeerID: String,
|
||||
nickname: String,
|
||||
publicKey: ByteArray,
|
||||
previousPeerID: String?
|
||||
) {
|
||||
peerManager.addOrUpdatePeer(newPeerID, nickname)
|
||||
val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey)
|
||||
previousPeerID?.let { peerManager.removePeer(it) }
|
||||
Log.d("MeshCore", "Updated peer ID binding: $newPeerID fp=${fingerprint.take(16)}")
|
||||
hooks.onPeerIdBindingUpdated?.invoke(newPeerID, nickname, publicKey, previousPeerID)
|
||||
}
|
||||
|
||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String? {
|
||||
return delegate?.decryptChannelMessage(encryptedContent, channel)
|
||||
}
|
||||
|
||||
override fun onMessageReceived(message: BitchatMessage) {
|
||||
hooks.onMessageReceived?.invoke(message)
|
||||
delegate?.didReceiveMessage(message)
|
||||
}
|
||||
|
||||
override fun onChannelLeave(channel: String, fromPeer: String) {
|
||||
delegate?.didReceiveChannelLeave(channel, fromPeer)
|
||||
}
|
||||
|
||||
override fun onDeliveryAckReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveDeliveryAck(messageID, peerID)
|
||||
}
|
||||
|
||||
override fun onReadReceiptReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveReadReceipt(messageID, peerID)
|
||||
}
|
||||
}
|
||||
|
||||
packetProcessor.delegate = object : PacketProcessorDelegate {
|
||||
override fun validatePacketSecurity(packet: BitchatPacket, peerID: String): Boolean {
|
||||
return securityManager.validatePacket(packet, peerID)
|
||||
}
|
||||
|
||||
override fun updatePeerLastSeen(peerID: String) {
|
||||
peerManager.updatePeerLastSeen(peerID)
|
||||
}
|
||||
|
||||
override fun getPeerNickname(peerID: String): String? {
|
||||
return peerManager.getPeerNickname(peerID)
|
||||
}
|
||||
|
||||
override fun getNetworkSize(): Int {
|
||||
return peerManager.getActivePeerCount()
|
||||
}
|
||||
|
||||
override fun getBroadcastRecipient(): ByteArray {
|
||||
return SpecialRecipients.BROADCAST
|
||||
}
|
||||
|
||||
override fun handleNoiseHandshake(routed: RoutedPacket): Boolean {
|
||||
return runBlocking { securityManager.handleNoiseHandshake(routed) }
|
||||
}
|
||||
|
||||
override fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||
scope.launch { messageHandler.handleNoiseEncrypted(routed) }
|
||||
}
|
||||
|
||||
override fun handleAnnounce(routed: RoutedPacket) {
|
||||
scope.launch {
|
||||
val isFirst = messageHandler.handleAnnounce(routed)
|
||||
hooks.onAnnounceProcessed?.invoke(routed, isFirst)
|
||||
try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleMessage(routed: RoutedPacket) {
|
||||
scope.launch { messageHandler.handleMessage(routed) }
|
||||
try {
|
||||
val pkt = routed.packet
|
||||
val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST))
|
||||
if (isBroadcast && pkt.type == MessageType.MESSAGE.value) {
|
||||
gossipSyncManager.onPublicPacketSeen(pkt)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
override fun handleLeave(routed: RoutedPacket) {
|
||||
scope.launch { messageHandler.handleLeave(routed) }
|
||||
}
|
||||
|
||||
override fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||
try {
|
||||
val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST))
|
||||
if (isBroadcast && packet.type == MessageType.FRAGMENT.value) {
|
||||
gossipSyncManager.onPublicPacketSeen(packet)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return fragmentManager.handleFragment(packet)
|
||||
}
|
||||
|
||||
override fun sendAnnouncementToPeer(peerID: String) {
|
||||
this@MeshCore.sendAnnouncementToPeer(peerID)
|
||||
}
|
||||
|
||||
override fun sendCachedMessages(peerID: String) {
|
||||
storeForwardManager.sendCachedMessages(peerID)
|
||||
}
|
||||
|
||||
override fun relayPacket(routed: RoutedPacket) {
|
||||
dispatchGlobal(routed)
|
||||
}
|
||||
|
||||
override fun handleRequestSync(routed: RoutedPacket) {
|
||||
val fromPeer = routed.peerID ?: return
|
||||
val req = RequestSyncPacket.decode(routed.packet.payload) ?: return
|
||||
gossipSyncManager.handleRequestSync(fromPeer, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null) {
|
||||
if (content.isEmpty()) return
|
||||
scope.launch {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = content.toByteArray(Charsets.UTF_8),
|
||||
signature = null,
|
||||
ttl = maxTtl
|
||||
)
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
fun sendFileBroadcast(file: BitchatFilePacket) {
|
||||
try {
|
||||
val payload = file.encode() ?: return
|
||||
scope.launch {
|
||||
val packet = BitchatPacket(
|
||||
version = 2u,
|
||||
type = MessageType.FILE_TRANSFER.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
ttl = maxTtl
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
val transferId = MeshPacketUtils.sha256Hex(payload)
|
||||
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MeshCore", "sendFileBroadcast failed: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) {
|
||||
try {
|
||||
scope.launch {
|
||||
if (!encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||
initiateNoiseHandshake(recipientPeerID)
|
||||
return@launch
|
||||
}
|
||||
val tlv = file.encode() ?: return@launch
|
||||
val np = NoisePayload(type = NoisePayloadType.FILE_TRANSFER, data = tlv).encode()
|
||||
val enc = encryptionService.encrypt(np, recipientPeerID)
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = enc,
|
||||
signature = null,
|
||||
ttl = maxTtl
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
val transferId = MeshPacketUtils.sha256Hex(tlv)
|
||||
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("MeshCore", "sendFilePrivate failed: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileTransfer(transferId: String): Boolean {
|
||||
return transport.cancelTransfer(transferId)
|
||||
}
|
||||
|
||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
||||
if (content.isEmpty() || recipientPeerID.isEmpty()) return
|
||||
scope.launch {
|
||||
val finalMessageID = messageID ?: java.util.UUID.randomUUID().toString()
|
||||
|
||||
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||
try {
|
||||
val privateMessage = PrivateMessagePacket(messageID = finalMessageID, content = content)
|
||||
val tlvData = privateMessage.encode() ?: return@launch
|
||||
val messagePayload = NoisePayload(
|
||||
type = NoisePayloadType.PRIVATE_MESSAGE,
|
||||
data = tlvData
|
||||
)
|
||||
val encrypted = encryptionService.encrypt(messagePayload.encode(), recipientPeerID)
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
ttl = maxTtl
|
||||
)
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
} catch (e: Exception) {
|
||||
Log.e("MeshCore", "Failed to encrypt private message: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
initiateNoiseHandshake(recipientPeerID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
scope.launch {
|
||||
if (hooks.readReceiptInterceptor?.invoke(messageID, recipientPeerID) == true) return@launch
|
||||
try {
|
||||
val payload = NoisePayload(
|
||||
type = NoisePayloadType.READ_RECEIPT,
|
||||
data = messageID.toByteArray(Charsets.UTF_8)
|
||||
).encode()
|
||||
val enc = encryptionService.encrypt(payload, recipientPeerID)
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = MeshPacketUtils.hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = enc,
|
||||
signature = null,
|
||||
ttl = maxTtl
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||
hooks.onReadReceiptSent?.invoke(messageID)
|
||||
} catch (e: Exception) {
|
||||
Log.e("MeshCore", "Failed to send read receipt: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendBroadcastAnnounce() {
|
||||
scope.launch {
|
||||
val nickname = hooks.announcementNicknameProvider?.invoke()
|
||||
?: delegate?.getNickname()
|
||||
?: myPeerID
|
||||
val staticKey = encryptionService.getStaticPublicKey() ?: run {
|
||||
Log.e("MeshCore", "No static public key available for announcement")
|
||||
return@launch
|
||||
}
|
||||
val signingKey = encryptionService.getSigningPublicKey() ?: run {
|
||||
Log.e("MeshCore", "No signing public key available for announcement")
|
||||
return@launch
|
||||
}
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||
val tlvPayload = announcement.encode() ?: return@launch
|
||||
val announcePacket = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = maxTtl,
|
||||
senderID = myPeerID,
|
||||
payload = tlvPayload
|
||||
)
|
||||
val signedPacket = signPacketBeforeBroadcast(announcePacket)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
fun sendAnnouncementToPeer(peerID: String) {
|
||||
if (peerManager.hasAnnouncedToPeer(peerID)) return
|
||||
val nickname = hooks.announcementNicknameProvider?.invoke()
|
||||
?: delegate?.getNickname()
|
||||
?: myPeerID
|
||||
val staticKey = encryptionService.getStaticPublicKey() ?: return
|
||||
val signingKey = encryptionService.getSigningPublicKey() ?: return
|
||||
val announcement = IdentityAnnouncement(nickname, staticKey, signingKey)
|
||||
val tlvPayload = announcement.encode() ?: return
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = maxTtl,
|
||||
senderID = myPeerID,
|
||||
payload = tlvPayload
|
||||
)
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
fun sendLeaveAnnouncement() {
|
||||
val payload = hooks.leavePayloadProvider?.invoke() ?: byteArrayOf()
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.LEAVE.value,
|
||||
ttl = maxTtl,
|
||||
senderID = myPeerID,
|
||||
payload = payload
|
||||
)
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
}
|
||||
|
||||
fun getPeerNicknames(): Map<String, String> = peerManager.getAllPeerNicknames()
|
||||
|
||||
fun getPeerRSSI(): Map<String, Int> = peerManager.getAllPeerRSSI()
|
||||
|
||||
fun getPeerNickname(peerID: String): String? = peerManager.getPeerNickname(peerID)
|
||||
|
||||
fun addOrUpdatePeer(peerID: String, nickname: String): Boolean {
|
||||
return peerManager.addOrUpdatePeer(peerID, nickname)
|
||||
}
|
||||
|
||||
fun removePeer(peerID: String) {
|
||||
peerManager.removePeer(peerID)
|
||||
}
|
||||
|
||||
fun setDirectConnection(peerID: String, isDirect: Boolean) {
|
||||
peerManager.setDirectConnection(peerID, isDirect)
|
||||
}
|
||||
|
||||
fun updatePeerRSSI(peerID: String, rssi: Int) {
|
||||
peerManager.updatePeerRSSI(peerID, rssi)
|
||||
}
|
||||
|
||||
fun getDebugInfoWithDeviceAddresses(deviceMap: Map<String, String>): String {
|
||||
return peerManager.getDebugInfoWithDeviceAddresses(deviceMap)
|
||||
}
|
||||
|
||||
fun getFingerprintDebugInfo(): String {
|
||||
return peerManager.getFingerprintDebugInfo()
|
||||
}
|
||||
|
||||
fun hasEstablishedSession(peerID: String): Boolean {
|
||||
return encryptionService.hasEstablishedSession(peerID)
|
||||
}
|
||||
|
||||
fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState {
|
||||
return encryptionService.getSessionState(peerID)
|
||||
}
|
||||
|
||||
fun initiateNoiseHandshake(peerID: String) {
|
||||
scope.launch {
|
||||
try {
|
||||
val handshakeData = encryptionService.initiateHandshake(peerID) ?: return@launch
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = MeshPacketUtils.hexStringToByteArray(myPeerID),
|
||||
recipientID = MeshPacketUtils.hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = handshakeData,
|
||||
ttl = maxTtl
|
||||
)
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signedPacket))
|
||||
} catch (e: Exception) {
|
||||
Log.e("MeshCore", "Failed to initiate Noise handshake with $peerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getPeerFingerprint(peerID: String): String? = peerManager.getFingerprintForPeer(peerID)
|
||||
|
||||
fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID)
|
||||
|
||||
fun updatePeerInfo(
|
||||
peerID: String,
|
||||
nickname: String,
|
||||
noisePublicKey: ByteArray,
|
||||
signingPublicKey: ByteArray,
|
||||
isVerified: Boolean
|
||||
): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
||||
|
||||
fun getIdentityFingerprint(): String = encryptionService.getIdentityFingerprint()
|
||||
|
||||
fun shouldShowEncryptionIcon(peerID: String): Boolean = encryptionService.hasEstablishedSession(peerID)
|
||||
|
||||
fun getEncryptedPeers(): List<String> = emptyList()
|
||||
|
||||
fun getActivePeerCount(): Int = try { peerManager.getActivePeerCount() } catch (_: Exception) { 0 }
|
||||
|
||||
fun getDeviceAddressForPeer(peerID: String): String? = transport.getDeviceAddressForPeer(peerID)
|
||||
|
||||
fun getDeviceAddressToPeerMapping(): Map<String, String> = transport.getDeviceAddressToPeerMapping()
|
||||
|
||||
fun getDebugStatus(
|
||||
transportInfo: String,
|
||||
deviceMap: Map<String, String>,
|
||||
extraLines: List<String> = emptyList(),
|
||||
title: String? = null
|
||||
): String {
|
||||
return buildString {
|
||||
appendLine("=== ${title ?: "${transport.id} Mesh Debug Status"} ===")
|
||||
appendLine("My Peer ID: $myPeerID")
|
||||
if (extraLines.isNotEmpty()) {
|
||||
extraLines.forEach { appendLine(it) }
|
||||
}
|
||||
appendLine(transportInfo)
|
||||
appendLine(peerManager.getDebugInfo(deviceMap))
|
||||
appendLine(fragmentManager.getDebugInfo())
|
||||
appendLine(securityManager.getDebugInfo())
|
||||
appendLine(storeForwardManager.getDebugInfo())
|
||||
appendLine(messageHandler.getDebugInfo())
|
||||
appendLine(packetProcessor.getDebugInfo())
|
||||
}
|
||||
}
|
||||
|
||||
fun clearAllInternalData() {
|
||||
fragmentManager.clearAllFragments()
|
||||
storeForwardManager.clearAllCache()
|
||||
securityManager.clearAllData()
|
||||
peerManager.clearAllPeers()
|
||||
peerManager.clearAllFingerprints()
|
||||
}
|
||||
|
||||
fun clearAllEncryptionData() {
|
||||
encryptionService.clearPersistentIdentity()
|
||||
}
|
||||
|
||||
private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||
return try {
|
||||
val packetDataForSigning = packet.toBinaryDataForSigning() ?: return packet
|
||||
val signature = encryptionService.signData(packetDataForSigning)
|
||||
if (signature != null) {
|
||||
packet.copy(signature = signature)
|
||||
} else {
|
||||
packet
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
packet
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
|
||||
/**
|
||||
* Shared mesh delegate interface for transport-agnostic callbacks.
|
||||
*/
|
||||
interface MeshDelegate {
|
||||
fun didReceiveMessage(message: BitchatMessage)
|
||||
fun didUpdatePeerList(peers: List<String>)
|
||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||
fun getNickname(): String?
|
||||
fun isFavorite(peerID: String): Boolean
|
||||
}
|
||||
|
||||
typealias BluetoothMeshDelegate = MeshDelegate
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
/**
|
||||
* Shared helpers for mesh packet handling.
|
||||
*/
|
||||
object MeshPacketUtils {
|
||||
/**
|
||||
* Convert hex string peer ID to binary data (8 bytes), matching iOS behavior.
|
||||
*/
|
||||
fun hexStringToByteArray(hexString: String): ByteArray {
|
||||
val result = ByteArray(8) { 0 }
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash payloads to a stable hex ID for transfer tracking.
|
||||
*/
|
||||
fun sha256Hex(bytes: ByteArray): String = try {
|
||||
val md = java.security.MessageDigest.getInstance("SHA-256")
|
||||
md.update(bytes)
|
||||
md.digest().joinToString("") { "%02x".format(it) }
|
||||
} catch (_: Exception) {
|
||||
bytes.size.toString(16)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import com.bitchat.android.model.BitchatFilePacket
|
||||
|
||||
/**
|
||||
* Transport-agnostic mesh service API for UI and routing layers.
|
||||
*/
|
||||
interface MeshService {
|
||||
val myPeerID: String
|
||||
var delegate: MeshDelegate?
|
||||
|
||||
fun startServices()
|
||||
fun stopServices()
|
||||
|
||||
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null)
|
||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null)
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String)
|
||||
fun sendFileBroadcast(file: BitchatFilePacket)
|
||||
fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket)
|
||||
fun cancelFileTransfer(transferId: String): Boolean
|
||||
|
||||
fun sendBroadcastAnnounce()
|
||||
fun sendAnnouncementToPeer(peerID: String)
|
||||
|
||||
fun getPeerNicknames(): Map<String, String>
|
||||
fun getPeerRSSI(): Map<String, Int>
|
||||
fun getActivePeerCount(): Int
|
||||
fun hasEstablishedSession(peerID: String): Boolean
|
||||
fun getSessionState(peerID: String): com.bitchat.android.noise.NoiseSession.NoiseSessionState
|
||||
fun initiateNoiseHandshake(peerID: String)
|
||||
fun getPeerFingerprint(peerID: String): String?
|
||||
fun getPeerInfo(peerID: String): PeerInfo?
|
||||
fun updatePeerInfo(
|
||||
peerID: String,
|
||||
nickname: String,
|
||||
noisePublicKey: ByteArray,
|
||||
signingPublicKey: ByteArray,
|
||||
isVerified: Boolean
|
||||
): Boolean
|
||||
fun getIdentityFingerprint(): String
|
||||
fun shouldShowEncryptionIcon(peerID: String): Boolean
|
||||
fun getEncryptedPeers(): List<String>
|
||||
|
||||
fun getDeviceAddressForPeer(peerID: String): String?
|
||||
fun getDeviceAddressToPeerMapping(): Map<String, String>
|
||||
fun printDeviceAddressesForPeers(): String
|
||||
fun getDebugStatus(): String
|
||||
|
||||
fun clearAllInternalData()
|
||||
fun clearAllEncryptionData()
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
|
||||
/**
|
||||
* Transport abstraction used by MeshCore to send packets via a specific medium.
|
||||
*/
|
||||
interface MeshTransport {
|
||||
val id: String
|
||||
|
||||
fun broadcastPacket(routed: RoutedPacket)
|
||||
|
||||
fun sendPacketToPeer(peerID: String, packet: BitchatPacket)
|
||||
|
||||
fun cancelTransfer(transferId: String): Boolean = false
|
||||
|
||||
fun getDeviceAddressForPeer(peerID: String): String? = null
|
||||
|
||||
fun getDeviceAddressToPeerMapping(): Map<String, String> = emptyMap()
|
||||
|
||||
fun getTransportDebugInfo(): String = ""
|
||||
}
|
||||
@@ -130,7 +130,7 @@ class MessageRouter private constructor(
|
||||
val myNpub = try { com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(context)?.npub } catch (_: Exception) { null }
|
||||
val content = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${myNpub ?: ""}"
|
||||
val nickname = mesh.getPeerNicknames()[toPeerID] ?: toPeerID
|
||||
mesh.sendPrivateMessage(content, toPeerID, nickname)
|
||||
mesh.sendPrivateMessage(content, toPeerID, nickname, null)
|
||||
} else {
|
||||
nostr.sendFavoriteNotification(toPeerID, isFavorite)
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ class ChatViewModel(
|
||||
|
||||
fun leaveChannel(channel: String) {
|
||||
channelManager.leaveChannel(channel)
|
||||
meshService.sendMessage("left $channel")
|
||||
meshService.sendMessage("left $channel", emptyList(), null)
|
||||
}
|
||||
|
||||
// MARK: - Private Chat Management (delegated)
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.bitchat.android.wifiaware
|
||||
|
||||
typealias WifiAwareMeshDelegate = com.bitchat.android.mesh.MeshDelegate
|
||||
@@ -12,32 +12,29 @@ import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.RequiresPermission
|
||||
import com.bitchat.android.crypto.EncryptionService
|
||||
import com.bitchat.android.model.*
|
||||
import com.bitchat.android.protocol.*
|
||||
import com.bitchat.android.mesh.MeshCore
|
||||
import com.bitchat.android.mesh.MeshService
|
||||
import com.bitchat.android.mesh.MeshTransport
|
||||
import com.bitchat.android.mesh.PeerInfo
|
||||
import com.bitchat.android.model.BitchatFilePacket
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.service.TransportBridgeService
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
// Mesh-layer components are reused from the existing Bluetooth stack
|
||||
import com.bitchat.android.mesh.PeerManager
|
||||
import com.bitchat.android.mesh.PeerManagerDelegate
|
||||
import com.bitchat.android.mesh.PeerInfo
|
||||
import com.bitchat.android.mesh.FragmentManager
|
||||
import com.bitchat.android.mesh.SecurityManager
|
||||
import com.bitchat.android.mesh.SecurityManagerDelegate
|
||||
import com.bitchat.android.mesh.StoreForwardManager
|
||||
import com.bitchat.android.mesh.StoreForwardManagerDelegate
|
||||
import com.bitchat.android.mesh.MessageHandler
|
||||
import com.bitchat.android.mesh.MessageHandlerDelegate
|
||||
import com.bitchat.android.mesh.PacketProcessor
|
||||
import com.bitchat.android.mesh.PacketProcessorDelegate
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import java.net.Inet6Address
|
||||
import java.net.ServerSocket
|
||||
import java.net.Socket
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@@ -52,7 +49,7 @@ import java.util.concurrent.Executors
|
||||
* - MessageHandler: Message type processing and relay logic
|
||||
* - PacketProcessor: Incoming packet routing
|
||||
*/
|
||||
class WifiAwareMeshService(private val context: Context) : TransportBridgeService.TransportLayer {
|
||||
class WifiAwareMeshService(private val context: Context) : MeshService, TransportBridgeService.TransportLayer {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WifiAwareMeshService"
|
||||
@@ -65,18 +62,10 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
private val encryptionService = EncryptionService(context)
|
||||
|
||||
// Peer ID must match BluetoothMeshService: first 16 hex chars of identity fingerprint (8 bytes)
|
||||
val myPeerID: String = encryptionService.getIdentityFingerprint().take(16)
|
||||
|
||||
// Core components
|
||||
private val peerManager = PeerManager()
|
||||
private val fragmentManager = FragmentManager()
|
||||
private val securityManager = SecurityManager(encryptionService, myPeerID)
|
||||
private val storeForwardManager = StoreForwardManager()
|
||||
private val messageHandler = MessageHandler(myPeerID, context.applicationContext)
|
||||
private val packetProcessor = PacketProcessor(myPeerID)
|
||||
|
||||
// Gossip sync
|
||||
private val gossipSyncManager: GossipSyncManager
|
||||
override val myPeerID: String = encryptionService.getIdentityFingerprint().take(16)
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val wifiTransport = WifiAwareTransport()
|
||||
private lateinit var meshCore: MeshCore
|
||||
|
||||
// Wi-Fi Aware transport
|
||||
private val awareManager = context.getSystemService(WifiAwareManager::class.java)
|
||||
@@ -87,10 +76,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
private var isActive = false
|
||||
|
||||
// Delegate
|
||||
var delegate: WifiAwareMeshDelegate? = null
|
||||
|
||||
// Coroutines - must be initialized before tracker
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
override var delegate: WifiAwareMeshDelegate? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (::meshCore.isInitialized) {
|
||||
meshCore.delegate = value
|
||||
}
|
||||
}
|
||||
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
// Transport state
|
||||
@@ -102,59 +94,31 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
||||
|
||||
init {
|
||||
setupDelegates()
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
|
||||
// Use shared GossipSyncManager from MeshServiceHolder if available (minimal refactor)
|
||||
val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager
|
||||
if (shared != null) {
|
||||
gossipSyncManager = shared
|
||||
} else {
|
||||
gossipSyncManager = GossipSyncManager(
|
||||
myPeerID = myPeerID,
|
||||
scope = serviceScope,
|
||||
configProvider = object : GossipSyncManager.ConfigProvider {
|
||||
override fun seenCapacity(): Int = 500
|
||||
override fun gcsMaxBytes(): Int = 400
|
||||
override fun gcsTargetFpr(): Double = 0.01
|
||||
meshCore = MeshCore(
|
||||
context = context.applicationContext,
|
||||
scope = serviceScope,
|
||||
transport = wifiTransport,
|
||||
encryptionService = encryptionService,
|
||||
myPeerID = myPeerID,
|
||||
maxTtl = MAX_TTL,
|
||||
sharedGossipManager = shared,
|
||||
gossipConfigProvider = object : GossipSyncManager.ConfigProvider {
|
||||
override fun seenCapacity(): Int = 500
|
||||
override fun gcsMaxBytes(): Int = 400
|
||||
override fun gcsTargetFpr(): Double = 0.01
|
||||
},
|
||||
hooks = MeshCore.Hooks(
|
||||
onAnnounceProcessed = { routed, _ ->
|
||||
routed.peerID?.let { pid ->
|
||||
try { meshCore.gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { }
|
||||
}
|
||||
},
|
||||
leavePayloadProvider = {
|
||||
(delegate?.getNickname() ?: myPeerID).toByteArray(Charsets.UTF_8)
|
||||
}
|
||||
)
|
||||
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||
this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
||||
}
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
dispatchGlobal(RoutedPacket(packet))
|
||||
}
|
||||
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||
return signPacketBeforeBroadcast(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method hexToBa.
|
||||
*/
|
||||
private fun hexStringToByteArray(hex: String): ByteArray {
|
||||
val out = ByteArray(8)
|
||||
var idx = 0
|
||||
var s = hex
|
||||
while (s.length >= 2 && idx < 8) {
|
||||
val b = s.substring(0, 2).toIntOrNull(16)?.toByte() ?: 0
|
||||
out[idx++] = b
|
||||
s = s.drop(2)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign packet before broadcasting.
|
||||
*/
|
||||
private fun signPacketBeforeBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||
val data = packet.toBinaryDataForSigning() ?: return packet
|
||||
val sig = encryptionService.signData(data) ?: return packet
|
||||
return packet.copy(signature = sig)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,17 +141,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
override fun send(packet: RoutedPacket) {
|
||||
// Received from bridge (e.g. BLE) -> Send via Wi-Fi
|
||||
// Direct injection prevents routing loops (bridge handles source check)
|
||||
broadcastPacket(packet)
|
||||
}
|
||||
|
||||
/**
|
||||
* unified dispatch: Send to local Wi-Fi and bridge to other transports
|
||||
*/
|
||||
private fun dispatchGlobal(routed: RoutedPacket) {
|
||||
// 1. Send to local Wi-Fi transport
|
||||
broadcastPacket(routed)
|
||||
// 2. Bridge to other transports (e.g. BLE)
|
||||
TransportBridgeService.broadcast("WIFI", routed)
|
||||
meshCore.sendFromBridge(packet)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,195 +180,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures delegates for internal components so that events are routed back
|
||||
* through this service and ultimately to the {@link WifiAwareMeshDelegate}.
|
||||
*/
|
||||
private fun setupDelegates() {
|
||||
peerManager.delegate = object : PeerManagerDelegate {
|
||||
override fun onPeerListUpdated(peerIDs: List<String>) {
|
||||
delegate?.didUpdatePeerList(peerIDs)
|
||||
}
|
||||
override fun onPeerRemoved(peerID: String) {
|
||||
try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { }
|
||||
try { encryptionService.removePeer(peerID) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
securityManager.delegate = object : SecurityManagerDelegate {
|
||||
override fun onKeyExchangeCompleted(peerID: String, peerPublicKeyData: ByteArray) {
|
||||
serviceScope.launch {
|
||||
delay(100)
|
||||
sendAnnouncementToPeer(peerID)
|
||||
delay(1000)
|
||||
storeForwardManager.sendCachedMessages(peerID)
|
||||
}
|
||||
}
|
||||
override fun sendHandshakeResponse(peerID: String, response: ByteArray) {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = response,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||
}
|
||||
override fun getPeerInfo(peerID: String): PeerInfo? {
|
||||
return peerManager.getPeerInfo(peerID)
|
||||
}
|
||||
}
|
||||
|
||||
storeForwardManager.delegate = object : StoreForwardManagerDelegate {
|
||||
override fun isFavorite(peerID: String) = delegate?.isFavorite(peerID) ?: false
|
||||
override fun isPeerOnline(peerID: String) = peerManager.isPeerActive(peerID)
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
dispatchGlobal(RoutedPacket(packet))
|
||||
}
|
||||
}
|
||||
|
||||
messageHandler.delegate = object : MessageHandlerDelegate {
|
||||
override fun addOrUpdatePeer(peerID: String, nickname: String) =
|
||||
peerManager.addOrUpdatePeer(peerID, nickname)
|
||||
override fun removePeer(peerID: String) = peerManager.removePeer(peerID)
|
||||
override fun updatePeerNickname(peerID: String, nickname: String) {
|
||||
peerManager.addOrUpdatePeer(peerID, nickname)
|
||||
}
|
||||
override fun getPeerNickname(peerID: String) =
|
||||
peerManager.getPeerNickname(peerID)
|
||||
override fun getNetworkSize() = peerManager.getActivePeerCount()
|
||||
override fun getMyNickname() = delegate?.getNickname()
|
||||
override fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID)
|
||||
override fun updatePeerInfo(
|
||||
peerID: String,
|
||||
nickname: String,
|
||||
noisePublicKey: ByteArray,
|
||||
signingPublicKey: ByteArray,
|
||||
isVerified: Boolean
|
||||
): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
||||
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||
}
|
||||
override fun relayPacket(routed: RoutedPacket) { dispatchGlobal(routed) }
|
||||
override fun getBroadcastRecipient() = SpecialRecipients.BROADCAST
|
||||
|
||||
override fun verifySignature(packet: BitchatPacket, peerID: String) =
|
||||
securityManager.verifySignature(packet, peerID)
|
||||
override fun encryptForPeer(data: ByteArray, recipientPeerID: String) =
|
||||
securityManager.encryptForPeer(data, recipientPeerID)
|
||||
override fun decryptFromPeer(encryptedData: ByteArray, senderPeerID: String) =
|
||||
securityManager.decryptFromPeer(encryptedData, senderPeerID)
|
||||
override fun verifyEd25519Signature(signature: ByteArray, data: ByteArray, publicKey: ByteArray): Boolean =
|
||||
encryptionService.verifyEd25519Signature(signature, data, publicKey)
|
||||
|
||||
override fun hasNoiseSession(peerID: String) =
|
||||
encryptionService.hasEstablishedSession(peerID)
|
||||
override fun initiateNoiseHandshake(peerID: String) {
|
||||
serviceScope.launch {
|
||||
val hs = encryptionService.initiateHandshake(peerID) ?: return@launch
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_HANDSHAKE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(peerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = hs,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||
}
|
||||
}
|
||||
override fun processNoiseHandshakeMessage(payload: ByteArray, peerID: String): ByteArray? =
|
||||
try { encryptionService.processHandshakeMessage(payload, peerID) } catch (_: Exception) { null }
|
||||
|
||||
override fun updatePeerIDBinding(newPeerID: String, nickname: String, publicKey: ByteArray, previousPeerID: String?) {
|
||||
peerManager.addOrUpdatePeer(newPeerID, nickname)
|
||||
val fingerprint = peerManager.storeFingerprintForPeer(newPeerID, publicKey)
|
||||
previousPeerID?.let { peerManager.removePeer(it) }
|
||||
Log.d(TAG, "Updated peer binding to $newPeerID, fp=${fingerprint.take(16)}")
|
||||
}
|
||||
|
||||
override fun decryptChannelMessage(encryptedContent: ByteArray, channel: String) =
|
||||
delegate?.decryptChannelMessage(encryptedContent, channel)
|
||||
|
||||
override fun onMessageReceived(message: BitchatMessage) {
|
||||
delegate?.didReceiveMessage(message)
|
||||
}
|
||||
override fun onChannelLeave(channel: String, fromPeer: String) {
|
||||
delegate?.didReceiveChannelLeave(channel, fromPeer)
|
||||
}
|
||||
override fun onDeliveryAckReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveDeliveryAck(messageID, peerID)
|
||||
}
|
||||
override fun onReadReceiptReceived(messageID: String, peerID: String) {
|
||||
delegate?.didReceiveReadReceipt(messageID, peerID)
|
||||
}
|
||||
}
|
||||
|
||||
packetProcessor.delegate = object : PacketProcessorDelegate {
|
||||
override fun validatePacketSecurity(packet: BitchatPacket, peerID: String) =
|
||||
securityManager.validatePacket(packet, peerID)
|
||||
override fun updatePeerLastSeen(peerID: String) = peerManager.updatePeerLastSeen(peerID)
|
||||
override fun getPeerNickname(peerID: String): String? = peerManager.getPeerNickname(peerID)
|
||||
override fun getNetworkSize(): Int = peerManager.getActivePeerCount()
|
||||
override fun getBroadcastRecipient(): ByteArray = SpecialRecipients.BROADCAST
|
||||
|
||||
override fun handleNoiseHandshake(routed: RoutedPacket): Boolean =
|
||||
runBlocking { securityManager.handleNoiseHandshake(routed) }
|
||||
|
||||
override fun handleNoiseEncrypted(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleNoiseEncrypted(routed) }
|
||||
}
|
||||
|
||||
override fun handleAnnounce(routed: RoutedPacket) {
|
||||
serviceScope.launch {
|
||||
val isFirst = messageHandler.handleAnnounce(routed)
|
||||
routed.peerID?.let { pid ->
|
||||
try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { }
|
||||
}
|
||||
try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleMessage(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleMessage(routed) }
|
||||
try {
|
||||
val pkt = routed.packet
|
||||
val isBroadcast = (pkt.recipientID == null || pkt.recipientID.contentEquals(SpecialRecipients.BROADCAST))
|
||||
if (isBroadcast && pkt.type == MessageType.MESSAGE.value) {
|
||||
gossipSyncManager.onPublicPacketSeen(pkt)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
|
||||
override fun handleLeave(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleLeave(routed) }
|
||||
}
|
||||
|
||||
override fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||
try {
|
||||
val isBroadcast = (packet.recipientID == null || packet.recipientID.contentEquals(SpecialRecipients.BROADCAST))
|
||||
if (isBroadcast && packet.type == MessageType.FRAGMENT.value) {
|
||||
gossipSyncManager.onPublicPacketSeen(packet)
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
return fragmentManager.handleFragment(packet)
|
||||
}
|
||||
|
||||
override fun sendAnnouncementToPeer(peerID: String) = this@WifiAwareMeshService.sendAnnouncementToPeer(peerID)
|
||||
override fun sendCachedMessages(peerID: String) = storeForwardManager.sendCachedMessages(peerID)
|
||||
override fun relayPacket(routed: RoutedPacket) = dispatchGlobal(routed)
|
||||
|
||||
override fun handleRequestSync(routed: RoutedPacket) {
|
||||
val fromPeer = routed.peerID ?: return
|
||||
val req = RequestSyncPacket.decode(routed.packet.payload) ?: return
|
||||
gossipSyncManager.handleRequestSync(fromPeer, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts Wi-Fi Aware services (publish + subscribe).
|
||||
@@ -428,7 +194,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
Manifest.permission.ACCESS_WIFI_STATE,
|
||||
Manifest.permission.CHANGE_WIFI_STATE
|
||||
])
|
||||
fun startServices() {
|
||||
override fun startServices() {
|
||||
if (isActive) return
|
||||
isActive = true
|
||||
Log.i(TAG, "Starting Wi-Fi Aware mesh with peer ID: $myPeerID")
|
||||
@@ -527,10 +293,9 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
}
|
||||
}, Handler(Looper.getMainLooper()))
|
||||
|
||||
sendPeriodicBroadcastAnnounce()
|
||||
meshCore.startCore()
|
||||
startPeriodicConnectionMaintenance()
|
||||
connectionTracker.start()
|
||||
gossipSyncManager.start()
|
||||
|
||||
// Register with cross-layer transport bridge
|
||||
TransportBridgeService.register("WIFI", this)
|
||||
@@ -539,7 +304,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
/**
|
||||
* Stops the Wi-Fi Aware mesh services and cleans up sockets and sessions.
|
||||
*/
|
||||
fun stopServices() {
|
||||
override fun stopServices() {
|
||||
if (!isActive) return
|
||||
isActive = false
|
||||
Log.i(TAG, "Stopping Wi-Fi Aware mesh")
|
||||
@@ -547,12 +312,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
// Unregister from bridge
|
||||
TransportBridgeService.unregister("WIFI")
|
||||
|
||||
sendLeaveAnnouncement()
|
||||
meshCore.sendLeaveAnnouncement()
|
||||
|
||||
serviceScope.launch {
|
||||
delay(200)
|
||||
|
||||
gossipSyncManager.stop()
|
||||
meshCore.stopCore()
|
||||
connectionTracker.stop() // Handles socket closing and callback unregistration
|
||||
|
||||
publishSession?.close(); publishSession = null
|
||||
@@ -561,29 +326,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
|
||||
handleToPeerId.clear()
|
||||
|
||||
peerManager.shutdown()
|
||||
fragmentManager.shutdown()
|
||||
securityManager.shutdown()
|
||||
storeForwardManager.shutdown()
|
||||
messageHandler.shutdown()
|
||||
packetProcessor.shutdown()
|
||||
meshCore.shutdown()
|
||||
|
||||
serviceScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically broadcasts an ANNOUNCE packet (every ~30s) while the service is active,
|
||||
* so new/idle peers can discover us without user action.
|
||||
*/
|
||||
private fun sendPeriodicBroadcastAnnounce() {
|
||||
serviceScope.launch {
|
||||
while (isActive) {
|
||||
try { delay(30_000); sendBroadcastAnnounce() } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodic active maintenance: retries connections to discovered but unconnected peers.
|
||||
*/
|
||||
@@ -681,13 +429,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
client.keepAlive = true
|
||||
Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}")
|
||||
connectionTracker.onClientConnected(peerId, client)
|
||||
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||
try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||
try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||
listenerExec.execute { listenToPeer(client, peerId) }
|
||||
handleSubscriberKeepAlive(client, peerId, pubSession, peerHandle)
|
||||
// Kick off Noise handshake for this logical peer
|
||||
if (myPeerID < peerId) {
|
||||
messageHandler.delegate?.initiateNoiseHandshake(peerId)
|
||||
meshCore.initiateNoiseHandshake(peerId)
|
||||
Log.d(TAG, "SERVER: initiating Noise handshake to ${peerId.take(8)} (lower ID)")
|
||||
}
|
||||
// Ensure fast presence even before handshake settles
|
||||
@@ -817,13 +565,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
Log.d(TAG, "CLIENT: TCP connected to ${peerId.take(8)} addr=$scopedAddr:$port (iface=$iface)")
|
||||
|
||||
connectionTracker.onClientConnected(peerId, sock)
|
||||
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||
try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||
try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||
listenerExec.execute { listenToPeer(sock, peerId) }
|
||||
handleServerKeepAlive(sock, peerId, peerHandle)
|
||||
// Kick off Noise handshake for this logical peer
|
||||
if (myPeerID < peerId) {
|
||||
messageHandler.delegate?.initiateNoiseHandshake(peerId)
|
||||
meshCore.initiateNoiseHandshake(peerId)
|
||||
Log.d(TAG, "CLIENT: initiating Noise handshake to ${peerId.take(8)} (lower ID)")
|
||||
}
|
||||
// Ensure fast presence even before handshake settles
|
||||
@@ -908,7 +656,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
// Note: We do NOT update peerSockets mapping based on senderPeerHex.
|
||||
// The socket belongs to initialLogicalPeerId effectively serving as the "MAC address" layer.
|
||||
Log.d(TAG, "RX: packet type=${pkt.type} from ${senderPeerHex.take(8)} via ${initialLogicalPeerId.take(8)} (bytes=${raw.size})")
|
||||
packetProcessor.processPacket(RoutedPacket(pkt, senderPeerHex, initialLogicalPeerId))
|
||||
meshCore.processIncoming(pkt, senderPeerHex, initialLogicalPeerId)
|
||||
}
|
||||
|
||||
// Breaking out of the loop means the socket is dead or service is stopping.
|
||||
@@ -922,7 +670,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
Log.d(TAG, "Cleaning up peer: $initialId")
|
||||
|
||||
connectionTracker.disconnect(initialId)
|
||||
peerManager.removePeer(initialId)
|
||||
meshCore.removePeer(initialId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,24 +680,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @param mentions Optional list of mentioned peer IDs
|
||||
* @param channel Optional channel name
|
||||
*/
|
||||
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null) {
|
||||
if (content.isEmpty()) return
|
||||
|
||||
serviceScope.launch {
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.MESSAGE.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = content.toByteArray(Charsets.UTF_8),
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
dispatchGlobal(RoutedPacket(signed))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
override fun sendMessage(content: String, mentions: List<String>, channel: String?) {
|
||||
meshCore.sendMessage(content, mentions, channel)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -960,37 +692,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @param recipientNickname Recipient nickname
|
||||
* @param messageID Optional message ID (UUID if null)
|
||||
*/
|
||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
||||
if (content.isEmpty() || recipientPeerID.isEmpty()) return
|
||||
|
||||
serviceScope.launch {
|
||||
val finalId = messageID ?: UUID.randomUUID().toString()
|
||||
|
||||
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||
try {
|
||||
val pm = PrivateMessagePacket(messageID = finalId, content = content)
|
||||
val tlv = pm.encode() ?: return@launch
|
||||
val payload = NoisePayload(type = NoisePayloadType.PRIVATE_MESSAGE, data = tlv).encode()
|
||||
val enc = encryptionService.encrypt(payload, recipientPeerID)
|
||||
|
||||
val pkt = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = enc,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(pkt)))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to encrypt private message: ${e.message}")
|
||||
}
|
||||
} else {
|
||||
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
||||
}
|
||||
}
|
||||
override fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String?) {
|
||||
meshCore.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1001,29 +704,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @param recipientPeerID The peer to notify.
|
||||
* @param readerNickname Nickname of the reader (may be shown by the receiver).
|
||||
*/
|
||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val payload = NoisePayload(
|
||||
type = NoisePayloadType.READ_RECEIPT,
|
||||
data = messageID.toByteArray(Charsets.UTF_8)
|
||||
).encode()
|
||||
val enc = encryptionService.encrypt(payload, recipientPeerID)
|
||||
val pkt = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = enc,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(pkt)))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send read receipt: ${e.message}")
|
||||
}
|
||||
}
|
||||
override fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||
meshCore.sendReadReceipt(messageID, recipientPeerID, readerNickname)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1032,28 +714,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
*
|
||||
* @param file Encoded metadata and chunks descriptor of the file to send.
|
||||
*/
|
||||
fun sendFileBroadcast(file: BitchatFilePacket) {
|
||||
try {
|
||||
val payload = file.encode() ?: run { Log.e(TAG, "file TLV encode failed"); return }
|
||||
serviceScope.launch {
|
||||
val pkt = BitchatPacket(
|
||||
version = 2u, // FILE_TRANSFER big length
|
||||
type = MessageType.FILE_TRANSFER.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = SpecialRecipients.BROADCAST,
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = payload,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(pkt)
|
||||
val transferId = sha256Hex(payload)
|
||||
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "sendFileBroadcast failed: ${e.message}", e)
|
||||
}
|
||||
override fun sendFileBroadcast(file: BitchatFilePacket) {
|
||||
meshCore.sendFileBroadcast(file)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1063,33 +725,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @param recipientPeerID Target peer.
|
||||
* @param file Encoded metadata and chunks descriptor of the file to send.
|
||||
*/
|
||||
fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) {
|
||||
try {
|
||||
serviceScope.launch {
|
||||
if (!encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
||||
return@launch
|
||||
}
|
||||
val tlv = file.encode() ?: return@launch
|
||||
val np = NoisePayload(type = NoisePayloadType.FILE_TRANSFER, data = tlv).encode()
|
||||
val enc = encryptionService.encrypt(np, recipientPeerID)
|
||||
val pkt = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = enc,
|
||||
signature = null,
|
||||
ttl = MAX_TTL
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(pkt)
|
||||
val transferId = sha256Hex(tlv)
|
||||
dispatchGlobal(RoutedPacket(signed, transferId = transferId))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "sendFilePrivate failed: ${e.message}", e)
|
||||
}
|
||||
override fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) {
|
||||
meshCore.sendFilePrivate(recipientPeerID, file)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1098,116 +735,57 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @param transferId Deterministic id (usually sha256 of the file TLV).
|
||||
* @return true if a transfer with this id was found and cancellation was scheduled, false otherwise.
|
||||
*/
|
||||
fun cancelFileTransfer(transferId: String): Boolean {
|
||||
return false
|
||||
override fun cancelFileTransfer(transferId: String): Boolean {
|
||||
return meshCore.cancelFileTransfer(transferId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the SHA-256 of the given bytes and returns a lowercase hex string.
|
||||
* Falls back to the byte-length in hex if MessageDigest is unavailable.
|
||||
*/
|
||||
private fun sha256Hex(bytes: ByteArray): String = try {
|
||||
val md = java.security.MessageDigest.getInstance("SHA-256")
|
||||
md.update(bytes); md.digest().joinToString("") { "%02x".format(it) }
|
||||
} catch (_: Exception) { bytes.size.toString(16) }
|
||||
|
||||
/**
|
||||
* Broadcasts an ANNOUNCE packet to the entire mesh.
|
||||
*/
|
||||
fun sendBroadcastAnnounce() {
|
||||
serviceScope.launch {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
val staticKey = encryptionService.getStaticPublicKey() ?: run {
|
||||
Log.e(TAG, "No static public key available for announcement"); return@launch
|
||||
}
|
||||
val signingKey = encryptionService.getSigningPublicKey() ?: run {
|
||||
Log.e(TAG, "No signing public key available for announcement"); return@launch
|
||||
}
|
||||
|
||||
val tlvPayload = IdentityAnnouncement(nickname, staticKey, signingKey).encode() ?: return@launch
|
||||
|
||||
val announcePacket = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = tlvPayload
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(announcePacket)
|
||||
|
||||
dispatchGlobal(RoutedPacket(signed))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
override fun sendBroadcastAnnounce() {
|
||||
meshCore.sendBroadcastAnnounce()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an ANNOUNCE packet to a specific peer.
|
||||
*/
|
||||
fun sendAnnouncementToPeer(peerID: String) {
|
||||
if (peerManager.hasAnnouncedToPeer(peerID)) return
|
||||
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
val staticKey = encryptionService.getStaticPublicKey() ?: return
|
||||
val signingKey = encryptionService.getSigningPublicKey() ?: return
|
||||
|
||||
val tlvPayload = IdentityAnnouncement(nickname, staticKey, signingKey).encode() ?: return
|
||||
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.ANNOUNCE.value,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = tlvPayload
|
||||
)
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
|
||||
dispatchGlobal(RoutedPacket(signed))
|
||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a LEAVE announcement to all peers before disconnecting.
|
||||
*/
|
||||
private fun sendLeaveAnnouncement() {
|
||||
val nickname = delegate?.getNickname() ?: myPeerID
|
||||
val packet = BitchatPacket(
|
||||
type = MessageType.LEAVE.value,
|
||||
ttl = MAX_TTL,
|
||||
senderID = myPeerID,
|
||||
payload = nickname.toByteArray()
|
||||
)
|
||||
dispatchGlobal(RoutedPacket(signPacketBeforeBroadcast(packet)))
|
||||
override fun sendAnnouncementToPeer(peerID: String) {
|
||||
meshCore.sendAnnouncementToPeer(peerID)
|
||||
}
|
||||
|
||||
/** @return Mapping of peer IDs to nicknames. */
|
||||
fun getPeerNicknames(): Map<String, String> = peerManager.getAllPeerNicknames()
|
||||
override fun getPeerNicknames(): Map<String, String> = meshCore.getPeerNicknames()
|
||||
|
||||
/** @return Mapping of peer IDs to RSSI values. */
|
||||
fun getPeerRSSI(): Map<String, Int> = peerManager.getAllPeerRSSI()
|
||||
override fun getPeerRSSI(): Map<String, Int> = meshCore.getPeerRSSI()
|
||||
|
||||
/** @return current active peer count for status surfaces. */
|
||||
override fun getActivePeerCount(): Int = meshCore.getActivePeerCount()
|
||||
|
||||
/**
|
||||
* @return true if a Noise session with the peer is fully established.
|
||||
*/
|
||||
fun hasEstablishedSession(peerID: String) = encryptionService.hasEstablishedSession(peerID)
|
||||
override fun hasEstablishedSession(peerID: String) = meshCore.hasEstablishedSession(peerID)
|
||||
|
||||
/**
|
||||
* @return a human-readable Noise session state for the given peer (implementation-defined).
|
||||
*/
|
||||
fun getSessionState(peerID: String) = encryptionService.getSessionState(peerID)
|
||||
override fun getSessionState(peerID: String) = meshCore.getSessionState(peerID)
|
||||
|
||||
/**
|
||||
* Triggers a Noise handshake with the given peer. Safe to call repeatedly; no-op if already handshaking/established.
|
||||
*/
|
||||
fun initiateNoiseHandshake(peerID: String) = messageHandler.delegate?.initiateNoiseHandshake(peerID)
|
||||
override fun initiateNoiseHandshake(peerID: String) = meshCore.initiateNoiseHandshake(peerID)
|
||||
|
||||
/**
|
||||
* @return the stored public-key fingerprint (hex) for a peer, if known.
|
||||
*/
|
||||
fun getPeerFingerprint(peerID: String): String? = peerManager.getFingerprintForPeer(peerID)
|
||||
override fun getPeerFingerprint(peerID: String): String? = meshCore.getPeerFingerprint(peerID)
|
||||
|
||||
/**
|
||||
* Retrieves the full profile for a peer, including keys and verification state, if available.
|
||||
*/
|
||||
fun getPeerInfo(peerID: String): PeerInfo? = peerManager.getPeerInfo(peerID)
|
||||
override fun getPeerInfo(peerID: String): PeerInfo? = meshCore.getPeerInfo(peerID)
|
||||
|
||||
/**
|
||||
* Updates local metadata for a peer and returns whether the change was applied.
|
||||
@@ -1219,35 +797,35 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @param isVerified Whether this identity is verified by the user.
|
||||
* @return true if the record was updated or created, false otherwise.
|
||||
*/
|
||||
fun updatePeerInfo(
|
||||
override fun updatePeerInfo(
|
||||
peerID: String,
|
||||
nickname: String,
|
||||
noisePublicKey: ByteArray,
|
||||
signingPublicKey: ByteArray,
|
||||
isVerified: Boolean
|
||||
): Boolean = peerManager.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
||||
): Boolean = meshCore.updatePeerInfo(peerID, nickname, noisePublicKey, signingPublicKey, isVerified)
|
||||
|
||||
/**
|
||||
* @return the local device’s long-term identity fingerprint (hex).
|
||||
*/
|
||||
fun getIdentityFingerprint(): String = encryptionService.getIdentityFingerprint()
|
||||
override fun getIdentityFingerprint(): String = meshCore.getIdentityFingerprint()
|
||||
|
||||
/**
|
||||
* @return true if the UI should show an “encrypted” indicator for this peer.
|
||||
*/
|
||||
fun shouldShowEncryptionIcon(peerID: String) = encryptionService.hasEstablishedSession(peerID)
|
||||
override fun shouldShowEncryptionIcon(peerID: String) = meshCore.shouldShowEncryptionIcon(peerID)
|
||||
|
||||
/**
|
||||
* @return a snapshot list of peers with established Noise sessions.
|
||||
*/
|
||||
fun getEncryptedPeers(): List<String> = emptyList()
|
||||
override fun getEncryptedPeers(): List<String> = meshCore.getEncryptedPeers()
|
||||
|
||||
/**
|
||||
* @return the current IPv4/IPv6 address of a connected peer, if any.
|
||||
* Prefers the scoped IPv6 address format.
|
||||
*/
|
||||
fun getDeviceAddressForPeer(peerID: String): String? =
|
||||
connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) }
|
||||
override fun getDeviceAddressForPeer(peerID: String): String? =
|
||||
meshCore.getDeviceAddressForPeer(peerID)
|
||||
|
||||
/**
|
||||
* Helper to resolve a scoped IPv6 address from a socket for UI display.
|
||||
@@ -1269,18 +847,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
* @return a mapping of peerID → connected device IP address for all active sockets.
|
||||
* Results are formatted as scoped addresses if applicable.
|
||||
*/
|
||||
fun getDeviceAddressToPeerMapping(): Map<String, String> {
|
||||
val map = mutableMapOf<String, String>()
|
||||
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
||||
map[pid] = resolveScopedAddress(sock) ?: "unknown"
|
||||
}
|
||||
return map
|
||||
}
|
||||
override fun getDeviceAddressToPeerMapping(): Map<String, String> =
|
||||
meshCore.getDeviceAddressToPeerMapping()
|
||||
|
||||
/**
|
||||
* @return map of peer ID to nickname, bridged for UI warning fix.
|
||||
*/
|
||||
fun getPeerNicknamesMap(): Map<String, String?> = peerManager.getAllPeerNicknames()
|
||||
fun getPeerNicknamesMap(): Map<String, String?> = meshCore.getPeerNicknames()
|
||||
|
||||
/** Returns recently discovered peer IDs via Aware discovery (may not be connected). */
|
||||
fun getDiscoveredPeerIds(): Set<String> =
|
||||
@@ -1289,23 +862,27 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
/**
|
||||
* Utility for logs/UI: pretty-prints one peer-to-address mapping per line.
|
||||
*/
|
||||
fun printDeviceAddressesForPeers(): String =
|
||||
override fun printDeviceAddressesForPeers(): String =
|
||||
getDeviceAddressToPeerMapping().entries.joinToString("\n") { "${it.key} -> ${it.value}" }
|
||||
|
||||
/**
|
||||
* @return A detailed string containing the debug status of all mesh components.
|
||||
*/
|
||||
fun getDebugStatus(): String = buildString {
|
||||
appendLine("=== Wi-Fi Aware Mesh Debug Status ===")
|
||||
appendLine("My Peer ID: $myPeerID")
|
||||
appendLine("Peers: ${connectionTracker.peerSockets.keys}")
|
||||
appendLine(connectionTracker.getDebugInfo())
|
||||
appendLine(peerManager.getDebugInfo(getDeviceAddressToPeerMapping()))
|
||||
appendLine(fragmentManager.getDebugInfo())
|
||||
appendLine(securityManager.getDebugInfo())
|
||||
appendLine(storeForwardManager.getDebugInfo())
|
||||
appendLine(messageHandler.getDebugInfo())
|
||||
appendLine(packetProcessor.getDebugInfo())
|
||||
override fun getDebugStatus(): String {
|
||||
return meshCore.getDebugStatus(
|
||||
transportInfo = connectionTracker.getDebugInfo(),
|
||||
deviceMap = getDeviceAddressToPeerMapping(),
|
||||
extraLines = listOf("Peers: ${connectionTracker.peerSockets.keys}"),
|
||||
title = "Wi-Fi Aware Mesh Debug Status"
|
||||
)
|
||||
}
|
||||
|
||||
override fun clearAllInternalData() {
|
||||
meshCore.clearAllInternalData()
|
||||
}
|
||||
|
||||
override fun clearAllEncryptionData() {
|
||||
meshCore.clearAllEncryptionData()
|
||||
}
|
||||
|
||||
/** Utility extension to safely close sockets. */
|
||||
@@ -1313,20 +890,32 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
||||
|
||||
/** Utility extension to safely close server sockets. */
|
||||
private fun ServerSocket.closeQuietly() = try { close() } catch (_: Exception) {}
|
||||
}
|
||||
|
||||
private inner class WifiAwareTransport : MeshTransport {
|
||||
override val id: String = "WIFI"
|
||||
|
||||
/**
|
||||
* Delegate interface for mesh service callbacks (maintains exact same interface)
|
||||
*/
|
||||
interface WifiAwareMeshDelegate {
|
||||
fun didReceiveMessage(message: BitchatMessage)
|
||||
fun didUpdatePeerList(peers: List<String>)
|
||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
||||
fun getNickname(): String?
|
||||
fun isFavorite(peerID: String): Boolean
|
||||
// registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
|
||||
override fun broadcastPacket(routed: RoutedPacket) {
|
||||
this@WifiAwareMeshService.broadcastPacket(routed)
|
||||
}
|
||||
|
||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||
this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
||||
}
|
||||
|
||||
override fun getDeviceAddressForPeer(peerID: String): String? {
|
||||
return connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) }
|
||||
}
|
||||
|
||||
override fun getDeviceAddressToPeerMapping(): Map<String, String> {
|
||||
val map = mutableMapOf<String, String>()
|
||||
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
||||
map[pid] = resolveScopedAddress(sock) ?: "unknown"
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
override fun getTransportDebugInfo(): String {
|
||||
return connectionTracker.getDebugInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user