mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 20:45:20 +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 myNpub = try { com.bitchat.android.nostr.NostrIdentityBridge.getCurrentNostrIdentity(context)?.npub } catch (_: Exception) { null }
|
||||||
val content = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${myNpub ?: ""}"
|
val content = if (isFavorite) "[FAVORITED]:${myNpub ?: ""}" else "[UNFAVORITED]:${myNpub ?: ""}"
|
||||||
val nickname = mesh.getPeerNicknames()[toPeerID] ?: toPeerID
|
val nickname = mesh.getPeerNicknames()[toPeerID] ?: toPeerID
|
||||||
mesh.sendPrivateMessage(content, toPeerID, nickname)
|
mesh.sendPrivateMessage(content, toPeerID, nickname, null)
|
||||||
} else {
|
} else {
|
||||||
nostr.sendFavoriteNotification(toPeerID, isFavorite)
|
nostr.sendFavoriteNotification(toPeerID, isFavorite)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ class ChatViewModel(
|
|||||||
|
|
||||||
fun leaveChannel(channel: String) {
|
fun leaveChannel(channel: String) {
|
||||||
channelManager.leaveChannel(channel)
|
channelManager.leaveChannel(channel)
|
||||||
meshService.sendMessage("left $channel")
|
meshService.sendMessage("left $channel", emptyList(), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private Chat Management (delegated)
|
// 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.RequiresApi
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import com.bitchat.android.crypto.EncryptionService
|
import com.bitchat.android.crypto.EncryptionService
|
||||||
import com.bitchat.android.model.*
|
import com.bitchat.android.mesh.MeshCore
|
||||||
import com.bitchat.android.protocol.*
|
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.service.TransportBridgeService
|
||||||
import com.bitchat.android.sync.GossipSyncManager
|
import com.bitchat.android.sync.GossipSyncManager
|
||||||
import com.bitchat.android.util.toHexString
|
import com.bitchat.android.util.toHexString
|
||||||
// Mesh-layer components are reused from the existing Bluetooth stack
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import com.bitchat.android.mesh.PeerManager
|
import kotlinx.coroutines.Dispatchers
|
||||||
import com.bitchat.android.mesh.PeerManagerDelegate
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import com.bitchat.android.mesh.PeerInfo
|
import kotlinx.coroutines.cancel
|
||||||
import com.bitchat.android.mesh.FragmentManager
|
import kotlinx.coroutines.delay
|
||||||
import com.bitchat.android.mesh.SecurityManager
|
import kotlinx.coroutines.isActive
|
||||||
import com.bitchat.android.mesh.SecurityManagerDelegate
|
import kotlinx.coroutines.launch
|
||||||
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 java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.Inet6Address
|
import java.net.Inet6Address
|
||||||
import java.net.ServerSocket
|
import java.net.ServerSocket
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.nio.ByteOrder
|
import java.nio.ByteOrder
|
||||||
import java.util.UUID
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
@@ -52,7 +49,7 @@ import java.util.concurrent.Executors
|
|||||||
* - MessageHandler: Message type processing and relay logic
|
* - MessageHandler: Message type processing and relay logic
|
||||||
* - PacketProcessor: Incoming packet routing
|
* - PacketProcessor: Incoming packet routing
|
||||||
*/
|
*/
|
||||||
class WifiAwareMeshService(private val context: Context) : TransportBridgeService.TransportLayer {
|
class WifiAwareMeshService(private val context: Context) : MeshService, TransportBridgeService.TransportLayer {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "WifiAwareMeshService"
|
private const val TAG = "WifiAwareMeshService"
|
||||||
@@ -65,18 +62,10 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
private val encryptionService = EncryptionService(context)
|
private val encryptionService = EncryptionService(context)
|
||||||
|
|
||||||
// Peer ID must match BluetoothMeshService: first 16 hex chars of identity fingerprint (8 bytes)
|
// Peer ID must match BluetoothMeshService: first 16 hex chars of identity fingerprint (8 bytes)
|
||||||
val myPeerID: String = encryptionService.getIdentityFingerprint().take(16)
|
override val myPeerID: String = encryptionService.getIdentityFingerprint().take(16)
|
||||||
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
// Core components
|
private val wifiTransport = WifiAwareTransport()
|
||||||
private val peerManager = PeerManager()
|
private lateinit var meshCore: MeshCore
|
||||||
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
|
|
||||||
|
|
||||||
// Wi-Fi Aware transport
|
// Wi-Fi Aware transport
|
||||||
private val awareManager = context.getSystemService(WifiAwareManager::class.java)
|
private val awareManager = context.getSystemService(WifiAwareManager::class.java)
|
||||||
@@ -87,10 +76,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
private var isActive = false
|
private var isActive = false
|
||||||
|
|
||||||
// Delegate
|
// Delegate
|
||||||
var delegate: WifiAwareMeshDelegate? = null
|
override var delegate: WifiAwareMeshDelegate? = null
|
||||||
|
set(value) {
|
||||||
// Coroutines - must be initialized before tracker
|
field = value
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
if (::meshCore.isInitialized) {
|
||||||
|
meshCore.delegate = value
|
||||||
|
}
|
||||||
|
}
|
||||||
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
// Transport state
|
// Transport state
|
||||||
@@ -102,59 +94,31 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupDelegates()
|
|
||||||
messageHandler.packetProcessor = packetProcessor
|
|
||||||
|
|
||||||
// Use shared GossipSyncManager from MeshServiceHolder if available (minimal refactor)
|
|
||||||
val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager
|
val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager
|
||||||
if (shared != null) {
|
meshCore = MeshCore(
|
||||||
gossipSyncManager = shared
|
context = context.applicationContext,
|
||||||
} else {
|
scope = serviceScope,
|
||||||
gossipSyncManager = GossipSyncManager(
|
transport = wifiTransport,
|
||||||
myPeerID = myPeerID,
|
encryptionService = encryptionService,
|
||||||
scope = serviceScope,
|
myPeerID = myPeerID,
|
||||||
configProvider = object : GossipSyncManager.ConfigProvider {
|
maxTtl = MAX_TTL,
|
||||||
override fun seenCapacity(): Int = 500
|
sharedGossipManager = shared,
|
||||||
override fun gcsMaxBytes(): Int = 400
|
gossipConfigProvider = object : GossipSyncManager.ConfigProvider {
|
||||||
override fun gcsTargetFpr(): Double = 0.01
|
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) {
|
override fun send(packet: RoutedPacket) {
|
||||||
// Received from bridge (e.g. BLE) -> Send via Wi-Fi
|
// Received from bridge (e.g. BLE) -> Send via Wi-Fi
|
||||||
// Direct injection prevents routing loops (bridge handles source check)
|
// Direct injection prevents routing loops (bridge handles source check)
|
||||||
broadcastPacket(packet)
|
meshCore.sendFromBridge(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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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).
|
* 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.ACCESS_WIFI_STATE,
|
||||||
Manifest.permission.CHANGE_WIFI_STATE
|
Manifest.permission.CHANGE_WIFI_STATE
|
||||||
])
|
])
|
||||||
fun startServices() {
|
override fun startServices() {
|
||||||
if (isActive) return
|
if (isActive) return
|
||||||
isActive = true
|
isActive = true
|
||||||
Log.i(TAG, "Starting Wi-Fi Aware mesh with peer ID: $myPeerID")
|
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()))
|
}, Handler(Looper.getMainLooper()))
|
||||||
|
|
||||||
sendPeriodicBroadcastAnnounce()
|
meshCore.startCore()
|
||||||
startPeriodicConnectionMaintenance()
|
startPeriodicConnectionMaintenance()
|
||||||
connectionTracker.start()
|
connectionTracker.start()
|
||||||
gossipSyncManager.start()
|
|
||||||
|
|
||||||
// Register with cross-layer transport bridge
|
// Register with cross-layer transport bridge
|
||||||
TransportBridgeService.register("WIFI", this)
|
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.
|
* Stops the Wi-Fi Aware mesh services and cleans up sockets and sessions.
|
||||||
*/
|
*/
|
||||||
fun stopServices() {
|
override fun stopServices() {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
isActive = false
|
isActive = false
|
||||||
Log.i(TAG, "Stopping Wi-Fi Aware mesh")
|
Log.i(TAG, "Stopping Wi-Fi Aware mesh")
|
||||||
@@ -547,12 +312,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Unregister from bridge
|
// Unregister from bridge
|
||||||
TransportBridgeService.unregister("WIFI")
|
TransportBridgeService.unregister("WIFI")
|
||||||
|
|
||||||
sendLeaveAnnouncement()
|
meshCore.sendLeaveAnnouncement()
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
delay(200)
|
delay(200)
|
||||||
|
|
||||||
gossipSyncManager.stop()
|
meshCore.stopCore()
|
||||||
connectionTracker.stop() // Handles socket closing and callback unregistration
|
connectionTracker.stop() // Handles socket closing and callback unregistration
|
||||||
|
|
||||||
publishSession?.close(); publishSession = null
|
publishSession?.close(); publishSession = null
|
||||||
@@ -561,29 +326,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
handleToPeerId.clear()
|
handleToPeerId.clear()
|
||||||
|
|
||||||
peerManager.shutdown()
|
meshCore.shutdown()
|
||||||
fragmentManager.shutdown()
|
|
||||||
securityManager.shutdown()
|
|
||||||
storeForwardManager.shutdown()
|
|
||||||
messageHandler.shutdown()
|
|
||||||
packetProcessor.shutdown()
|
|
||||||
|
|
||||||
serviceScope.cancel()
|
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.
|
* Periodic active maintenance: retries connections to discovered but unconnected peers.
|
||||||
*/
|
*/
|
||||||
@@ -681,13 +429,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
client.keepAlive = true
|
client.keepAlive = true
|
||||||
Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}")
|
Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}")
|
||||||
connectionTracker.onClientConnected(peerId, client)
|
connectionTracker.onClientConnected(peerId, client)
|
||||||
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||||
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||||
listenerExec.execute { listenToPeer(client, peerId) }
|
listenerExec.execute { listenToPeer(client, peerId) }
|
||||||
handleSubscriberKeepAlive(client, peerId, pubSession, peerHandle)
|
handleSubscriberKeepAlive(client, peerId, pubSession, peerHandle)
|
||||||
// Kick off Noise handshake for this logical peer
|
// Kick off Noise handshake for this logical peer
|
||||||
if (myPeerID < peerId) {
|
if (myPeerID < peerId) {
|
||||||
messageHandler.delegate?.initiateNoiseHandshake(peerId)
|
meshCore.initiateNoiseHandshake(peerId)
|
||||||
Log.d(TAG, "SERVER: initiating Noise handshake to ${peerId.take(8)} (lower ID)")
|
Log.d(TAG, "SERVER: initiating Noise handshake to ${peerId.take(8)} (lower ID)")
|
||||||
}
|
}
|
||||||
// Ensure fast presence even before handshake settles
|
// 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)")
|
Log.d(TAG, "CLIENT: TCP connected to ${peerId.take(8)} addr=$scopedAddr:$port (iface=$iface)")
|
||||||
|
|
||||||
connectionTracker.onClientConnected(peerId, sock)
|
connectionTracker.onClientConnected(peerId, sock)
|
||||||
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||||
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||||
listenerExec.execute { listenToPeer(sock, peerId) }
|
listenerExec.execute { listenToPeer(sock, peerId) }
|
||||||
handleServerKeepAlive(sock, peerId, peerHandle)
|
handleServerKeepAlive(sock, peerId, peerHandle)
|
||||||
// Kick off Noise handshake for this logical peer
|
// Kick off Noise handshake for this logical peer
|
||||||
if (myPeerID < peerId) {
|
if (myPeerID < peerId) {
|
||||||
messageHandler.delegate?.initiateNoiseHandshake(peerId)
|
meshCore.initiateNoiseHandshake(peerId)
|
||||||
Log.d(TAG, "CLIENT: initiating Noise handshake to ${peerId.take(8)} (lower ID)")
|
Log.d(TAG, "CLIENT: initiating Noise handshake to ${peerId.take(8)} (lower ID)")
|
||||||
}
|
}
|
||||||
// Ensure fast presence even before handshake settles
|
// 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.
|
// Note: We do NOT update peerSockets mapping based on senderPeerHex.
|
||||||
// The socket belongs to initialLogicalPeerId effectively serving as the "MAC address" layer.
|
// 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})")
|
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.
|
// 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")
|
Log.d(TAG, "Cleaning up peer: $initialId")
|
||||||
|
|
||||||
connectionTracker.disconnect(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 mentions Optional list of mentioned peer IDs
|
||||||
* @param channel Optional channel name
|
* @param channel Optional channel name
|
||||||
*/
|
*/
|
||||||
fun sendMessage(content: String, mentions: List<String> = emptyList(), channel: String? = null) {
|
override fun sendMessage(content: String, mentions: List<String>, channel: String?) {
|
||||||
if (content.isEmpty()) return
|
meshCore.sendMessage(content, mentions, channel)
|
||||||
|
|
||||||
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) { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -960,37 +692,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* @param recipientNickname Recipient nickname
|
* @param recipientNickname Recipient nickname
|
||||||
* @param messageID Optional message ID (UUID if null)
|
* @param messageID Optional message ID (UUID if null)
|
||||||
*/
|
*/
|
||||||
fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String? = null) {
|
override fun sendPrivateMessage(content: String, recipientPeerID: String, recipientNickname: String, messageID: String?) {
|
||||||
if (content.isEmpty() || recipientPeerID.isEmpty()) return
|
meshCore.sendPrivateMessage(content, recipientPeerID, recipientNickname, messageID)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1001,29 +704,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* @param recipientPeerID The peer to notify.
|
* @param recipientPeerID The peer to notify.
|
||||||
* @param readerNickname Nickname of the reader (may be shown by the receiver).
|
* @param readerNickname Nickname of the reader (may be shown by the receiver).
|
||||||
*/
|
*/
|
||||||
fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
override fun sendReadReceipt(messageID: String, recipientPeerID: String, readerNickname: String) {
|
||||||
serviceScope.launch {
|
meshCore.sendReadReceipt(messageID, recipientPeerID, readerNickname)
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1032,28 +714,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
*
|
*
|
||||||
* @param file Encoded metadata and chunks descriptor of the file to send.
|
* @param file Encoded metadata and chunks descriptor of the file to send.
|
||||||
*/
|
*/
|
||||||
fun sendFileBroadcast(file: BitchatFilePacket) {
|
override fun sendFileBroadcast(file: BitchatFilePacket) {
|
||||||
try {
|
meshCore.sendFileBroadcast(file)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1063,33 +725,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* @param recipientPeerID Target peer.
|
* @param recipientPeerID Target peer.
|
||||||
* @param file Encoded metadata and chunks descriptor of the file to send.
|
* @param file Encoded metadata and chunks descriptor of the file to send.
|
||||||
*/
|
*/
|
||||||
fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) {
|
override fun sendFilePrivate(recipientPeerID: String, file: BitchatFilePacket) {
|
||||||
try {
|
meshCore.sendFilePrivate(recipientPeerID, file)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1098,116 +735,57 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* @param transferId Deterministic id (usually sha256 of the file TLV).
|
* @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.
|
* @return true if a transfer with this id was found and cancellation was scheduled, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun cancelFileTransfer(transferId: String): Boolean {
|
override fun cancelFileTransfer(transferId: String): Boolean {
|
||||||
return false
|
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.
|
* Broadcasts an ANNOUNCE packet to the entire mesh.
|
||||||
*/
|
*/
|
||||||
fun sendBroadcastAnnounce() {
|
override fun sendBroadcastAnnounce() {
|
||||||
serviceScope.launch {
|
meshCore.sendBroadcastAnnounce()
|
||||||
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) { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an ANNOUNCE packet to a specific peer.
|
* Sends an ANNOUNCE packet to a specific peer.
|
||||||
*/
|
*/
|
||||||
fun sendAnnouncementToPeer(peerID: String) {
|
override fun sendAnnouncementToPeer(peerID: String) {
|
||||||
if (peerManager.hasAnnouncedToPeer(peerID)) return
|
meshCore.sendAnnouncementToPeer(peerID)
|
||||||
|
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Mapping of peer IDs to nicknames. */
|
/** @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. */
|
/** @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.
|
* @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).
|
* @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.
|
* 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.
|
* @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.
|
* 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.
|
* 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.
|
* @param isVerified Whether this identity is verified by the user.
|
||||||
* @return true if the record was updated or created, false otherwise.
|
* @return true if the record was updated or created, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun updatePeerInfo(
|
override fun updatePeerInfo(
|
||||||
peerID: String,
|
peerID: String,
|
||||||
nickname: String,
|
nickname: String,
|
||||||
noisePublicKey: ByteArray,
|
noisePublicKey: ByteArray,
|
||||||
signingPublicKey: ByteArray,
|
signingPublicKey: ByteArray,
|
||||||
isVerified: Boolean
|
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).
|
* @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.
|
* @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.
|
* @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.
|
* @return the current IPv4/IPv6 address of a connected peer, if any.
|
||||||
* Prefers the scoped IPv6 address format.
|
* Prefers the scoped IPv6 address format.
|
||||||
*/
|
*/
|
||||||
fun getDeviceAddressForPeer(peerID: String): String? =
|
override fun getDeviceAddressForPeer(peerID: String): String? =
|
||||||
connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) }
|
meshCore.getDeviceAddressForPeer(peerID)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to resolve a scoped IPv6 address from a socket for UI display.
|
* 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.
|
* @return a mapping of peerID → connected device IP address for all active sockets.
|
||||||
* Results are formatted as scoped addresses if applicable.
|
* Results are formatted as scoped addresses if applicable.
|
||||||
*/
|
*/
|
||||||
fun getDeviceAddressToPeerMapping(): Map<String, String> {
|
override fun getDeviceAddressToPeerMapping(): Map<String, String> =
|
||||||
val map = mutableMapOf<String, String>()
|
meshCore.getDeviceAddressToPeerMapping()
|
||||||
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
|
||||||
map[pid] = resolveScopedAddress(sock) ?: "unknown"
|
|
||||||
}
|
|
||||||
return map
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return map of peer ID to nickname, bridged for UI warning fix.
|
* @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). */
|
/** Returns recently discovered peer IDs via Aware discovery (may not be connected). */
|
||||||
fun getDiscoveredPeerIds(): Set<String> =
|
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.
|
* 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}" }
|
getDeviceAddressToPeerMapping().entries.joinToString("\n") { "${it.key} -> ${it.value}" }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return A detailed string containing the debug status of all mesh components.
|
* @return A detailed string containing the debug status of all mesh components.
|
||||||
*/
|
*/
|
||||||
fun getDebugStatus(): String = buildString {
|
override fun getDebugStatus(): String {
|
||||||
appendLine("=== Wi-Fi Aware Mesh Debug Status ===")
|
return meshCore.getDebugStatus(
|
||||||
appendLine("My Peer ID: $myPeerID")
|
transportInfo = connectionTracker.getDebugInfo(),
|
||||||
appendLine("Peers: ${connectionTracker.peerSockets.keys}")
|
deviceMap = getDeviceAddressToPeerMapping(),
|
||||||
appendLine(connectionTracker.getDebugInfo())
|
extraLines = listOf("Peers: ${connectionTracker.peerSockets.keys}"),
|
||||||
appendLine(peerManager.getDebugInfo(getDeviceAddressToPeerMapping()))
|
title = "Wi-Fi Aware Mesh Debug Status"
|
||||||
appendLine(fragmentManager.getDebugInfo())
|
)
|
||||||
appendLine(securityManager.getDebugInfo())
|
}
|
||||||
appendLine(storeForwardManager.getDebugInfo())
|
|
||||||
appendLine(messageHandler.getDebugInfo())
|
override fun clearAllInternalData() {
|
||||||
appendLine(packetProcessor.getDebugInfo())
|
meshCore.clearAllInternalData()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clearAllEncryptionData() {
|
||||||
|
meshCore.clearAllEncryptionData()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Utility extension to safely close sockets. */
|
/** Utility extension to safely close sockets. */
|
||||||
@@ -1313,20 +890,32 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
/** Utility extension to safely close server sockets. */
|
/** Utility extension to safely close server sockets. */
|
||||||
private fun ServerSocket.closeQuietly() = try { close() } catch (_: Exception) {}
|
private fun ServerSocket.closeQuietly() = try { close() } catch (_: Exception) {}
|
||||||
}
|
|
||||||
|
|
||||||
|
private inner class WifiAwareTransport : MeshTransport {
|
||||||
|
override val id: String = "WIFI"
|
||||||
|
|
||||||
/**
|
override fun broadcastPacket(routed: RoutedPacket) {
|
||||||
* Delegate interface for mesh service callbacks (maintains exact same interface)
|
this@WifiAwareMeshService.broadcastPacket(routed)
|
||||||
*/
|
}
|
||||||
interface WifiAwareMeshDelegate {
|
|
||||||
fun didReceiveMessage(message: BitchatMessage)
|
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||||
fun didUpdatePeerList(peers: List<String>)
|
this@WifiAwareMeshService.sendPacketToPeer(peerID, packet)
|
||||||
fun didReceiveChannelLeave(channel: String, fromPeer: String)
|
}
|
||||||
fun didReceiveDeliveryAck(messageID: String, recipientPeerID: String)
|
|
||||||
fun didReceiveReadReceipt(messageID: String, recipientPeerID: String)
|
override fun getDeviceAddressForPeer(peerID: String): String? {
|
||||||
fun decryptChannelMessage(encryptedContent: ByteArray, channel: String): String?
|
return connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) }
|
||||||
fun getNickname(): String?
|
}
|
||||||
fun isFavorite(peerID: String): Boolean
|
|
||||||
// registerPeerPublicKey REMOVED - fingerprints now handled centrally in PeerManager
|
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