new files

This commit is contained in:
CC
2026-06-09 00:02:37 +02:00
parent efe15967b8
commit d2d9ab6c69
5 changed files with 336 additions and 23 deletions
@@ -0,0 +1,138 @@
package com.bitchat.android.mesh
import android.util.Log
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import com.bitchat.android.protocol.MessageType
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.security.MessageDigest
import java.util.concurrent.ConcurrentHashMap
/**
* Shared transport send wrapper that applies bitchat packet fragmentation and
* transfer progress before a transport writes packets to its concrete medium.
*/
class FragmentingPacketSender(
private val scope: CoroutineScope,
private val fragmentManager: FragmentManager?,
private val logTag: String,
private val interFragmentDelayMs: Long = 20L
) {
private val transferJobs = ConcurrentHashMap<String, Job>()
fun send(
routed: RoutedPacket,
description: String,
sendSingle: (RoutedPacket) -> Boolean
): Boolean {
val transferId = transferIdFor(routed)
val packets = packetsForTransport(routed.packet) ?: return false
val total = packets.size
if (total <= 1) {
if (transferId != null) {
TransferProgressManager.start(transferId, 1)
}
val sent = sendSingle(routed.copy(packet = packets.first(), transferId = transferId))
if (sent && transferId != null) {
TransferProgressManager.progress(transferId, 1, 1)
TransferProgressManager.complete(transferId, 1)
}
return sent
}
Log.d(logTag, "Fragmenting packet type ${routed.packet.type} into $total fragments for $description")
if (transferId != null) {
TransferProgressManager.start(transferId, total)
}
val job = scope.launch(start = CoroutineStart.LAZY) {
var sent = 0
for (packet in packets) {
if (!isActive) return@launch
if (transferId != null && transferJobs[transferId]?.isCancelled == true) return@launch
val fragment = routed.copy(packet = packet, transferId = transferId)
val delivered = try {
sendSingle(fragment)
} catch (e: Exception) {
Log.e(logTag, "Fragment send failed for $description: ${e.message}", e)
false
}
if (!delivered) {
Log.w(logTag, "Stopping fragmented send for $description after $sent/$total fragments")
return@launch
}
sent += 1
if (transferId != null) {
TransferProgressManager.progress(transferId, sent, total)
}
if (sent < total) {
delay(interFragmentDelayMs)
}
}
if (transferId != null) {
TransferProgressManager.complete(transferId, total)
}
}
if (transferId != null) {
transferJobs[transferId] = job
job.invokeOnCompletion { transferJobs.remove(transferId, job) }
}
job.start()
return true
}
fun cancelTransfer(transferId: String): Boolean {
val job = transferJobs.remove(transferId) ?: return false
job.cancel()
return true
}
private fun packetsForTransport(packet: BitchatPacket): List<BitchatPacket>? {
if (packet.type == MessageType.FRAGMENT.value) {
return listOf(packet)
}
val manager = fragmentManager ?: return listOf(packet)
return try {
val fragments = manager.createFragments(packet)
if (fragments.isEmpty()) {
Log.e(logTag, "Fragment manager returned no packets for packet type ${packet.type}")
null
} else {
fragments
}
} catch (e: Exception) {
Log.e(logTag, "Fragment creation failed for packet type ${packet.type}: ${e.message}", e)
null
}
}
private fun transferIdFor(routed: RoutedPacket): String? {
routed.transferId?.let { return it }
val packet = routed.packet
return if (packet.type == MessageType.FILE_TRANSFER.value) {
sha256Hex(packet.payload)
} else {
null
}
}
private fun sha256Hex(bytes: ByteArray): String = try {
val md = MessageDigest.getInstance("SHA-256")
md.update(bytes)
md.digest().joinToString("") { "%02x".format(it) }
} catch (_: Exception) {
bytes.size.toString(16)
}
}
@@ -636,7 +636,7 @@ private fun PeerItem(
else -> "Routed"
},
modifier = Modifier.size(16.dp),
tint = if (isWifiAware) Color(0xFF9C27B0) else colorScheme.onSurface.copy(alpha = 0.6f)
tint = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
@@ -673,7 +673,7 @@ private fun PeerItem(
imageVector = Icons.Filled.Wifi,
contentDescription = "Direct Wi-Fi Aware",
modifier = Modifier.size(13.dp),
tint = Color(0xFF9C27B0).copy(alpha = 0.8f)
tint = colorScheme.onSurface.copy(alpha = 0.8f)
)
}
}
@@ -42,7 +42,10 @@ import com.bitchat.android.core.ui.component.sheet.BitchatSheetTopBar
import com.bitchat.android.core.ui.component.sheet.BitchatSheetTitle
@Composable
fun MeshTopologySection(localPeerID: String? = null) {
fun MeshTopologySection(
localPeerID: String? = null,
blePeerIDs: Set<String> = emptySet(),
) {
val colorScheme = MaterialTheme.colorScheme
val graphService = remember { MeshGraphService.getInstance() }
val snapshot by graphService.graphState.collectAsState()
@@ -65,6 +68,7 @@ fun MeshTopologySection(localPeerID: String? = null) {
nodes = nodes,
edges = edges,
wifiAwarePeerIDs = wifiAwarePeerIDs,
blePeerIDs = blePeerIDs,
localPeerID = localPeerID,
modifier = Modifier
.fillMaxWidth()
@@ -225,7 +229,13 @@ fun DebugSettingsSheet(
// Mesh topology visualization (moved below verbose logging)
item {
MeshTopologySection(localPeerID = meshService.myPeerID)
val blePeerIDs = remember(connectedDevices) {
connectedDevices.mapNotNull { it.peerID }.toSet()
}
MeshTopologySection(
localPeerID = meshService.myPeerID,
blePeerIDs = blePeerIDs,
)
}
// GATT controls
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.outlined.Bluetooth
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
@@ -37,6 +38,44 @@ private const val DAMPING = 0.85f
private const val MAX_VELOCITY = 30f
private const val PULSE_DECAY = 0.05f
private const val ROUTE_DECAY = 0.02f
private val EDGE_ICON_OFFSET = 14.dp
private enum class EdgeTransport { WIFI, BLE }
private data class EdgeMarker(val x: Float, val y: Float, val transport: EdgeTransport)
private fun shouldMarkDirectEdge(
edge: MeshGraphService.GraphEdge,
transportPeerIDs: Set<String>,
localID: String?
): Boolean {
if (transportPeerIDs.isEmpty()) return false
return if (localID != null) {
(edge.a == localID && edge.b in transportPeerIDs) ||
(edge.b == localID && edge.a in transportPeerIDs)
} else {
edge.a in transportPeerIDs || edge.b in transportPeerIDs
}
}
private fun edgeMarkerPosition(
x1: Float,
y1: Float,
x2: Float,
y2: Float,
offsetPx: Float,
sideSign: Float
): Pair<Float, Float> {
val midX = (x1 + x2) / 2f
val midY = (y1 + y2) / 2f
val dx = x2 - x1
val dy = y2 - y1
val len = sqrt(dx * dx + dy * dy)
if (len < 0.1f) return midX to midY
val px = -dy / len * offsetPx * sideSign
val py = dx / len * offsetPx * sideSign
return (midX + px) to (midY + py)
}
private class GraphNodeState(
val id: String,
@@ -219,6 +258,7 @@ fun ForceDirectedMeshGraph(
nodes: List<MeshGraphService.GraphNode>,
edges: List<MeshGraphService.GraphEdge>,
wifiAwarePeerIDs: Set<String> = emptySet(),
blePeerIDs: Set<String> = emptySet(),
localPeerID: String? = null,
modifier: Modifier = Modifier
) {
@@ -417,38 +457,46 @@ fun ForceDirectedMeshGraph(
val iconSize = 16.dp
val iconSizePx = with(density) { iconSize.toPx() }
val halfIconSizePx = iconSizePx / 2f
val iconOffsetPx = with(density) { EDGE_ICON_OFFSET.toPx() }
val localID = localPeerID
val shouldMarkWifiEdge: (MeshGraphService.GraphEdge) -> Boolean = { edge ->
if (localID != null) {
(edge.a == localID && edge.b in wifiAwarePeerIDs) ||
(edge.b == localID && edge.a in wifiAwarePeerIDs)
} else {
edge.a in wifiAwarePeerIDs || edge.b in wifiAwarePeerIDs
}
}
val wifiEdgeMidpoints = tick.let {
simulation.edges.mapNotNull { edge ->
val edgeMarkers = tick.let {
simulation.edges.flatMap { edge ->
val n1 = simulation.nodes[edge.a]
val n2 = simulation.nodes[edge.b]
if (n1 != null && n2 != null && shouldMarkWifiEdge(edge)) {
((n1.x + n2.x) / 2f) to ((n1.y + n2.y) / 2f)
} else {
null
if (n1 == null || n2 == null) return@flatMap emptyList()
val isWifi = shouldMarkDirectEdge(edge, wifiAwarePeerIDs, localID)
val isBle = shouldMarkDirectEdge(edge, blePeerIDs, localID)
if (!isWifi && !isBle) return@flatMap emptyList()
val markers = mutableListOf<EdgeMarker>()
if (isWifi) {
val (x, y) = edgeMarkerPosition(n1.x, n1.y, n2.x, n2.y, iconOffsetPx, sideSign = 1f)
markers.add(EdgeMarker(x, y, EdgeTransport.WIFI))
}
if (isBle) {
val side = if (isWifi) -1f else 1f
val (x, y) = edgeMarkerPosition(n1.x, n1.y, n2.x, n2.y, iconOffsetPx, sideSign = side)
markers.add(EdgeMarker(x, y, EdgeTransport.BLE))
}
markers
}
}
wifiEdgeMidpoints.forEach { (midX, midY) ->
edgeMarkers.forEach { marker ->
Icon(
imageVector = Icons.Filled.Wifi,
imageVector = when (marker.transport) {
EdgeTransport.WIFI -> Icons.Filled.Wifi
EdgeTransport.BLE -> Icons.Outlined.Bluetooth
},
contentDescription = null,
tint = Color(0xFF9C27B0).copy(alpha = 0.82f),
tint = colorScheme.onSurface.copy(alpha = 0.82f),
modifier = Modifier
.offset {
IntOffset(
x = (midX - halfIconSizePx).roundToInt(),
y = (midY - halfIconSizePx).roundToInt()
x = (marker.x - halfIconSizePx).roundToInt(),
y = (marker.y - halfIconSizePx).roundToInt()
)
}
.size(iconSize)