mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 18:25:21 +00:00
Merge branch 'main' into gossip-routing-2
This commit is contained in:
@@ -29,7 +29,7 @@ class BluetoothConnectionManager(
|
||||
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
|
||||
|
||||
// Power management
|
||||
private val powerManager = PowerManager(context)
|
||||
private val powerManager = PowerManager(context.applicationContext)
|
||||
|
||||
// Coroutines
|
||||
private val connectionScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
@@ -257,6 +257,23 @@ class BluetoothConnectionManager(
|
||||
serverManager.getCharacteristic()
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelTransfer(transferId: String): Boolean {
|
||||
return packetBroadcaster.cancelTransfer(transferId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet directly to a specific peer, without broadcasting to others.
|
||||
*/
|
||||
fun sendPacketToPeer(peerID: String, packet: BitchatPacket): Boolean {
|
||||
if (!isActive) return false
|
||||
return packetBroadcaster.sendPacketToPeer(
|
||||
RoutedPacket(packet),
|
||||
peerID,
|
||||
serverManager.getGattServer(),
|
||||
serverManager.getCharacteristic()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Expose role controls for debug UI
|
||||
|
||||
@@ -327,8 +327,31 @@ class BluetoothGattServerManager(
|
||||
private fun startAdvertising() {
|
||||
// Respect debug setting
|
||||
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
|
||||
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive || bluetoothAdapter == null || !bluetoothAdapter.isMultipleAdvertisementSupported() || !enabled) {
|
||||
throw Exception("Missing Bluetooth permissions or BLE advertiser not available")
|
||||
|
||||
// Guard conditions – never throw here to avoid crashing the app from a background coroutine
|
||||
if (!permissionManager.hasBluetoothPermissions()) {
|
||||
Log.w(TAG, "Not starting advertising: missing Bluetooth permissions")
|
||||
return
|
||||
}
|
||||
if (bluetoothAdapter == null) {
|
||||
Log.w(TAG, "Not starting advertising: bluetoothAdapter is null")
|
||||
return
|
||||
}
|
||||
if (!isActive) {
|
||||
Log.d(TAG, "Not starting advertising: manager not active")
|
||||
return
|
||||
}
|
||||
if (!enabled) {
|
||||
Log.i(TAG, "Not starting advertising: GATT Server disabled via debug settings")
|
||||
return
|
||||
}
|
||||
if (bleAdvertiser == null) {
|
||||
Log.w(TAG, "Not starting advertising: BLE advertiser not available on this device")
|
||||
return
|
||||
}
|
||||
if (!bluetoothAdapter.isMultipleAdvertisementSupported) {
|
||||
Log.w(TAG, "Not starting advertising: multiple advertisement not supported on this device")
|
||||
return
|
||||
}
|
||||
|
||||
val settings = powerManager.getAdvertiseSettings()
|
||||
@@ -341,7 +364,10 @@ class BluetoothGattServerManager(
|
||||
|
||||
advertiseCallback = object : AdvertiseCallback() {
|
||||
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
|
||||
Log.i(TAG, "Advertising started (power mode: ${powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]})")
|
||||
val mode = try {
|
||||
powerManager.getPowerInfo().split("Current Mode: ")[1].split("\n")[0]
|
||||
} catch (_: Exception) { "unknown" }
|
||||
Log.i(TAG, "Advertising started (power mode: $mode)")
|
||||
}
|
||||
|
||||
override fun onStartFailure(errorCode: Int) {
|
||||
@@ -351,6 +377,8 @@ class BluetoothGattServerManager(
|
||||
|
||||
try {
|
||||
bleAdvertiser.startAdvertising(settings, data, advertiseCallback)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e(TAG, "SecurityException starting advertising (missing permission?): ${se.message}")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Exception starting advertising: ${e.message}")
|
||||
}
|
||||
@@ -363,7 +391,7 @@ class BluetoothGattServerManager(
|
||||
private fun stopAdvertising() {
|
||||
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null) return
|
||||
try {
|
||||
advertiseCallback?.let { bleAdvertiser.stopAdvertising(it) }
|
||||
advertiseCallback?.let { cb -> bleAdvertiser.stopAdvertising(cb) }
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Error stopping advertising: ${e.message}")
|
||||
}
|
||||
@@ -386,4 +414,4 @@ class BluetoothGattServerManager(
|
||||
startAdvertising()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
import com.bitchat.android.protocol.MessageType
|
||||
import com.bitchat.android.protocol.SpecialRecipients
|
||||
import com.bitchat.android.model.RequestSyncPacket
|
||||
import com.bitchat.android.sync.GossipSyncManager
|
||||
import com.bitchat.android.util.toHexString
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.*
|
||||
@@ -37,18 +39,19 @@ class BluetoothMeshService(private val context: Context) {
|
||||
private const val MAX_TTL: UByte = 7u
|
||||
}
|
||||
|
||||
// My peer identification - same format as iOS
|
||||
val myPeerID: String = generateCompatiblePeerID()
|
||||
|
||||
// Core components - each handling specific responsibilities
|
||||
private val encryptionService = EncryptionService(context)
|
||||
|
||||
// My peer identification - derived from persisted Noise identity fingerprint (first 16 hex chars)
|
||||
val myPeerID: String = encryptionService.getIdentityFingerprint().take(16)
|
||||
private val peerManager = PeerManager()
|
||||
private val fragmentManager = FragmentManager()
|
||||
private val securityManager = SecurityManager(encryptionService, myPeerID)
|
||||
private val storeForwardManager = StoreForwardManager()
|
||||
private val messageHandler = MessageHandler(myPeerID)
|
||||
private val messageHandler = MessageHandler(myPeerID, context.applicationContext)
|
||||
internal val connectionManager = BluetoothConnectionManager(context, myPeerID, fragmentManager) // Made internal for access
|
||||
private val packetProcessor = PacketProcessor(myPeerID)
|
||||
private lateinit var gossipSyncManager: GossipSyncManager
|
||||
|
||||
// Service state management
|
||||
private var isActive = false
|
||||
@@ -63,6 +66,38 @@ class BluetoothMeshService(private val context: Context) {
|
||||
setupDelegates()
|
||||
messageHandler.packetProcessor = packetProcessor
|
||||
//startPeriodicDebugLogging()
|
||||
|
||||
// Initialize sync manager (needs serviceScope)
|
||||
gossipSyncManager = GossipSyncManager(
|
||||
myPeerID = myPeerID,
|
||||
scope = serviceScope,
|
||||
configProvider = object : GossipSyncManager.ConfigProvider {
|
||||
override fun seenCapacity(): Int = try {
|
||||
com.bitchat.android.ui.debug.DebugPreferenceManager.getSeenPacketCapacity(500)
|
||||
} catch (_: Exception) { 500 }
|
||||
|
||||
override fun gcsMaxBytes(): Int = try {
|
||||
com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsMaxFilterBytes(400)
|
||||
} catch (_: Exception) { 400 }
|
||||
|
||||
override fun gcsTargetFpr(): Double = try {
|
||||
com.bitchat.android.ui.debug.DebugPreferenceManager.getGcsFprPercent(1.0) / 100.0
|
||||
} catch (_: Exception) { 0.01 }
|
||||
}
|
||||
)
|
||||
|
||||
// Wire sync manager delegate
|
||||
gossipSyncManager.delegate = object : GossipSyncManager.Delegate {
|
||||
override fun sendPacket(packet: BitchatPacket) {
|
||||
connectionManager.broadcastPacket(RoutedPacket(packet))
|
||||
}
|
||||
override fun sendPacketToPeer(peerID: String, packet: BitchatPacket) {
|
||||
connectionManager.sendPacketToPeer(peerID, packet)
|
||||
}
|
||||
override fun signPacketForBroadcast(packet: BitchatPacket): BitchatPacket {
|
||||
return signPacketBeforeBroadcast(packet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,6 +148,16 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun onPeerListUpdated(peerIDs: List<String>) {
|
||||
delegate?.didUpdatePeerList(peerIDs)
|
||||
}
|
||||
override fun onPeerRemoved(peerID: String) {
|
||||
try { gossipSyncManager.removeAnnouncementForPeer(peerID) } catch (_: Exception) { }
|
||||
// Also drop any Noise session state for this peer when they go offline
|
||||
try {
|
||||
encryptionService.removePeer(peerID)
|
||||
Log.d(TAG, "Removed Noise session for offline peer $peerID")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to remove Noise session for $peerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SecurityManager delegate for key exchange notifications
|
||||
@@ -380,13 +425,26 @@ class BluetoothMeshService(private val context: Context) {
|
||||
} catch (_: Exception) { }
|
||||
}
|
||||
} catch (_: Exception) { }
|
||||
|
||||
// Schedule initial sync for this new directly connected peer only
|
||||
try { gossipSyncManager.scheduleInitialSyncToPeer(pid, 1_000) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
// Track for sync
|
||||
try { gossipSyncManager.onPublicPacketSeen(routed.packet) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleMessage(routed: RoutedPacket) {
|
||||
serviceScope.launch { messageHandler.handleMessage(routed) }
|
||||
// Track broadcast messages for sync
|
||||
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) {
|
||||
@@ -394,6 +452,13 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
|
||||
override fun handleFragment(packet: BitchatPacket): BitchatPacket? {
|
||||
// Track broadcast fragments for gossip sync
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -412,6 +477,13 @@ class BluetoothMeshService(private val context: Context) {
|
||||
override fun sendToPeer(peerID: String, routed: RoutedPacket): Boolean {
|
||||
return connectionManager.sendToPeer(peerID, routed)
|
||||
}
|
||||
|
||||
override fun handleRequestSync(routed: RoutedPacket) {
|
||||
// Decode request and respond with missing packets
|
||||
val fromPeer = routed.peerID ?: return
|
||||
val req = RequestSyncPacket.decode(routed.packet.payload) ?: return
|
||||
gossipSyncManager.handleRequestSync(fromPeer, req)
|
||||
}
|
||||
}
|
||||
|
||||
// BluetoothConnectionManager delegates
|
||||
@@ -484,6 +556,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Start periodic announcements for peer discovery and connectivity
|
||||
sendPeriodicBroadcastAnnounce()
|
||||
Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)")
|
||||
// Start periodic syncs
|
||||
gossipSyncManager.start()
|
||||
} else {
|
||||
Log.e(TAG, "Failed to start Bluetooth services")
|
||||
}
|
||||
@@ -508,6 +582,7 @@ class BluetoothMeshService(private val context: Context) {
|
||||
delay(200) // Give leave message time to send
|
||||
|
||||
// Stop all components
|
||||
gossipSyncManager.stop()
|
||||
connectionManager.stopServices()
|
||||
peerManager.shutdown()
|
||||
fragmentManager.shutdown()
|
||||
@@ -541,8 +616,123 @@ class BluetoothMeshService(private val context: Context) {
|
||||
// Sign the packet before broadcasting
|
||||
val signedPacket = signPacketBeforeBroadcast(packet)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
// Track our own broadcast message for sync
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a file over mesh as a broadcast MESSAGE (public mesh timeline/channels).
|
||||
*/
|
||||
fun sendFileBroadcast(file: com.bitchat.android.model.BitchatFilePacket) {
|
||||
try {
|
||||
Log.d(TAG, "📤 sendFileBroadcast: name=${file.fileName}, size=${file.fileSize}")
|
||||
val payload = file.encode()
|
||||
if (payload == null) {
|
||||
Log.e(TAG, "❌ Failed to encode file packet in sendFileBroadcast")
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "📦 Encoded payload: ${payload.size} bytes")
|
||||
serviceScope.launch {
|
||||
val packet = BitchatPacket(
|
||||
version = 2u, // FILE_TRANSFER uses v2 for 4-byte payload length to support large files
|
||||
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(packet)
|
||||
// Use a stable transferId based on the file TLV payload for progress tracking
|
||||
val transferId = sha256Hex(payload)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
||||
try { gossipSyncManager.onPublicPacketSeen(signed) } catch (_: Exception) { }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ sendFileBroadcast failed: ${e.message}", e)
|
||||
Log.e(TAG, "❌ File: name=${file.fileName}, size=${file.fileSize}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a file as an encrypted private message using Noise protocol
|
||||
*/
|
||||
fun sendFilePrivate(recipientPeerID: String, file: com.bitchat.android.model.BitchatFilePacket) {
|
||||
try {
|
||||
Log.d(TAG, "📤 sendFilePrivate (ENCRYPTED): to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}")
|
||||
|
||||
serviceScope.launch {
|
||||
// Check if we have an established Noise session
|
||||
if (encryptionService.hasEstablishedSession(recipientPeerID)) {
|
||||
try {
|
||||
// Encode the file packet as TLV
|
||||
val filePayload = file.encode()
|
||||
if (filePayload == null) {
|
||||
Log.e(TAG, "❌ Failed to encode file packet for private send")
|
||||
return@launch
|
||||
}
|
||||
Log.d(TAG, "📦 Encoded file TLV: ${filePayload.size} bytes")
|
||||
|
||||
// Create NoisePayload wrapper (type byte + file TLV data) - same as iOS
|
||||
val noisePayload = com.bitchat.android.model.NoisePayload(
|
||||
type = com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER,
|
||||
data = filePayload
|
||||
)
|
||||
|
||||
// Encrypt the payload using Noise
|
||||
val encrypted = encryptionService.encrypt(noisePayload.encode(), recipientPeerID)
|
||||
if (encrypted == null) {
|
||||
Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID")
|
||||
return@launch
|
||||
}
|
||||
Log.d(TAG, "🔐 Encrypted file payload: ${encrypted.size} bytes")
|
||||
|
||||
// Create NOISE_ENCRYPTED packet (not FILE_TRANSFER!)
|
||||
val packet = BitchatPacket(
|
||||
version = 1u,
|
||||
type = MessageType.NOISE_ENCRYPTED.value,
|
||||
senderID = hexStringToByteArray(myPeerID),
|
||||
recipientID = hexStringToByteArray(recipientPeerID),
|
||||
timestamp = System.currentTimeMillis().toULong(),
|
||||
payload = encrypted,
|
||||
signature = null,
|
||||
ttl = 7u
|
||||
)
|
||||
|
||||
// Sign and send the encrypted packet
|
||||
val signed = signPacketBeforeBroadcast(packet)
|
||||
// Use a stable transferId based on the unencrypted file TLV payload for progress tracking
|
||||
val transferId = sha256Hex(filePayload)
|
||||
connectionManager.broadcastPacket(RoutedPacket(signed, transferId = transferId))
|
||||
Log.d(TAG, "✅ Sent encrypted file to $recipientPeerID")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to encrypt file for $recipientPeerID: ${e.message}", e)
|
||||
}
|
||||
} else {
|
||||
// No session - initiate handshake but don't queue file
|
||||
Log.w(TAG, "⚠️ No Noise session with $recipientPeerID for file transfer, initiating handshake")
|
||||
messageHandler.delegate?.initiateNoiseHandshake(recipientPeerID)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ sendFilePrivate failed: ${e.message}", e)
|
||||
Log.e(TAG, "❌ File: to=$recipientPeerID, name=${file.fileName}, size=${file.fileSize}")
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelFileTransfer(transferId: String): Boolean {
|
||||
return connectionManager.cancelTransfer(transferId)
|
||||
}
|
||||
|
||||
// Local helper to hash payloads to a stable hex ID for progress mapping
|
||||
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) }
|
||||
|
||||
/**
|
||||
* Send private message - SIMPLIFIED iOS-compatible version
|
||||
@@ -726,6 +916,8 @@ class BluetoothMeshService(private val context: Context) {
|
||||
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
Log.d(TAG, "Sent iOS-compatible signed TLV announce (${tlvPayload.size} bytes)")
|
||||
// Track announce for sync
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -788,6 +980,9 @@ class BluetoothMeshService(private val context: Context) {
|
||||
connectionManager.broadcastPacket(RoutedPacket(signedPacket))
|
||||
peerManager.markPeerAsAnnouncedTo(peerID)
|
||||
Log.d(TAG, "Sent iOS-compatible signed TLV peer announce to $peerID (${tlvPayload.size} bytes)")
|
||||
|
||||
// Track announce for sync
|
||||
try { gossipSyncManager.onPublicPacketSeen(signedPacket) } catch (_: Exception) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -950,15 +1145,6 @@ class BluetoothMeshService(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate peer ID compatible with iOS - exactly 8 bytes (16 hex characters)
|
||||
*/
|
||||
private fun generateCompatiblePeerID(): String {
|
||||
val randomBytes = ByteArray(8) // 8 bytes = 16 hex characters (like iOS)
|
||||
Random.nextBytes(randomBytes)
|
||||
return randomBytes.joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex string peer ID to binary data (8 bytes) - exactly same as iOS
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import android.bluetooth.BluetoothDevice
|
||||
@@ -17,6 +18,8 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlinx.coroutines.channels.actor
|
||||
|
||||
/**
|
||||
@@ -70,6 +73,7 @@ class BluetoothPacketBroadcaster(
|
||||
try {
|
||||
val fromNick = incomingPeer?.let { nicknameResolver?.invoke(it) }
|
||||
val toNick = toPeer?.let { nicknameResolver?.invoke(it) }
|
||||
val isRelay = (incomingAddr != null || incomingPeer != null)
|
||||
|
||||
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().logPacketRelayDetailed(
|
||||
packetType = typeName,
|
||||
@@ -81,7 +85,8 @@ class BluetoothPacketBroadcaster(
|
||||
toPeerID = toPeer,
|
||||
toNickname = toNick,
|
||||
toDeviceAddress = toDeviceAddress,
|
||||
ttl = ttl
|
||||
ttl = ttl,
|
||||
isRelay = isRelay
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
// Silently ignore debug logging failures
|
||||
@@ -97,6 +102,7 @@ class BluetoothPacketBroadcaster(
|
||||
|
||||
// Actor scope for the broadcaster
|
||||
private val broadcasterScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val transferJobs = ConcurrentHashMap<String, Job>()
|
||||
|
||||
// SERIALIZATION: Actor to serialize all broadcast operations
|
||||
@OptIn(kotlinx.coroutines.ObsoleteCoroutinesApi::class)
|
||||
@@ -119,26 +125,136 @@ class BluetoothPacketBroadcaster(
|
||||
characteristic: BluetoothGattCharacteristic?
|
||||
) {
|
||||
val packet = routed.packet
|
||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
||||
if (isFile) {
|
||||
Log.d(TAG, "📤 Broadcasting FILE_TRANSFER: ${packet.payload.size} bytes")
|
||||
}
|
||||
// Prefer caller-provided transferId (e.g., for encrypted media), else derive for FILE_TRANSFER
|
||||
val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null)
|
||||
// Check if we need to fragment
|
||||
if (fragmentManager != null) {
|
||||
val fragments = fragmentManager.createFragments(packet)
|
||||
val fragments = try {
|
||||
fragmentManager.createFragments(packet)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Fragment creation failed: ${e.message}", e)
|
||||
if (isFile) {
|
||||
Log.e(TAG, "❌ File fragmentation failed for ${packet.payload.size} byte file")
|
||||
}
|
||||
return
|
||||
}
|
||||
if (fragments.size > 1) {
|
||||
if (isFile) {
|
||||
Log.d(TAG, "🔀 File needs ${fragments.size} fragments")
|
||||
}
|
||||
Log.d(TAG, "Fragmenting packet into ${fragments.size} fragments")
|
||||
connectionScope.launch {
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.start(transferId, fragments.size)
|
||||
}
|
||||
val job = connectionScope.launch {
|
||||
var sent = 0
|
||||
fragments.forEach { fragment ->
|
||||
broadcastSinglePacket(RoutedPacket(fragment), gattServer, characteristic)
|
||||
// 20ms delay between fragments (matching iOS/Rust)
|
||||
delay(200)
|
||||
if (!isActive) return@launch
|
||||
// If cancelled, stop sending remaining fragments
|
||||
if (transferId != null && transferJobs[transferId]?.isCancelled == true) return@launch
|
||||
broadcastSinglePacket(RoutedPacket(fragment, transferId = transferId), gattServer, characteristic)
|
||||
// 20ms delay between fragments
|
||||
delay(20)
|
||||
if (transferId != null) {
|
||||
sent += 1
|
||||
TransferProgressManager.progress(transferId, sent, fragments.size)
|
||||
if (sent == fragments.size) TransferProgressManager.complete(transferId, fragments.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (transferId != null) {
|
||||
transferJobs[transferId] = job
|
||||
job.invokeOnCompletion { transferJobs.remove(transferId) }
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Send single packet if no fragmentation needed
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.start(transferId, 1)
|
||||
}
|
||||
broadcastSinglePacket(routed, gattServer, characteristic)
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.progress(transferId, 1, 1)
|
||||
TransferProgressManager.complete(transferId, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelTransfer(transferId: String): Boolean {
|
||||
val job = transferJobs.remove(transferId) ?: return false
|
||||
job.cancel()
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a packet to a specific peer only, without broadcasting.
|
||||
* Returns true if a direct path was found and used.
|
||||
*/
|
||||
fun sendPacketToPeer(
|
||||
routed: RoutedPacket,
|
||||
targetPeerID: String,
|
||||
gattServer: BluetoothGattServer?,
|
||||
characteristic: BluetoothGattCharacteristic?
|
||||
): Boolean {
|
||||
val packet = routed.packet
|
||||
val data = packet.toBinaryData() ?: return false
|
||||
val isFile = packet.type == MessageType.FILE_TRANSFER.value
|
||||
if (isFile) {
|
||||
Log.d(TAG, "📤 Broadcasting FILE_TRANSFER: ${packet.payload.size} bytes")
|
||||
}
|
||||
// Prefer caller-provided transferId (e.g., for encrypted media), else derive for FILE_TRANSFER
|
||||
val transferId = routed.transferId ?: (if (isFile) sha256Hex(packet.payload) else null)
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.start(transferId, 1)
|
||||
}
|
||||
val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString()
|
||||
val incomingAddr = routed.relayAddress
|
||||
val incomingPeer = incomingAddr?.let { connectionTracker.addressPeerMap[it] }
|
||||
val senderPeerID = routed.peerID ?: packet.senderID.toHexString()
|
||||
val senderNick = senderPeerID.let { pid -> nicknameResolver?.invoke(pid) }
|
||||
|
||||
// Prefer server-side subscriptions
|
||||
val serverTarget = connectionTracker.getSubscribedDevices()
|
||||
.firstOrNull { connectionTracker.addressPeerMap[it.address] == targetPeerID }
|
||||
if (serverTarget != null) {
|
||||
if (notifyDevice(serverTarget, data, gattServer, characteristic)) {
|
||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, targetPeerID, serverTarget.address, packet.ttl)
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.progress(transferId, 1, 1)
|
||||
TransferProgressManager.complete(transferId, 1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Then client connections
|
||||
val clientTarget = connectionTracker.getConnectedDevices().values
|
||||
.firstOrNull { connectionTracker.addressPeerMap[it.device.address] == targetPeerID }
|
||||
if (clientTarget != null) {
|
||||
if (writeToDeviceConn(clientTarget, data)) {
|
||||
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, targetPeerID, clientTarget.device.address, packet.ttl)
|
||||
if (transferId != null) {
|
||||
TransferProgressManager.progress(transferId, 1, 1)
|
||||
TransferProgressManager.complete(transferId, 1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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) }
|
||||
|
||||
|
||||
/**
|
||||
* Public entry point for broadcasting - submits request to actor for serialization
|
||||
|
||||
@@ -48,10 +48,23 @@ class FragmentManager {
|
||||
* Matches iOS sendFragmentedPacket() implementation exactly
|
||||
*/
|
||||
fun createFragments(packet: BitchatPacket): List<BitchatPacket> {
|
||||
val encoded = packet.toBinaryData() ?: return emptyList()
|
||||
try {
|
||||
Log.d(TAG, "🔀 Creating fragments for packet type ${packet.type}, payload: ${packet.payload.size} bytes")
|
||||
val encoded = packet.toBinaryData()
|
||||
if (encoded == null) {
|
||||
Log.e(TAG, "❌ Failed to encode packet to binary data")
|
||||
return emptyList()
|
||||
}
|
||||
Log.d(TAG, "📦 Encoded to ${encoded.size} bytes")
|
||||
|
||||
// Fragment the unpadded frame; each fragment will be encoded (and padded) independently - iOS fix
|
||||
val fullData = MessagePadding.unpad(encoded)
|
||||
val fullData = try {
|
||||
MessagePadding.unpad(encoded)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Failed to unpad data: ${e.message}", e)
|
||||
return emptyList()
|
||||
}
|
||||
Log.d(TAG, "📏 Unpadded to ${fullData.size} bytes")
|
||||
|
||||
// iOS logic: if data.count > 512 && packet.type != MessageType.fragment.rawValue
|
||||
if (fullData.size <= FRAGMENT_SIZE_THRESHOLD) {
|
||||
@@ -98,7 +111,13 @@ class FragmentManager {
|
||||
fragments.add(fragmentPacket)
|
||||
}
|
||||
|
||||
return fragments
|
||||
Log.d(TAG, "✅ Created ${fragments.size} fragments successfully")
|
||||
return fragments
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "❌ Fragment creation failed: ${e.message}", e)
|
||||
Log.e(TAG, "❌ Packet type: ${packet.type}, payload: ${packet.payload.size} bytes")
|
||||
return emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.bitchat.android.mesh
|
||||
|
||||
import android.util.Log
|
||||
import com.bitchat.android.model.BitchatMessage
|
||||
import com.bitchat.android.model.BitchatMessageType
|
||||
import com.bitchat.android.model.IdentityAnnouncement
|
||||
import com.bitchat.android.model.RoutedPacket
|
||||
import com.bitchat.android.protocol.BitchatPacket
|
||||
@@ -15,7 +16,7 @@ import kotlin.random.Random
|
||||
* Handles processing of different message types
|
||||
* Extracted from BluetoothMeshService for better separation of concerns
|
||||
*/
|
||||
class MessageHandler(private val myPeerID: String) {
|
||||
class MessageHandler(private val myPeerID: String, private val appContext: android.content.Context) {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MessageHandler"
|
||||
@@ -110,6 +111,35 @@ class MessageHandler(private val myPeerID: String) {
|
||||
}
|
||||
}
|
||||
|
||||
com.bitchat.android.model.NoisePayloadType.FILE_TRANSFER -> {
|
||||
// Handle encrypted file transfer; generate unique message ID
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(noisePayload.data)
|
||||
if (file != null) {
|
||||
Log.d(TAG, "🔓 Decrypted encrypted file from $peerID: name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}'")
|
||||
val uniqueMsgId = java.util.UUID.randomUUID().toString().uppercase()
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||
val message = BitchatMessage(
|
||||
id = uniqueMsgId,
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "Unknown",
|
||||
content = savedPath,
|
||||
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
||||
timestamp = java.util.Date(packet.timestamp.toLong()),
|
||||
isRelay = false,
|
||||
isPrivate = true,
|
||||
recipientNickname = delegate?.getMyNickname(),
|
||||
senderPeerID = peerID
|
||||
)
|
||||
|
||||
Log.d(TAG, "📄 Saved encrypted incoming file to $savedPath (msgId=$uniqueMsgId)")
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
// Send delivery ACK with generated message ID
|
||||
sendDeliveryAck(uniqueMsgId, peerID)
|
||||
} else {
|
||||
Log.w(TAG, "⚠️ Failed to decode encrypted file transfer from $peerID")
|
||||
}
|
||||
}
|
||||
|
||||
com.bitchat.android.model.NoisePayloadType.DELIVERED -> {
|
||||
// Handle delivery ACK exactly like iOS
|
||||
val messageID = String(noisePayload.data, Charsets.UTF_8)
|
||||
@@ -345,16 +375,37 @@ class MessageHandler(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse message
|
||||
// Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER
|
||||
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
||||
if (file != null) {
|
||||
if (isFileTransfer) {
|
||||
Log.d(TAG, "📥 FILE_TRANSFER decode success (broadcast): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
||||
}
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||
val message = BitchatMessage(
|
||||
id = java.util.UUID.randomUUID().toString().uppercase(),
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||
content = savedPath,
|
||||
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date(packet.timestamp.toLong())
|
||||
)
|
||||
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
||||
delegate?.onMessageReceived(message)
|
||||
return
|
||||
} else if (isFileTransfer) {
|
||||
Log.w(TAG, "⚠️ FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)} payloadSize=${packet.payload.size}")
|
||||
}
|
||||
|
||||
// Fallback: plain text
|
||||
val message = BitchatMessage(
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||
content = String(packet.payload, Charsets.UTF_8),
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date(packet.timestamp.toLong())
|
||||
)
|
||||
|
||||
delegate?.onMessageReceived(message)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to process broadcast message: ${e.message}")
|
||||
}
|
||||
@@ -371,7 +422,32 @@ class MessageHandler(private val myPeerID: String) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse message
|
||||
// Try file packet first (voice, image, etc.) and log outcome for FILE_TRANSFER
|
||||
val isFileTransfer = com.bitchat.android.protocol.MessageType.fromValue(packet.type) == com.bitchat.android.protocol.MessageType.FILE_TRANSFER
|
||||
val file = com.bitchat.android.model.BitchatFilePacket.decode(packet.payload)
|
||||
if (file != null) {
|
||||
if (isFileTransfer) {
|
||||
Log.d(TAG, "📥 FILE_TRANSFER decode success (private): name='${file.fileName}', size=${file.fileSize}, mime='${file.mimeType}', from=${peerID.take(8)}")
|
||||
}
|
||||
val savedPath = com.bitchat.android.features.file.FileUtils.saveIncomingFile(appContext, file)
|
||||
val message = BitchatMessage(
|
||||
id = java.util.UUID.randomUUID().toString().uppercase(),
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||
content = savedPath,
|
||||
type = com.bitchat.android.features.file.FileUtils.messageTypeForMime(file.mimeType),
|
||||
senderPeerID = peerID,
|
||||
timestamp = Date(packet.timestamp.toLong()),
|
||||
isPrivate = true,
|
||||
recipientNickname = delegate?.getMyNickname()
|
||||
)
|
||||
Log.d(TAG, "📄 Saved incoming file to $savedPath")
|
||||
delegate?.onMessageReceived(message)
|
||||
return
|
||||
} else if (isFileTransfer) {
|
||||
Log.w(TAG, "⚠️ FILE_TRANSFER decode failed (private) from ${peerID.take(8)} payloadSize=${packet.payload.size}")
|
||||
}
|
||||
|
||||
// Fallback: plain text
|
||||
val message = BitchatMessage(
|
||||
sender = delegate?.getPeerNickname(peerID) ?: "unknown",
|
||||
content = String(packet.payload, Charsets.UTF_8),
|
||||
@@ -384,6 +460,8 @@ class MessageHandler(private val myPeerID: String) {
|
||||
Log.e(TAG, "Failed to process private message from $peerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle leave message
|
||||
|
||||
@@ -147,14 +147,17 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
when (messageType) {
|
||||
MessageType.ANNOUNCE -> handleAnnounce(routed)
|
||||
MessageType.MESSAGE -> handleMessage(routed)
|
||||
MessageType.FILE_TRANSFER -> handleMessage(routed) // treat same routing path; parsing happens in handler
|
||||
MessageType.LEAVE -> handleLeave(routed)
|
||||
MessageType.FRAGMENT -> handleFragment(routed)
|
||||
MessageType.REQUEST_SYNC -> handleRequestSync(routed)
|
||||
else -> {
|
||||
// Handle private packet types (address check required)
|
||||
if (packetRelayManager.isPacketAddressedToMe(packet)) {
|
||||
when (messageType) {
|
||||
MessageType.NOISE_HANDSHAKE -> handleNoiseHandshake(routed)
|
||||
MessageType.NOISE_ENCRYPTED -> handleNoiseEncrypted(routed)
|
||||
MessageType.FILE_TRANSFER -> handleMessage(routed)
|
||||
else -> {
|
||||
validPacket = false
|
||||
Log.w(TAG, "Unknown message type: ${packet.type}")
|
||||
@@ -235,6 +238,15 @@ class PacketProcessor(private val myPeerID: String) {
|
||||
|
||||
// Fragment relay is now handled by centralized PacketRelayManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle REQUEST_SYNC packets (public, TTL=1)
|
||||
*/
|
||||
private suspend fun handleRequestSync(routed: RoutedPacket) {
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
Log.d(TAG, "Processing REQUEST_SYNC from ${formatPeerForLog(peerID)}")
|
||||
delegate?.handleRequestSync(routed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delivery acknowledgment
|
||||
@@ -308,6 +320,7 @@ interface PacketProcessorDelegate {
|
||||
fun handleMessage(routed: RoutedPacket)
|
||||
fun handleLeave(routed: RoutedPacket)
|
||||
fun handleFragment(packet: BitchatPacket): BitchatPacket?
|
||||
fun handleRequestSync(routed: RoutedPacket)
|
||||
|
||||
// Communication
|
||||
fun sendAnnouncementToPeer(peerID: String)
|
||||
|
||||
@@ -41,7 +41,7 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
val packet = routed.packet
|
||||
val peerID = routed.peerID ?: "unknown"
|
||||
|
||||
Log.d(TAG, "Evaluating relay for packet type ${'$'}{packet.type} from ${'$'}peerID (TTL: ${'$'}{packet.ttl})")
|
||||
Log.d(TAG, "Evaluating relay for packet type ${packet.type} from ${peerID} (TTL: ${packet.ttl})")
|
||||
|
||||
// Double-check this packet isn't addressed to us
|
||||
if (isPacketAddressedToMe(packet)) {
|
||||
@@ -63,7 +63,7 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
|
||||
// Decrement TTL by 1
|
||||
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
Log.d(TAG, "Decremented TTL from ${'$'}{packet.ttl} to ${'$'}{relayPacket.ttl}")
|
||||
Log.d(TAG, "Decremented TTL from ${packet.ttl} to ${relayPacket.ttl}")
|
||||
|
||||
// Source-based routing: if route is set and includes us, try targeted next-hop forwarding
|
||||
val route = relayPacket.route
|
||||
@@ -102,7 +102,7 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
if (shouldRelay) {
|
||||
relayPacket(RoutedPacket(relayPacket, peerID, routed.relayAddress))
|
||||
} else {
|
||||
Log.d(TAG, "Relay decision: NOT relaying packet type ${'$'}{packet.type}")
|
||||
Log.d(TAG, "Relay decision: NOT relaying packet type ${packet.type}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
private fun shouldRelayPacket(packet: BitchatPacket, fromPeerID: String): Boolean {
|
||||
// Always relay if TTL is high enough (indicates important message)
|
||||
if (packet.ttl >= 4u) {
|
||||
Log.d(TAG, "High TTL (${ '$' }{packet.ttl}), relaying")
|
||||
Log.d(TAG, "High TTL (${packet.ttl}), relaying")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
|
||||
// Small networks always relay to ensure connectivity
|
||||
if (networkSize <= 3) {
|
||||
Log.d(TAG, "Small network (${ '$' }networkSize peers), relaying")
|
||||
Log.d(TAG, "Small network (${networkSize} peers), relaying")
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -157,52 +157,16 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
}
|
||||
|
||||
val shouldRelay = Random.nextDouble() < relayProb
|
||||
Log.d(TAG, "Network size: ${'$'}networkSize, Relay probability: ${'$'}relayProb, Decision: ${'$'}shouldRelay")
|
||||
Log.d(TAG, "Network size: ${networkSize}, Relay probability: ${relayProb}, Decision: ${shouldRelay}")
|
||||
|
||||
return shouldRelay
|
||||
}
|
||||
|
||||
/**
|
||||
* Relay message with adaptive probability and timing (same as iOS)
|
||||
* Moved from MessageHandler.kt
|
||||
*/
|
||||
suspend fun relayMessage(routed: RoutedPacket) {
|
||||
val packet = routed.packet
|
||||
|
||||
if (packet.ttl == 0u.toUByte()) {
|
||||
Log.d(TAG, "TTL expired, not relaying message")
|
||||
return
|
||||
}
|
||||
|
||||
val relayPacket = packet.copy(ttl = (packet.ttl - 1u).toUByte())
|
||||
|
||||
// Check network size and apply adaptive relay probability
|
||||
val networkSize = delegate?.getNetworkSize() ?: 1
|
||||
val relayProb = when {
|
||||
networkSize <= 10 -> 1.0
|
||||
networkSize <= 30 -> 0.85
|
||||
networkSize <= 50 -> 0.7
|
||||
networkSize <= 100 -> 0.55
|
||||
else -> 0.4
|
||||
}
|
||||
|
||||
val shouldRelay = relayPacket.ttl >= 4u || networkSize <= 3 || Random.nextDouble() < relayProb
|
||||
|
||||
if (shouldRelay) {
|
||||
val delay = Random.nextLong(50, 500) // Random delay like iOS
|
||||
Log.d(TAG, "Relaying message after ${'$'}delay ms delay")
|
||||
delay(delay)
|
||||
relayPacket(routed.copy(packet = relayPacket))
|
||||
} else {
|
||||
Log.d(TAG, "Relay decision: NOT relaying message (network size: ${'$'}networkSize, prob: ${'$'}relayProb)")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually broadcast the packet for relay
|
||||
*/
|
||||
private fun relayPacket(routed: RoutedPacket) {
|
||||
Log.d(TAG, "🔄 Relaying packet type ${'$'}{routed.packet.type} with TTL ${'$'}{routed.packet.ttl}")
|
||||
Log.d(TAG, "🔄 Relaying packet type ${routed.packet.type} with TTL ${routed.packet.ttl}")
|
||||
delegate?.broadcastPacket(routed)
|
||||
}
|
||||
|
||||
@@ -212,9 +176,9 @@ class PacketRelayManager(private val myPeerID: String) {
|
||||
fun getDebugInfo(): String {
|
||||
return buildString {
|
||||
appendLine("=== Packet Relay Manager Debug Info ===")
|
||||
appendLine("Relay Scope Active: ${'$'}{relayScope.isActive}")
|
||||
appendLine("My Peer ID: ${'$'}myPeerID")
|
||||
appendLine("Network Size: ${'$'}{delegate?.getNetworkSize() ?: \"unknown\"}")
|
||||
appendLine("Relay Scope Active: ${relayScope.isActive}")
|
||||
appendLine("My Peer ID: ${myPeerID}")
|
||||
appendLine("Network Size: ${delegate?.getNetworkSize() ?: "unknown"}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,8 @@ class PeerManager {
|
||||
fingerprintManager.removePeer(peerID)
|
||||
|
||||
if (notifyDelegate && removed != null) {
|
||||
// Notify specific removal event then list update
|
||||
try { delegate?.onPeerRemoved(peerID) } catch (_: Exception) {}
|
||||
notifyPeerListUpdate()
|
||||
}
|
||||
}
|
||||
@@ -529,4 +531,5 @@ class PeerManager {
|
||||
*/
|
||||
interface PeerManagerDelegate {
|
||||
fun onPeerListUpdated(peerIDs: List<String>)
|
||||
fun onPeerRemoved(peerID: String)
|
||||
}
|
||||
|
||||
@@ -50,12 +50,6 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
return false
|
||||
}
|
||||
|
||||
// TTL check
|
||||
if (packet.ttl == 0u.toUByte()) {
|
||||
Log.d(TAG, "Dropping packet with TTL 0")
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate packet payload
|
||||
if (packet.payload.isEmpty()) {
|
||||
Log.d(TAG, "Dropping packet with empty payload")
|
||||
@@ -67,11 +61,11 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
val packetTime = packet.timestamp.toLong()
|
||||
val timeDiff = kotlin.math.abs(currentTime - packetTime)
|
||||
|
||||
if (timeDiff > MESSAGE_TIMEOUT) {
|
||||
Log.d(TAG, "Dropping old packet from $peerID, time diff: ${timeDiff/1000}s")
|
||||
return false
|
||||
}
|
||||
|
||||
// if (timeDiff > MESSAGE_TIMEOUT) {
|
||||
// Log.d(TAG, "Dropping old packet from $peerID, time diff: ${timeDiff/1000}s")
|
||||
// return false
|
||||
// }
|
||||
|
||||
// Duplicate detection
|
||||
val messageID = generateMessageID(packet, peerID)
|
||||
if (processedMessages.contains(messageID)) {
|
||||
@@ -107,9 +101,17 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
// Skip our own handshake messages
|
||||
if (peerID == myPeerID) return false
|
||||
|
||||
// If we already have an established session but the peer is initiating a new handshake,
|
||||
// drop the existing session so we can re-establish cleanly.
|
||||
var forcedRehandshake = false
|
||||
if (encryptionService.hasEstablishedSession(peerID)) {
|
||||
Log.d(TAG, "Handshake already completed with $peerID")
|
||||
return true
|
||||
Log.d(TAG, "Received new Noise handshake from $peerID with an existing session. Dropping old session to re-handshake.")
|
||||
try {
|
||||
encryptionService.removePeer(peerID)
|
||||
forcedRehandshake = true
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to remove existing Noise session for $peerID: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
if (packet.payload.isEmpty()) {
|
||||
@@ -120,7 +122,7 @@ class SecurityManager(private val encryptionService: EncryptionService, private
|
||||
// Prevent duplicate handshake processing
|
||||
val exchangeKey = "$peerID-${packet.payload.sliceArray(0 until minOf(16, packet.payload.size)).contentHashCode()}"
|
||||
|
||||
if (processedKeyExchanges.contains(exchangeKey)) {
|
||||
if (!forcedRehandshake && processedKeyExchanges.contains(exchangeKey)) {
|
||||
Log.d(TAG, "Already processed handshake: $exchangeKey")
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ class StoreForwardManager {
|
||||
|
||||
// Send with delays to avoid overwhelming the connection
|
||||
messagesToSend.forEachIndexed { index, storedMessage ->
|
||||
delay(index * 100L) // 100ms between messages
|
||||
delay(index * 10L) // 10ms between messages
|
||||
delegate?.sendPacket(storedMessage.packet)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bitchat.android.mesh
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class TransferProgressEvent(
|
||||
val transferId: String,
|
||||
val sent: Int,
|
||||
val total: Int,
|
||||
val completed: Boolean
|
||||
)
|
||||
|
||||
object TransferProgressManager {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val _events = MutableSharedFlow<TransferProgressEvent>(replay = 0, extraBufferCapacity = 32)
|
||||
val events: SharedFlow<TransferProgressEvent> = _events
|
||||
|
||||
fun start(id: String, total: Int) { emit(id, 0, total, false) }
|
||||
fun progress(id: String, sent: Int, total: Int) { emit(id, sent, total, sent >= total) }
|
||||
fun complete(id: String, total: Int) { emit(id, total, total, true) }
|
||||
|
||||
private fun emit(id: String, sent: Int, total: Int, done: Boolean) {
|
||||
scope.launch { _events.emit(TransferProgressEvent(id, sent, total, done)) }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user