mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 21:05:24 +00:00
share gossip
This commit is contained in:
@@ -169,6 +169,10 @@ class BluetoothMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
meshCore.sendFromBridge(packet)
|
meshCore.sendFromBridge(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun sendToPeer(peerID: String, packet: BitchatPacket) {
|
||||||
|
connectionManager.sendPacketToPeer(peerID, packet)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start periodic debug logging every 10 seconds
|
* Start periodic debug logging every 10 seconds
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class MeshCore(
|
|||||||
|
|
||||||
val gossipSyncManager: GossipSyncManager =
|
val gossipSyncManager: GossipSyncManager =
|
||||||
sharedGossipManager ?: GossipSyncManager(myPeerID = myPeerID, scope = scope, configProvider = gossipConfigProvider)
|
sharedGossipManager ?: GossipSyncManager(myPeerID = myPeerID, scope = scope, configProvider = gossipConfigProvider)
|
||||||
|
private val ownsGossipManager: Boolean = sharedGossipManager == null
|
||||||
|
|
||||||
var delegate: MeshDelegate? = null
|
var delegate: MeshDelegate? = null
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ class MeshCore(
|
|||||||
|
|
||||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||||
transport.sendPacketToPeer(peerID, packet)
|
transport.sendPacketToPeer(peerID, packet)
|
||||||
|
TransportBridgeService.sendToPeer(transport.id, peerID, packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||||
@@ -87,16 +89,20 @@ class MeshCore(
|
|||||||
if (isActive) return
|
if (isActive) return
|
||||||
isActive = true
|
isActive = true
|
||||||
startPeriodicBroadcastAnnounce()
|
startPeriodicBroadcastAnnounce()
|
||||||
|
if (ownsGossipManager) {
|
||||||
gossipSyncManager.start()
|
gossipSyncManager.start()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun stopCore() {
|
fun stopCore() {
|
||||||
if (!isActive) return
|
if (!isActive) return
|
||||||
isActive = false
|
isActive = false
|
||||||
announceJob?.cancel()
|
announceJob?.cancel()
|
||||||
announceJob = null
|
announceJob = null
|
||||||
|
if (ownsGossipManager) {
|
||||||
gossipSyncManager.stop()
|
gossipSyncManager.stop()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun shutdown() {
|
fun shutdown() {
|
||||||
peerManager.shutdown()
|
peerManager.shutdown()
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ class NoiseEncryptionService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize session manager
|
// Initialize session manager
|
||||||
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey)
|
val localPeerID = calculateFingerprint(staticIdentityPublicKey).take(16)
|
||||||
|
sessionManager = NoiseSessionManager(staticIdentityPrivateKey, staticIdentityPublicKey, localPeerID)
|
||||||
|
|
||||||
// Set up session callbacks
|
// Set up session callbacks
|
||||||
sessionManager.onSessionEstablished = { peerID, remoteStaticKey ->
|
sessionManager.onSessionEstablished = { peerID, remoteStaticKey ->
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ class NoiseSession(
|
|||||||
// Session state
|
// Session state
|
||||||
private var state: NoiseSessionState = NoiseSessionState.Uninitialized
|
private var state: NoiseSessionState = NoiseSessionState.Uninitialized
|
||||||
private val creationTime = System.currentTimeMillis()
|
private val creationTime = System.currentTimeMillis()
|
||||||
|
private var handshakeStartMs: Long? = null
|
||||||
|
private var lastHandshakeActivityMs: Long? = null
|
||||||
|
private var handshakeMessage1: ByteArray? = null
|
||||||
|
|
||||||
// Session counters
|
// Session counters
|
||||||
private var currentPattern = 0;
|
private var currentPattern = 0;
|
||||||
@@ -195,6 +198,15 @@ class NoiseSession(
|
|||||||
fun isEstablished(): Boolean = state is NoiseSessionState.Established
|
fun isEstablished(): Boolean = state is NoiseSessionState.Established
|
||||||
fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking
|
fun isHandshaking(): Boolean = state is NoiseSessionState.Handshaking
|
||||||
fun getCreationTime(): Long = creationTime
|
fun getCreationTime(): Long = creationTime
|
||||||
|
fun isInitiatorRole(): Boolean = isInitiator
|
||||||
|
fun getHandshakeStartMs(): Long? = handshakeStartMs
|
||||||
|
fun getLastHandshakeActivityMs(): Long? = lastHandshakeActivityMs
|
||||||
|
|
||||||
|
internal fun getHandshakeMessage1(): ByteArray? = handshakeMessage1?.clone()
|
||||||
|
|
||||||
|
internal fun setLastHandshakeActivityForTest(timestampMs: Long) {
|
||||||
|
lastHandshakeActivityMs = timestampMs
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
try {
|
try {
|
||||||
@@ -317,12 +329,17 @@ class NoiseSession(
|
|||||||
// Initialize handshake as initiator
|
// Initialize handshake as initiator
|
||||||
initializeNoiseHandshake(HandshakeState.INITIATOR)
|
initializeNoiseHandshake(HandshakeState.INITIATOR)
|
||||||
state = NoiseSessionState.Handshaking
|
state = NoiseSessionState.Handshaking
|
||||||
|
if (handshakeStartMs == null) {
|
||||||
|
handshakeStartMs = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
lastHandshakeActivityMs = System.currentTimeMillis()
|
||||||
|
|
||||||
val messageBuffer = ByteArray(XX_MESSAGE_1_SIZE)
|
val messageBuffer = ByteArray(XX_MESSAGE_1_SIZE)
|
||||||
val handshakeStateLocal = handshakeState ?: throw IllegalStateException("Handshake state is null")
|
val handshakeStateLocal = handshakeState ?: throw IllegalStateException("Handshake state is null")
|
||||||
val messageLength = handshakeStateLocal.writeMessage(messageBuffer, 0, null, 0, 0)
|
val messageLength = handshakeStateLocal.writeMessage(messageBuffer, 0, null, 0, 0)
|
||||||
currentPattern++
|
currentPattern++
|
||||||
val firstMessage = messageBuffer.copyOf(messageLength)
|
val firstMessage = messageBuffer.copyOf(messageLength)
|
||||||
|
handshakeMessage1 = firstMessage
|
||||||
|
|
||||||
// Validate message size matches XX pattern expectations
|
// Validate message size matches XX pattern expectations
|
||||||
if (firstMessage.size != XX_MESSAGE_1_SIZE) {
|
if (firstMessage.size != XX_MESSAGE_1_SIZE) {
|
||||||
@@ -351,12 +368,16 @@ class NoiseSession(
|
|||||||
if (state == NoiseSessionState.Uninitialized && !isInitiator) {
|
if (state == NoiseSessionState.Uninitialized && !isInitiator) {
|
||||||
initializeNoiseHandshake(HandshakeState.RESPONDER)
|
initializeNoiseHandshake(HandshakeState.RESPONDER)
|
||||||
state = NoiseSessionState.Handshaking
|
state = NoiseSessionState.Handshaking
|
||||||
|
if (handshakeStartMs == null) {
|
||||||
|
handshakeStartMs = System.currentTimeMillis()
|
||||||
|
}
|
||||||
Log.d(TAG, "Initialized as RESPONDER for XX handshake with $peerID")
|
Log.d(TAG, "Initialized as RESPONDER for XX handshake with $peerID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != NoiseSessionState.Handshaking) {
|
if (state != NoiseSessionState.Handshaking) {
|
||||||
throw IllegalStateException("Invalid state for handshake: $state")
|
throw IllegalStateException("Invalid state for handshake: $state")
|
||||||
}
|
}
|
||||||
|
lastHandshakeActivityMs = System.currentTimeMillis()
|
||||||
|
|
||||||
val handshakeStateLocal = handshakeState ?: throw IllegalStateException("Handshake state is null")
|
val handshakeStateLocal = handshakeState ?: throw IllegalStateException("Handshake state is null")
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
*/
|
*/
|
||||||
class NoiseSessionManager(
|
class NoiseSessionManager(
|
||||||
private val localStaticPrivateKey: ByteArray,
|
private val localStaticPrivateKey: ByteArray,
|
||||||
private val localStaticPublicKey: ByteArray
|
private val localStaticPublicKey: ByteArray,
|
||||||
|
private val localPeerID: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "NoiseSessionManager"
|
private const val TAG = "NoiseSessionManager"
|
||||||
|
private const val HANDSHAKE_TIMEOUT_MS = 20_000L
|
||||||
|
private const val HANDSHAKE_MESSAGE_1_SIZE = 32
|
||||||
}
|
}
|
||||||
|
|
||||||
private val sessions = ConcurrentHashMap<String, NoiseSession>()
|
private val sessions = ConcurrentHashMap<String, NoiseSession>()
|
||||||
@@ -51,11 +54,30 @@ class NoiseSessionManager(
|
|||||||
/**
|
/**
|
||||||
* SIMPLIFIED: Initiate handshake - no tie breaker, just start
|
* SIMPLIFIED: Initiate handshake - no tie breaker, just start
|
||||||
*/
|
*/
|
||||||
fun initiateHandshake(peerID: String): ByteArray {
|
fun initiateHandshake(peerID: String): ByteArray? {
|
||||||
Log.d(TAG, "initiateHandshake($peerID)")
|
Log.d(TAG, "initiateHandshake($peerID)")
|
||||||
|
|
||||||
// Remove any existing session first
|
val now = System.currentTimeMillis()
|
||||||
|
val existing = getSession(peerID)
|
||||||
|
if (existing != null) {
|
||||||
|
when {
|
||||||
|
existing.isEstablished() -> {
|
||||||
|
Log.d(TAG, "Handshake already established with $peerID, skipping initiate")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
existing.isHandshaking() -> {
|
||||||
|
if (!isHandshakeStale(existing, now)) {
|
||||||
|
Log.d(TAG, "Handshake already in progress with $peerID, not restarting")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Handshake with $peerID is stale; restarting")
|
||||||
removeSession(peerID)
|
removeSession(peerID)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
removeSession(peerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create new session as initiator
|
// Create new session as initiator
|
||||||
val session = NoiseSession(
|
val session = NoiseSession(
|
||||||
@@ -86,6 +108,23 @@ class NoiseSessionManager(
|
|||||||
try {
|
try {
|
||||||
var session = getSession(peerID)
|
var session = getSession(peerID)
|
||||||
|
|
||||||
|
// Collision handling: both sides initiated and we received message 1
|
||||||
|
if (session != null &&
|
||||||
|
session.isHandshaking() &&
|
||||||
|
session.isInitiatorRole() &&
|
||||||
|
message.size == HANDSHAKE_MESSAGE_1_SIZE
|
||||||
|
) {
|
||||||
|
val shouldYield = localPeerID > peerID
|
||||||
|
if (shouldYield) {
|
||||||
|
Log.d(TAG, "Handshake collision with $peerID; yielding to responder role")
|
||||||
|
removeSession(peerID)
|
||||||
|
session = null
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Handshake collision with $peerID; keeping initiator role")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If no session exists, create one as responder
|
// If no session exists, create one as responder
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
Log.d(TAG, "Creating new RESPONDER session for $peerID")
|
Log.d(TAG, "Creating new RESPONDER session for $peerID")
|
||||||
@@ -120,6 +159,12 @@ class NoiseSessionManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isHandshakeStale(session: NoiseSession, nowMs: Long): Boolean {
|
||||||
|
val lastActivity = session.getLastHandshakeActivityMs() ?: session.getHandshakeStartMs()
|
||||||
|
if (lastActivity == null) return false
|
||||||
|
return (nowMs - lastActivity) > HANDSHAKE_TIMEOUT_MS
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SIMPLIFIED: Encrypt data
|
* SIMPLIFIED: Encrypt data
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bitchat.android.service
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.bitchat.android.model.RoutedPacket
|
import com.bitchat.android.model.RoutedPacket
|
||||||
|
import com.bitchat.android.protocol.BitchatPacket
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +24,11 @@ object TransportBridgeService {
|
|||||||
* Send a packet out via this transport.
|
* Send a packet out via this transport.
|
||||||
*/
|
*/
|
||||||
fun send(packet: RoutedPacket)
|
fun send(packet: RoutedPacket)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a packet to a specific peer via this transport (optional).
|
||||||
|
*/
|
||||||
|
fun sendToPeer(peerID: String, packet: BitchatPacket) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val transports = ConcurrentHashMap<String, TransportLayer>()
|
private val transports = ConcurrentHashMap<String, TransportLayer>()
|
||||||
@@ -66,4 +72,20 @@ object TransportBridgeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a packet to a specific peer across all other transports.
|
||||||
|
*/
|
||||||
|
fun sendToPeer(sourceId: String, peerID: String, packet: BitchatPacket) {
|
||||||
|
val targets = transports.filterKeys { it != sourceId }
|
||||||
|
if (targets.isEmpty()) return
|
||||||
|
|
||||||
|
targets.forEach { (id, layer) ->
|
||||||
|
try {
|
||||||
|
layer.sendToPeer(peerID, packet)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to bridge unicast packet to $id: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,8 +308,26 @@ class MeshDelegateHandler(
|
|||||||
if (shouldSendReadReceipt) {
|
if (shouldSendReadReceipt) {
|
||||||
android.util.Log.d("MeshDelegateHandler", "Sending reactive read receipt for focused chat with $senderPeerID (message=${message.id})")
|
android.util.Log.d("MeshDelegateHandler", "Sending reactive read receipt for focused chat with $senderPeerID (message=${message.id})")
|
||||||
val nickname = state.getNicknameValue() ?: "unknown"
|
val nickname = state.getNicknameValue() ?: "unknown"
|
||||||
// Send directly for this message to avoid relying on unread queues
|
val mesh = getMeshService()
|
||||||
getMeshService().sendReadReceipt(message.id, senderPeerID!!, nickname)
|
val sent = try {
|
||||||
|
val hasMesh = mesh.getPeerInfo(senderPeerID!!)?.isConnected == true && mesh.hasEstablishedSession(senderPeerID)
|
||||||
|
if (hasMesh) {
|
||||||
|
mesh.sendReadReceipt(message.id, senderPeerID, nickname)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
||||||
|
val hasAware = try { aware?.getPeerInfo(senderPeerID)?.isConnected == true && aware.hasEstablishedSession(senderPeerID) } catch (_: Exception) { false }
|
||||||
|
if (hasAware) {
|
||||||
|
aware?.sendReadReceipt(message.id, senderPeerID, nickname)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (sent) {
|
||||||
// Ensure unread badge is cleared for this peer immediately
|
// Ensure unread badge is cleared for this peer immediately
|
||||||
try {
|
try {
|
||||||
val current = state.getUnreadPrivateMessagesValue().toMutableSet()
|
val current = state.getUnreadPrivateMessagesValue().toMutableSet()
|
||||||
@@ -317,6 +335,7 @@ class MeshDelegateHandler(
|
|||||||
state.setUnreadPrivateMessages(current)
|
state.setUnreadPrivateMessages(current)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) { }
|
} catch (_: Exception) { }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
android.util.Log.d("MeshDelegateHandler", "Skipping read receipt - chat not focused (background: $isAppInBackground, current peer: $currentPrivateChatPeer, sender: $senderPeerID)")
|
android.util.Log.d("MeshDelegateHandler", "Skipping read receipt - chat not focused (background: $isAppInBackground, current peer: $currentPrivateChatPeer, sender: $senderPeerID)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,13 +333,21 @@ class PrivateChatManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val myNickname = state.getNicknameValue() ?: "unknown"
|
val myNickname = state.getNicknameValue() ?: "unknown"
|
||||||
|
val aware = try { com.bitchat.android.wifiaware.WifiAwareController.getService() } catch (_: Exception) { null }
|
||||||
|
val hasMesh = try { meshService.getPeerInfo(peerID)?.isConnected == true && meshService.hasEstablishedSession(peerID) } catch (_: Exception) { false }
|
||||||
|
val hasAware = try { aware?.getPeerInfo(peerID)?.isConnected == true && aware.hasEstablishedSession(peerID) } catch (_: Exception) { false }
|
||||||
var sentCount = 0
|
var sentCount = 0
|
||||||
messages.forEach { msg ->
|
messages.forEach { msg ->
|
||||||
// Only for incoming messages from this peer
|
// Only for incoming messages from this peer
|
||||||
if (msg.senderPeerID == peerID) {
|
if (msg.senderPeerID == peerID) {
|
||||||
try {
|
try {
|
||||||
|
if (hasMesh) {
|
||||||
meshService.sendReadReceipt(msg.id, peerID, myNickname)
|
meshService.sendReadReceipt(msg.id, peerID, myNickname)
|
||||||
sentCount += 1
|
sentCount += 1
|
||||||
|
} else if (hasAware) {
|
||||||
|
aware?.sendReadReceipt(msg.id, peerID, myNickname)
|
||||||
|
sentCount += 1
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "Failed to send read receipt for message ${msg.id}: ${e.message}")
|
Log.w(TAG, "Failed to send read receipt for message ${msg.id}: ${e.message}")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ class WifiAwareMeshService(private val context: Context) : MeshService, Transpor
|
|||||||
meshCore.sendFromBridge(packet)
|
meshCore.sendFromBridge(packet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun sendToPeer(peerID: String, packet: BitchatPacket) {
|
||||||
|
sendPacketToPeer(peerID, packet)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Broadcasts routed packet to currently connected peers.
|
* Broadcasts routed packet to currently connected peers.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user