mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
handshake works
This commit is contained in:
@@ -3,11 +3,12 @@ package com.bitchat.android.services
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
|
import com.bitchat.android.mesh.MeshService
|
||||||
import com.bitchat.android.model.ReadReceipt
|
import com.bitchat.android.model.ReadReceipt
|
||||||
import com.bitchat.android.nostr.NostrTransport
|
import com.bitchat.android.nostr.NostrTransport
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes messages between BLE mesh and Nostr transports, matching iOS behavior.
|
* Routes messages between local mesh transports and Nostr, matching iOS behavior.
|
||||||
*/
|
*/
|
||||||
class MessageRouter private constructor(
|
class MessageRouter private constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@@ -70,15 +71,13 @@ class MessageRouter private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val hasMesh = mesh.getPeerInfo(toPeerID)?.isConnected == true
|
val aware = getAwareService()
|
||||||
val hasEstablished = mesh.hasEstablishedSession(toPeerID)
|
val hasMesh = isConnected(mesh, toPeerID)
|
||||||
// Check Wi‑Fi Aware availability as a secondary transport
|
val hasAwareConnection = isConnected(aware, toPeerID)
|
||||||
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
if (isReady(mesh, toPeerID)) {
|
||||||
val hasAware = try { aware?.getPeerInfo(toPeerID)?.isConnected == true && aware.hasEstablishedSession(toPeerID) } catch (_: Exception) { false }
|
|
||||||
if (hasMesh && hasEstablished) {
|
|
||||||
Log.d(TAG, "Routing PM via mesh to ${toPeerID} msg_id=${messageID.take(8)}…")
|
Log.d(TAG, "Routing PM via mesh to ${toPeerID} msg_id=${messageID.take(8)}…")
|
||||||
mesh.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
mesh.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
||||||
} else if (hasAware) {
|
} else if (isReady(aware, toPeerID)) {
|
||||||
Log.d(TAG, "Routing PM via Wi‑Fi Aware to ${toPeerID} msg_id=${messageID.take(8)}…")
|
Log.d(TAG, "Routing PM via Wi‑Fi Aware to ${toPeerID} msg_id=${messageID.take(8)}…")
|
||||||
aware?.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
aware?.sendPrivateMessage(content, toPeerID, recipientNickname, messageID)
|
||||||
} else if (canSendViaNostr(toPeerID)) {
|
} else if (canSendViaNostr(toPeerID)) {
|
||||||
@@ -89,18 +88,20 @@ class MessageRouter private constructor(
|
|||||||
val q = outbox.getOrPut(toPeerID) { mutableListOf() }
|
val q = outbox.getOrPut(toPeerID) { mutableListOf() }
|
||||||
q.add(Triple(content, recipientNickname, messageID))
|
q.add(Triple(content, recipientNickname, messageID))
|
||||||
Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…")
|
Log.d(TAG, "Initiating noise handshake after queueing PM for ${toPeerID.take(8)}…")
|
||||||
if (hasMesh) mesh.initiateNoiseHandshake(toPeerID) else aware?.initiateNoiseHandshake(toPeerID)
|
when {
|
||||||
|
hasMesh -> mesh.initiateNoiseHandshake(toPeerID)
|
||||||
|
hasAwareConnection -> aware?.initiateNoiseHandshake(toPeerID)
|
||||||
|
else -> aware?.initiateNoiseHandshake(toPeerID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) {
|
fun sendReadReceipt(receipt: ReadReceipt, toPeerID: String) {
|
||||||
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
val aware = getAwareService()
|
||||||
val viaMesh = (mesh.getPeerInfo(toPeerID)?.isConnected == true) && mesh.hasEstablishedSession(toPeerID)
|
if (isReady(mesh, toPeerID)) {
|
||||||
val viaAware = try { aware?.getPeerInfo(toPeerID)?.isConnected == true && aware.hasEstablishedSession(toPeerID) } catch (_: Exception) { false }
|
|
||||||
if (viaMesh) {
|
|
||||||
Log.d(TAG, "Routing READ via mesh to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
Log.d(TAG, "Routing READ via mesh to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
||||||
mesh.sendReadReceipt(receipt.originalMessageID, toPeerID, mesh.getPeerNicknames()[toPeerID] ?: mesh.myPeerID)
|
mesh.sendReadReceipt(receipt.originalMessageID, toPeerID, mesh.getPeerNicknames()[toPeerID] ?: mesh.myPeerID)
|
||||||
} else if (viaAware) {
|
} else if (isReady(aware, toPeerID)) {
|
||||||
Log.d(TAG, "Routing READ via Wi‑Fi Aware to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
Log.d(TAG, "Routing READ via Wi‑Fi Aware to ${toPeerID.take(8)}… id=${receipt.originalMessageID.take(8)}…")
|
||||||
val me = try { aware?.myPeerID } catch (_: Exception) { null }
|
val me = try { aware?.myPeerID } catch (_: Exception) { null }
|
||||||
aware?.sendReadReceipt(receipt.originalMessageID, toPeerID, me ?: "")
|
aware?.sendReadReceipt(receipt.originalMessageID, toPeerID, me ?: "")
|
||||||
@@ -141,23 +142,34 @@ class MessageRouter private constructor(
|
|||||||
val queued = outbox[peerID] ?: return
|
val queued = outbox[peerID] ?: return
|
||||||
if (queued.isEmpty()) return
|
if (queued.isEmpty()) return
|
||||||
Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}")
|
Log.d(TAG, "Flushing outbox for ${peerID.take(8)}… count=${queued.size}")
|
||||||
|
val aware = getAwareService()
|
||||||
val iterator = queued.iterator()
|
val iterator = queued.iterator()
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
val (content, nickname, messageID) = iterator.next()
|
val (content, nickname, messageID) = iterator.next()
|
||||||
var hasMesh = mesh.getPeerInfo(peerID)?.isConnected == true && mesh.hasEstablishedSession(peerID)
|
val hasMesh = isReady(mesh, peerID)
|
||||||
|
val hasAware = isReady(aware, peerID)
|
||||||
// If this is a noiseHex key, see if there is a connected mesh peer for this identity
|
// If this is a noiseHex key, see if there is a connected mesh peer for this identity
|
||||||
if (!hasMesh && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
if (!hasMesh && !hasAware && peerID.length == 64 && peerID.matches(Regex("^[0-9a-fA-F]+$"))) {
|
||||||
val meshPeer = resolveMeshPeerForNoiseHex(peerID)
|
val meshPeer = resolvePeerForNoiseHex(peerID, mesh)
|
||||||
if (meshPeer != null && mesh.getPeerInfo(meshPeer)?.isConnected == true && mesh.hasEstablishedSession(meshPeer)) {
|
if (meshPeer != null && isReady(mesh, meshPeer)) {
|
||||||
mesh.sendPrivateMessage(content, meshPeer, nickname, messageID)
|
mesh.sendPrivateMessage(content, meshPeer, nickname, messageID)
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
val awarePeer = resolvePeerForNoiseHex(peerID, aware)
|
||||||
|
if (awarePeer != null && isReady(aware, awarePeer)) {
|
||||||
|
aware?.sendPrivateMessage(content, awarePeer, nickname, messageID)
|
||||||
|
iterator.remove()
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val canNostr = canSendViaNostr(peerID)
|
val canNostr = canSendViaNostr(peerID)
|
||||||
if (hasMesh) {
|
if (hasMesh) {
|
||||||
mesh.sendPrivateMessage(content, peerID, nickname, messageID)
|
mesh.sendPrivateMessage(content, peerID, nickname, messageID)
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
|
} else if (hasAware) {
|
||||||
|
aware?.sendPrivateMessage(content, peerID, nickname, messageID)
|
||||||
|
iterator.remove()
|
||||||
} else if (canNostr) {
|
} else if (canNostr) {
|
||||||
nostr.sendPrivateMessage(content, peerID, nickname, messageID)
|
nostr.sendPrivateMessage(content, peerID, nickname, messageID)
|
||||||
iterator.remove()
|
iterator.remove()
|
||||||
@@ -195,10 +207,60 @@ class MessageRouter private constructor(
|
|||||||
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveMeshPeerForNoiseHex(noiseHex: String): String? {
|
private fun getAwareService(): MeshService? {
|
||||||
|
return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConnected(service: BluetoothMeshService, peerID: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
mesh.getPeerNicknames().keys.firstOrNull { pid ->
|
service.getPeerInfo(peerID)?.isConnected == true
|
||||||
val info = mesh.getPeerInfo(pid)
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConnected(service: MeshService?, peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
service != null && service.getPeerInfo(peerID)?.isConnected == true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isReady(service: BluetoothMeshService, peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
service.getPeerInfo(peerID)?.isConnected == true &&
|
||||||
|
service.hasEstablishedSession(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isReady(service: MeshService?, peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
service != null &&
|
||||||
|
service.getPeerInfo(peerID)?.isConnected == true &&
|
||||||
|
service.hasEstablishedSession(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePeerForNoiseHex(noiseHex: String, service: BluetoothMeshService): String? {
|
||||||
|
return try {
|
||||||
|
service.getPeerNicknames().keys.firstOrNull { pid ->
|
||||||
|
val info = service.getPeerInfo(pid)
|
||||||
|
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
|
keyHex != null && keyHex.equals(noiseHex, ignoreCase = true)
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolvePeerForNoiseHex(noiseHex: String, service: MeshService?): String? {
|
||||||
|
return try {
|
||||||
|
if (service == null) return null
|
||||||
|
service.getPeerNicknames().keys.firstOrNull { pid ->
|
||||||
|
val info = service.getPeerInfo(pid)
|
||||||
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
val keyHex = info?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
keyHex != null && keyHex.equals(noiseHex, ignoreCase = true)
|
keyHex != null && keyHex.equals(noiseHex, ignoreCase = true)
|
||||||
}
|
}
|
||||||
@@ -213,6 +275,10 @@ class MessageRouter private constructor(
|
|||||||
mesh.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
mesh.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
noiseHex?.let { flushOutboxFor(it) }
|
noiseHex?.let { flushOutboxFor(it) }
|
||||||
|
val awareNoiseHex = try {
|
||||||
|
getAwareService()?.getPeerInfo(pid)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
awareNoiseHex?.let { flushOutboxFor(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,5 +289,9 @@ class MessageRouter private constructor(
|
|||||||
mesh.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
mesh.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
} catch (_: Exception) { null }
|
} catch (_: Exception) { null }
|
||||||
noiseHex?.let { flushOutboxFor(it) }
|
noiseHex?.let { flushOutboxFor(it) }
|
||||||
|
val awareNoiseHex = try {
|
||||||
|
getAwareService()?.getPeerInfo(peerID)?.noisePublicKey?.joinToString("") { b -> "%02x".format(b) }
|
||||||
|
} catch (_: Exception) { null }
|
||||||
|
awareNoiseHex?.let { flushOutboxFor(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ class ChatViewModel(
|
|||||||
|
|
||||||
// Create Noise session delegate for clean dependency injection
|
// Create Noise session delegate for clean dependency injection
|
||||||
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
private val noiseSessionDelegate = object : NoiseSessionDelegate {
|
||||||
override fun hasEstablishedSession(peerID: String): Boolean = meshService.hasEstablishedSession(peerID)
|
override fun hasEstablishedSession(peerID: String): Boolean = hasEstablishedSessionOnAnyLocalTransport(peerID)
|
||||||
override fun initiateHandshake(peerID: String) = meshService.initiateNoiseHandshake(peerID)
|
override fun initiateHandshake(peerID: String) = initiateNoiseHandshakeOnBestLocalTransport(peerID)
|
||||||
override fun getMyPeerID(): String = meshService.myPeerID
|
override fun getMyPeerID(): String = meshService.myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,7 +490,7 @@ class ChatViewModel(
|
|||||||
meshService.sendMessage(messageContent, mentions, channel)
|
meshService.sendMessage(messageContent, mentions, channel)
|
||||||
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {}
|
try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.sendMessage(messageContent, mentions, channel) } catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
})
|
}, this)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +520,7 @@ class ChatViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send private message
|
// Send private message
|
||||||
val recipientNickname = meshService.getPeerNicknames()[selectedPeer]
|
val recipientNickname = nicknameForPeer(selectedPeer)
|
||||||
privateChatManager.sendPrivateMessage(
|
privateChatManager.sendPrivateMessage(
|
||||||
content,
|
content,
|
||||||
selectedPeer,
|
selectedPeer,
|
||||||
@@ -661,6 +661,79 @@ class ChatViewModel(
|
|||||||
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
|
Log.i("ChatViewModel", "Peer fingerprints: ${privateChatManager.getAllPeerFingerprints()}")
|
||||||
Log.i("ChatViewModel", "==============================")
|
Log.i("ChatViewModel", "==============================")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getWifiAwareService(): com.bitchat.android.mesh.MeshService? {
|
||||||
|
return try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConnectedOnMesh(peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
meshService.getPeerInfo(peerID)?.isConnected == true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isConnectedOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
service != null && service.getPeerInfo(peerID)?.isConnected == true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasEstablishedSessionOnMesh(peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
meshService.getPeerInfo(peerID)?.isConnected == true &&
|
||||||
|
meshService.hasEstablishedSession(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasEstablishedSessionOnService(service: com.bitchat.android.mesh.MeshService?, peerID: String): Boolean {
|
||||||
|
return try {
|
||||||
|
service != null &&
|
||||||
|
service.getPeerInfo(peerID)?.isConnected == true &&
|
||||||
|
service.hasEstablishedSession(peerID)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasEstablishedSessionOnAnyLocalTransport(peerID: String): Boolean {
|
||||||
|
return hasEstablishedSessionOnMesh(peerID) ||
|
||||||
|
hasEstablishedSessionOnService(getWifiAwareService(), peerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initiateNoiseHandshakeOnBestLocalTransport(peerID: String) {
|
||||||
|
val aware = getWifiAwareService()
|
||||||
|
when {
|
||||||
|
isConnectedOnMesh(peerID) -> meshService.initiateNoiseHandshake(peerID)
|
||||||
|
isConnectedOnService(aware, peerID) -> aware?.initiateNoiseHandshake(peerID)
|
||||||
|
else -> meshService.initiateNoiseHandshake(peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nicknameForPeer(peerID: String): String? {
|
||||||
|
return state.peerNicknames.value[peerID]
|
||||||
|
?: try { meshService.getPeerNicknames()[peerID] } catch (_: Exception) { null }
|
||||||
|
?: try { getWifiAwareService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sessionStateForPeer(peerID: String): NoiseSession.NoiseSessionState {
|
||||||
|
val meshState = try { meshService.getSessionState(peerID) } catch (_: Exception) { NoiseSession.NoiseSessionState.Uninitialized }
|
||||||
|
val awareState = try { getWifiAwareService()?.getSessionState(peerID) } catch (_: Exception) { null }
|
||||||
|
return when {
|
||||||
|
meshState is NoiseSession.NoiseSessionState.Established -> meshState
|
||||||
|
awareState is NoiseSession.NoiseSessionState.Established -> awareState
|
||||||
|
meshState is NoiseSession.NoiseSessionState.Handshaking -> meshState
|
||||||
|
awareState is NoiseSession.NoiseSessionState.Handshaking -> awareState
|
||||||
|
meshState !is NoiseSession.NoiseSessionState.Uninitialized -> meshState
|
||||||
|
awareState != null -> awareState
|
||||||
|
else -> meshState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize session state monitoring for reactive UI updates
|
* Initialize session state monitoring for reactive UI updates
|
||||||
@@ -685,7 +758,7 @@ class ChatViewModel(
|
|||||||
// Update session states
|
// Update session states
|
||||||
val prevStates = state.getPeerSessionStatesValue()
|
val prevStates = state.getPeerSessionStatesValue()
|
||||||
val sessionStates = currentPeers.associateWith { peerID ->
|
val sessionStates = currentPeers.associateWith { peerID ->
|
||||||
meshService.getSessionState(peerID).toString()
|
sessionStateForPeer(peerID).toString()
|
||||||
}
|
}
|
||||||
state.setPeerSessionStates(sessionStates)
|
state.setPeerSessionStates(sessionStates)
|
||||||
// Detect new established sessions and flush router outbox for them and their noiseHex aliases
|
// Detect new established sessions and flush router outbox for them and their noiseHex aliases
|
||||||
@@ -736,7 +809,7 @@ class ChatViewModel(
|
|||||||
|
|
||||||
// Flush any pending QR verification once a Noise session is established
|
// Flush any pending QR verification once a Noise session is established
|
||||||
currentPeers.forEach { peerID ->
|
currentPeers.forEach { peerID ->
|
||||||
if (meshService.getSessionState(peerID) is NoiseSession.NoiseSessionState.Established) {
|
if (sessionStateForPeer(peerID) is NoiseSession.NoiseSessionState.Established) {
|
||||||
verificationHandler.sendPendingVerificationIfNeeded(peerID)
|
verificationHandler.sendPendingVerificationIfNeeded(peerID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ class CommandProcessor(
|
|||||||
val cmd = parts.first().lowercase()
|
val cmd = parts.first().lowercase()
|
||||||
when (cmd) {
|
when (cmd) {
|
||||||
"/j", "/join" -> handleJoinCommand(parts, myPeerID)
|
"/j", "/join" -> handleJoinCommand(parts, myPeerID)
|
||||||
"/m", "/msg" -> handleMessageCommand(parts, meshService)
|
"/m", "/msg" -> handleMessageCommand(parts, meshService, viewModel)
|
||||||
"/w" -> handleWhoCommand(meshService, viewModel)
|
"/w" -> handleWhoCommand(meshService, viewModel)
|
||||||
"/clear" -> handleClearCommand()
|
"/clear" -> handleClearCommand()
|
||||||
"/pass" -> handlePassCommand(parts, myPeerID)
|
"/pass" -> handlePassCommand(parts, myPeerID)
|
||||||
"/block" -> handleBlockCommand(parts, meshService)
|
"/block" -> handleBlockCommand(parts, meshService)
|
||||||
"/unblock" -> handleUnblockCommand(parts, meshService)
|
"/unblock" -> handleUnblockCommand(parts, meshService)
|
||||||
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage)
|
"/hug" -> handleActionCommand(parts, "gives", "a warm hug 🫂", meshService, myPeerID, onSendMessage, viewModel)
|
||||||
"/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage)
|
"/slap" -> handleActionCommand(parts, "slaps", "around a bit with a large trout 🐟", meshService, myPeerID, onSendMessage, viewModel)
|
||||||
"/channels" -> handleChannelsCommand()
|
"/channels" -> handleChannelsCommand()
|
||||||
else -> handleUnknownCommand(cmd)
|
else -> handleUnknownCommand(cmd)
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ class CommandProcessor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService) {
|
private fun handleMessageCommand(parts: List<String>, meshService: BluetoothMeshService, viewModel: ChatViewModel?) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
val peerID = getPeerIDForNickname(targetName, meshService)
|
val peerID = getPeerIDForNickname(targetName, meshService)
|
||||||
@@ -96,8 +96,7 @@ class CommandProcessor(
|
|||||||
state.getNicknameValue(),
|
state.getNicknameValue(),
|
||||||
getMyPeerID(meshService)
|
getMyPeerID(meshService)
|
||||||
) { content, peerIdParam, recipientNicknameParam, messageId ->
|
) { content, peerIdParam, recipientNicknameParam, messageId ->
|
||||||
// This would trigger the actual mesh service send
|
sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId, viewModel)
|
||||||
sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val systemMessage = BitchatMessage(
|
val systemMessage = BitchatMessage(
|
||||||
@@ -286,7 +285,8 @@ class CommandProcessor(
|
|||||||
object_: String,
|
object_: String,
|
||||||
meshService: BluetoothMeshService,
|
meshService: BluetoothMeshService,
|
||||||
myPeerID: String,
|
myPeerID: String,
|
||||||
onSendMessage: (String, List<String>, String?) -> Unit
|
onSendMessage: (String, List<String>, String?) -> Unit,
|
||||||
|
viewModel: ChatViewModel?
|
||||||
) {
|
) {
|
||||||
if (parts.size > 1) {
|
if (parts.size > 1) {
|
||||||
val targetName = parts[1].removePrefix("@")
|
val targetName = parts[1].removePrefix("@")
|
||||||
@@ -306,7 +306,7 @@ class CommandProcessor(
|
|||||||
state.getNicknameValue(),
|
state.getNicknameValue(),
|
||||||
myPeerID
|
myPeerID
|
||||||
) { content, peerIdParam, recipientNicknameParam, messageId ->
|
) { content, peerIdParam, recipientNicknameParam, messageId ->
|
||||||
sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId)
|
sendPrivateMessageVia(meshService, content, peerIdParam, recipientNicknameParam, messageId, viewModel)
|
||||||
}
|
}
|
||||||
} else if (isInLocationChannel) {
|
} else if (isInLocationChannel) {
|
||||||
// Let the transport layer add the echo; just send it out
|
// Let the transport layer add the echo; just send it out
|
||||||
@@ -505,17 +505,41 @@ class CommandProcessor(
|
|||||||
|
|
||||||
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
private fun getPeerIDForNickname(nickname: String, meshService: BluetoothMeshService): String? {
|
||||||
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
return meshService.getPeerNicknames().entries.find { it.value == nickname }?.key
|
||||||
|
?: try {
|
||||||
|
com.bitchat.android.wifiaware.WifiAwareController.getService()
|
||||||
|
?.getPeerNicknames()
|
||||||
|
?.entries
|
||||||
|
?.find { it.value == nickname }
|
||||||
|
?.key
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
private fun getPeerNickname(peerID: String, meshService: BluetoothMeshService): String {
|
||||||
return meshService.getPeerNicknames()[peerID] ?: peerID
|
return meshService.getPeerNicknames()[peerID]
|
||||||
|
?: try { com.bitchat.android.wifiaware.WifiAwareController.getService()?.getPeerNicknames()?.get(peerID) } catch (_: Exception) { null }
|
||||||
|
?: peerID
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMyPeerID(meshService: BluetoothMeshService): String {
|
private fun getMyPeerID(meshService: BluetoothMeshService): String {
|
||||||
return meshService.myPeerID
|
return meshService.myPeerID
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendPrivateMessageVia(meshService: BluetoothMeshService, content: String, peerID: String, recipientNickname: String, messageId: String) {
|
private fun sendPrivateMessageVia(
|
||||||
meshService.sendPrivateMessage(content, peerID, recipientNickname, messageId)
|
meshService: BluetoothMeshService,
|
||||||
|
content: String,
|
||||||
|
peerID: String,
|
||||||
|
recipientNickname: String,
|
||||||
|
messageId: String,
|
||||||
|
viewModel: ChatViewModel?
|
||||||
|
) {
|
||||||
|
if (viewModel != null) {
|
||||||
|
com.bitchat.android.services.MessageRouter
|
||||||
|
.getInstance(viewModel.getApplication(), meshService)
|
||||||
|
.sendPrivate(content, peerID, recipientNickname, messageId)
|
||||||
|
} else {
|
||||||
|
meshService.sendPrivateMessage(content, peerID, recipientNickname, messageId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
private val connectionTracker = WifiAwareConnectionTracker(serviceScope, cm)
|
private val connectionTracker = WifiAwareConnectionTracker(serviceScope, cm)
|
||||||
private val handleToPeerId = ConcurrentHashMap<PeerHandle, String>() // discovery mapping
|
private val handleToPeerId = ConcurrentHashMap<PeerHandle, String>() // discovery mapping
|
||||||
private val discoveredTimestamps = ConcurrentHashMap<String, Long>() // peerID -> last seen time
|
private val discoveredTimestamps = ConcurrentHashMap<String, Long>() // peerID -> last seen time
|
||||||
|
// Subscribe-session-scoped handles only. PeerHandles are session-scoped, so a handle obtained
|
||||||
|
// from the publish session is NOT valid for subscribeSession.sendMessage(). Maintenance re-pings
|
||||||
|
// (subscriber -> publisher) must use a handle that originated from the subscribe session.
|
||||||
|
private val subscribeHandles = ConcurrentHashMap<String, PeerHandle>() // peerID -> latest subscribe handle
|
||||||
|
|
||||||
fun isRunning(): Boolean = isActive
|
fun isRunning(): Boolean = isActive
|
||||||
|
|
||||||
@@ -121,6 +125,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
// This avoids race conditions and ensures a single gossip source/delegate
|
// This avoids race conditions and ensures a single gossip source/delegate
|
||||||
com.bitchat.android.service.MeshServiceHolder.getOrCreate(context)
|
com.bitchat.android.service.MeshServiceHolder.getOrCreate(context)
|
||||||
val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager
|
val shared = com.bitchat.android.service.MeshServiceHolder.sharedGossipSyncManager
|
||||||
|
encryptionService.onSessionEstablished = { peerID ->
|
||||||
|
Log.d(TAG, "Wi-Fi Aware Noise session established with ${peerID.take(8)}")
|
||||||
|
try {
|
||||||
|
com.bitchat.android.services.MessageRouter
|
||||||
|
.tryGetInstance()
|
||||||
|
?.onSessionEstablished(peerID)
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
meshCore = MeshCore(
|
meshCore = MeshCore(
|
||||||
context = context.applicationContext,
|
context = context.applicationContext,
|
||||||
scope = serviceScope,
|
scope = serviceScope,
|
||||||
@@ -414,6 +426,9 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (!isCurrentSession(generation)) return
|
if (!isCurrentSession(generation)) return
|
||||||
val peerId = try { String(serviceSpecificInfo) } catch (_: Exception) { "" }
|
val peerId = try { String(serviceSpecificInfo) } catch (_: Exception) { "" }
|
||||||
handleToPeerId[peerHandle] = peerId
|
handleToPeerId[peerHandle] = peerId
|
||||||
|
// This handle came from the subscribe session, so it is valid for
|
||||||
|
// subscribeSession.sendMessage() (used by maintenance reconnection).
|
||||||
|
if (peerId.isNotBlank()) subscribeHandles[peerId] = peerHandle
|
||||||
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
||||||
subscribeSession?.sendMessage(peerHandle, msgId, myPeerID.toByteArray())
|
subscribeSession?.sendMessage(peerHandle, msgId, myPeerID.toByteArray())
|
||||||
if (peerId.isNotBlank()) discoveredTimestamps[peerId] = System.currentTimeMillis()
|
if (peerId.isNotBlank()) discoveredTimestamps[peerId] = System.currentTimeMillis()
|
||||||
@@ -507,6 +522,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
wifiAwareSession?.close(); wifiAwareSession = null
|
wifiAwareSession?.close(); wifiAwareSession = null
|
||||||
|
|
||||||
handleToPeerId.clear()
|
handleToPeerId.clear()
|
||||||
|
subscribeHandles.clear()
|
||||||
discoveredTimestamps.clear()
|
discoveredTimestamps.clear()
|
||||||
|
|
||||||
meshCore.shutdown()
|
meshCore.shutdown()
|
||||||
@@ -548,6 +564,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (subscribeSession === oldSubscribeSession) subscribeSession = null
|
if (subscribeSession === oldSubscribeSession) subscribeSession = null
|
||||||
if (wifiAwareSession === oldWifiAwareSession) wifiAwareSession = null
|
if (wifiAwareSession === oldWifiAwareSession) wifiAwareSession = null
|
||||||
handleToPeerId.clear()
|
handleToPeerId.clear()
|
||||||
|
subscribeHandles.clear()
|
||||||
discoveredTimestamps.clear()
|
discoveredTimestamps.clear()
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -582,6 +599,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
if (staleIds.isNotEmpty()) {
|
if (staleIds.isNotEmpty()) {
|
||||||
staleIds.forEach { discoveredTimestamps.remove(it) }
|
staleIds.forEach { discoveredTimestamps.remove(it) }
|
||||||
handleToPeerId.entries.removeIf { it.value in staleIds }
|
handleToPeerId.entries.removeIf { it.value in staleIds }
|
||||||
|
staleIds.forEach { subscribeHandles.remove(it) }
|
||||||
Log.d(TAG, "Maintenance: pruned ${staleIds.size} stale discovery entries")
|
Log.d(TAG, "Maintenance: pruned ${staleIds.size} stale discovery entries")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,8 +615,15 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
|
|
||||||
// 3. Attempt reconnection
|
// 3. Attempt reconnection
|
||||||
for (peerId in disconnectedPeers) {
|
for (peerId in disconnectedPeers) {
|
||||||
// Find the PeerHandle for this peerId
|
// Reconnection is a subscriber -> publisher ping. It only succeeds when the
|
||||||
val handle = handleToPeerId.entries.find { it.value == peerId }?.key ?: continue
|
// remote is the server for this pair (i.e. we are the client). Re-pinging a
|
||||||
|
// peer we are the server for is dropped on their side, so skip it; that peer
|
||||||
|
// must re-ping us instead.
|
||||||
|
if (amIServerFor(peerId)) continue
|
||||||
|
|
||||||
|
// Use a subscribe-session-scoped handle. A publish-scoped handle would be
|
||||||
|
// invalid for subscribeSession.sendMessage() and silently fail.
|
||||||
|
val handle = subscribeHandles[peerId] ?: continue
|
||||||
|
|
||||||
// Check tracker policy
|
// Check tracker policy
|
||||||
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
||||||
@@ -635,6 +660,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val peerId = handleToPeerId[peerHandle] ?: return
|
val peerId = handleToPeerId[peerHandle] ?: return
|
||||||
if (!amIServerFor(peerId)) return
|
if (!amIServerFor(peerId)) return
|
||||||
|
|
||||||
|
if (connectionTracker.isConnected(peerId)) {
|
||||||
|
Log.v(TAG, "↪ already connected to $peerId, skipping serve")
|
||||||
|
return
|
||||||
|
}
|
||||||
if (connectionTracker.hasOpenServerSocket(peerId)) {
|
if (connectionTracker.hasOpenServerSocket(peerId)) {
|
||||||
Log.v(TAG, "↪ already serving $peerId, skipping")
|
Log.v(TAG, "↪ already serving $peerId, skipping")
|
||||||
return
|
return
|
||||||
@@ -690,6 +719,11 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
val synced = SyncedSocket(client)
|
val synced = SyncedSocket(client)
|
||||||
activeSocket = synced
|
activeSocket = synced
|
||||||
connectionTracker.onClientConnected(peerId, synced)
|
connectionTracker.onClientConnected(peerId, synced)
|
||||||
|
// We only ever accept a single data socket per server request. Close the
|
||||||
|
// listening ServerSocket now so it can't block a future re-serve (its
|
||||||
|
// presence makes hasOpenServerSocket() true for the life of the process)
|
||||||
|
// and so we free the fd/port promptly.
|
||||||
|
connectionTracker.closeServerSocket(peerId)
|
||||||
try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
try { meshCore.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||||
try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
try { meshCore.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||||
listenerExec.execute { listenToPeer(synced, peerId) }
|
listenerExec.execute { listenToPeer(synced, peerId) }
|
||||||
@@ -762,7 +796,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
try {
|
try {
|
||||||
while (connectionTracker.isConnected(peerId)) {
|
while (connectionTracker.isConnected(peerId)) {
|
||||||
// write empty byte array effectively sends [4 bytes length=0] which is our ping
|
// write empty byte array effectively sends [4 bytes length=0] which is our ping
|
||||||
try { client.write(ByteArray(0)) } catch (_: IOException) { break }
|
try {
|
||||||
|
client.write(ByteArray(0))
|
||||||
|
} catch (_: IOException) {
|
||||||
|
// The write side is dead. Don't just stop pinging: actively tear down so the
|
||||||
|
// half-open socket stops counting as "connected" and maintenance can retry.
|
||||||
|
handlePeerDisconnection(peerId, client)
|
||||||
|
break
|
||||||
|
}
|
||||||
delay(2_000)
|
delay(2_000)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
@@ -907,7 +948,14 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
while (connectionTracker.isConnected(peerId)) {
|
while (connectionTracker.isConnected(peerId)) {
|
||||||
try { sock.write(ByteArray(0)) } catch (_: IOException) { break }
|
try {
|
||||||
|
sock.write(ByteArray(0))
|
||||||
|
} catch (_: IOException) {
|
||||||
|
// The write side is dead. Tear down so the half-open socket stops counting
|
||||||
|
// as "connected" and maintenance can retry instead of silently stalling.
|
||||||
|
handlePeerDisconnection(peerId, sock)
|
||||||
|
break
|
||||||
|
}
|
||||||
delay(2_000)
|
delay(2_000)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
@@ -956,6 +1004,7 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
handleToPeerId[handle] = senderPeerHex
|
handleToPeerId[handle] = senderPeerHex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
subscribeHandles.remove(previousPeerId)?.let { subscribeHandles[senderPeerHex] = it }
|
||||||
discoveredTimestamps.remove(previousPeerId)
|
discoveredTimestamps.remove(previousPeerId)
|
||||||
discoveredTimestamps[senderPeerHex] = System.currentTimeMillis()
|
discoveredTimestamps[senderPeerHex] = System.currentTimeMillis()
|
||||||
try { meshCore.setDirectConnection(previousPeerId, false) } catch (_: Exception) { }
|
try { meshCore.setDirectConnection(previousPeerId, false) } catch (_: Exception) { }
|
||||||
|
|||||||
Reference in New Issue
Block a user