Debug button (#377)

* feat(debug): add DebugSettingsManager + DebugSettingsSheet scaffold and AboutSheet entry; groundwork for verbose logging, GATT controls, relay stats, device/scan views

* fix(debug): wire debug sheet launch via onShowDebug in AboutSheet from ChatScreen; add public start/stop server/client methods}

* fix(about): remove misplaced debug item block inside PoW section; keep debug launcher managed by ChatScreen only

* feat(debug): wire DebugSettingsSheet role switches to explicit startServer/stopServer and startClient/stopClient; expose role controls + disconnectAll on BluetoothConnectionManager; fix syntax error; build passes

* feat(debug): add connect/disconnect helpers; wire DebugSettingsSheet connect/disconnect actions to BluetoothConnectionManager

* feat(debug): add debug settings button at bottom of AboutSheet; wire onShowDebug callback to open DebugSettingsSheet from ChatScreen

* feat(debug): wire verbose logging into chat view via DebugSettingsManager; add rolling relay stats; push connected devices and scan results; ensure GATT role stop closes connections; log incoming packets and relay events; add UI polling for devices; build passes

* chore(debug-branch): remove unrelated files that were mistakenly added in first commit; keep only intended debug settings changes

* fix(chat): prevent mesh→geohash leak

Root cause:
- DebugSettingsManager→chat bridge appended system logs to the global timeline regardless of active channel
- MeshDelegateHandler added public mesh messages to UI unconditionally

Fixes:
- Only inject debug system messages when selected location channel is Mesh
- Only add public mesh messages to UI when Mesh is selected (still send notifications)

Build: ./gradlew assembleDebug
}

* fix(timeline): restore message persistence across channel switches

Root cause:
- NostrGeohashService.switchLocationChannel() called messageManager.clearMessages()
  which wiped ALL messages (mesh + debug + geohash) from main timeline
- Geohash events were adding to main timeline, causing cross-contamination

Fixes:
- Remove messageManager.clearMessages() from channel switching
- ChatScreen displayMessages now routes to separate storage:
  - Mesh: messages (main timeline, includes debug logs when Mesh selected)
  - Geohash: viewModel.getGeohashMessages(geohash) from separate history
  - Private: privateChats[peerID]
  - Channels: channelMessages[channel]
- Geohash events no longer add to main timeline, only to geohash history
- Mesh messages always stored to preserve history when switching away

Result: Each chat type has persistent separate storage, no message loss

* Debug/relay logs: use MessageType names; include device route in verbose packet logs.

* Verbose device-peer assignment and connection/disconnection logs with peerId, deviceId, nickname; packet relay log uses MessageType names.}

* Debug logs API: include nickname and deviceId for incoming/relay; update callers.}

* Fix compile: import MessageType; correct debug manager API usage and remove stray insertion; build.}

* Respect relay toggle; helper to log relay with deviceId.}

* PacketRelayManager: add relay toggle and unified logging helper; clean file header comments; build fixes.}

* changes

* Geohash local echo: do not add to mesh timeline; rely on geohash history and ChatScreen display for location channels.

* persisting

* persist debug settings

* gitignore

* gatt server / client controls

* debugger

* max connection settings and graph

* more graph

* better logging

* refactor logging
This commit is contained in:
callebtc
2025-09-05 15:40:39 +02:00
committed by GitHub
parent 91f3f270d4
commit f47819a31e
19 changed files with 1318 additions and 79 deletions
@@ -6,6 +6,7 @@ import android.util.Log
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.BitchatPacket
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collect
/**
* Power-optimized Bluetooth connection manager with comprehensive memory management
@@ -86,6 +87,44 @@ class BluetoothConnectionManager(
init {
powerManager.delegate = this
// Observe debug settings to enforce role state while active
try {
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
// Role enable/disable
connectionScope.launch {
dbg.gattServerEnabled.collect { enabled ->
if (!isActive) return@collect
if (enabled) startServer() else stopServer()
}
}
connectionScope.launch {
dbg.gattClientEnabled.collect { enabled ->
if (!isActive) return@collect
if (enabled) startClient() else stopClient()
}
}
// Connection caps: enforce on change
connectionScope.launch {
dbg.maxConnectionsOverall.collect {
if (!isActive) return@collect
connectionTracker.enforceConnectionLimits()
// Also enforce server side best-effort
serverManager.enforceServerLimit(dbg.maxServerConnections.value)
}
}
connectionScope.launch {
dbg.maxClientConnections.collect {
if (!isActive) return@collect
connectionTracker.enforceConnectionLimits()
}
}
connectionScope.launch {
dbg.maxServerConnections.collect {
if (!isActive) return@collect
serverManager.enforceServerLimit(dbg.maxServerConnections.value)
}
}
} catch (_: Exception) { }
}
/**
@@ -125,18 +164,29 @@ class BluetoothConnectionManager(
// Start power manager
powerManager.start()
// Start server manager
if (!serverManager.start()) {
Log.e(TAG, "Failed to start server manager")
this@BluetoothConnectionManager.isActive = false
return@launch
// Start server/client based on debug settings
val dbg = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (_: Exception) { null }
val startServer = dbg?.gattServerEnabled?.value != false
val startClient = dbg?.gattClientEnabled?.value != false
if (startServer) {
if (!serverManager.start()) {
Log.e(TAG, "Failed to start server manager")
this@BluetoothConnectionManager.isActive = false
return@launch
}
} else {
Log.i(TAG, "GATT Server disabled by debug settings; not starting")
}
// Start client manager
if (!clientManager.start()) {
Log.e(TAG, "Failed to start client manager")
this@BluetoothConnectionManager.isActive = false
return@launch
if (startClient) {
if (!clientManager.start()) {
Log.e(TAG, "Failed to start client manager")
this@BluetoothConnectionManager.isActive = false
return@launch
}
} else {
Log.i(TAG, "GATT Client disabled by debug settings; not starting")
}
Log.i(TAG, "Bluetooth services started successfully")
@@ -198,6 +248,58 @@ class BluetoothConnectionManager(
)
}
// Expose role controls for debug UI
fun startServer() { connectionScope.launch { serverManager.start() } }
fun stopServer() { connectionScope.launch { serverManager.stop() } }
fun startClient() { connectionScope.launch { clientManager.start() } }
fun stopClient() { connectionScope.launch { clientManager.stop() } }
// Inject nickname resolver for broadcaster logs
fun setNicknameResolver(resolver: (String) -> String?) { packetBroadcaster.setNicknameResolver(resolver) }
// Debug snapshots for connected devices
fun getConnectedDeviceEntries(): List<Triple<String, Boolean, Int?>> {
return try {
connectionTracker.getConnectedDevices().values.map { dc ->
val rssi = if (dc.rssi != Int.MIN_VALUE) dc.rssi else null
Triple(dc.device.address, dc.isClient, rssi)
}
} catch (e: Exception) {
emptyList()
}
}
// Expose local adapter address for debug UI
fun getLocalAdapterAddress(): String? = try { bluetoothAdapter?.address } catch (e: Exception) { null }
fun isClientConnection(address: String): Boolean? {
return try { connectionTracker.getConnectedDevices()[address]?.isClient } catch (e: Exception) { null }
}
/**
* Public: connect/disconnect helpers for debug UI
*/
fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address)
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
// Optionally disconnect all connections (server and client)
fun disconnectAll() {
connectionScope.launch {
// Stop and restart to force disconnects
clientManager.stop()
serverManager.stop()
delay(200)
if (isActive) {
// Restart managers if service is active
serverManager.start()
clientManager.start()
}
}
}
/**
* Get connected device count
*/
@@ -230,20 +332,35 @@ class BluetoothConnectionManager(
// Avoid rapid scan restarts by checking if we need to change scan behavior
val wasUsingDutyCycle = powerManager.shouldUseDutyCycle()
// Update advertising with new power settings
serverManager.restartAdvertising()
// Update advertising with new power settings if server enabled
val serverEnabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
if (serverEnabled) {
serverManager.restartAdvertising()
} else {
serverManager.stop()
}
// Only restart scanning if the duty cycle behavior changed
val nowUsingDutyCycle = powerManager.shouldUseDutyCycle()
if (wasUsingDutyCycle != nowUsingDutyCycle) {
Log.d(TAG, "Duty cycle behavior changed (${wasUsingDutyCycle} -> ${nowUsingDutyCycle}), restarting scan")
clientManager.restartScanning()
val clientEnabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
if (clientEnabled) {
clientManager.restartScanning()
} else {
clientManager.stop()
}
} else {
Log.d(TAG, "Duty cycle behavior unchanged, keeping existing scan state")
}
// Enforce connection limits
connectionTracker.enforceConnectionLimits()
// Best-effort server cap
try {
val maxServer = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().maxServerConnections.value
serverManager.enforceServerLimit(maxServer)
} catch (_: Exception) { }
}
}
@@ -201,6 +201,17 @@ class BluetoothConnectionTracker(
}
}
/**
* Disconnect a specific device (by MAC address)
*/
fun disconnectDevice(deviceAddress: String) {
connectedDevices[deviceAddress]?.gatt?.let {
try { it.disconnect() } catch (_: Exception) { }
}
cleanupDeviceConnection(deviceAddress)
Log.d(TAG, "Requested disconnect for $deviceAddress")
}
/**
* Remove a pending connection
*/
@@ -224,19 +235,38 @@ class BluetoothConnectionTracker(
* Enforce connection limits by disconnecting oldest connections
*/
fun enforceConnectionLimits() {
val maxConnections = powerManager.getMaxConnections()
if (connectedDevices.size > maxConnections) {
Log.i(TAG, "Enforcing connection limit: ${connectedDevices.size} > $maxConnections")
// Disconnect oldest client connections first
val sortedConnections = connectedDevices.values
.filter { it.isClient }
// Read debug overrides if available
val dbg = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (_: Exception) { null }
val maxOverall = dbg?.maxConnectionsOverall?.value ?: powerManager.getMaxConnections()
val maxClient = dbg?.maxClientConnections?.value ?: maxOverall
val maxServer = dbg?.maxServerConnections?.value ?: maxOverall
val clients = connectedDevices.values.filter { it.isClient }
val servers = connectedDevices.values.filter { !it.isClient }
// Enforce client cap first (we can actively disconnect)
if (clients.size > maxClient) {
Log.i(TAG, "Enforcing client cap: ${clients.size} > $maxClient")
val toDisconnect = clients.sortedBy { it.connectedAt }.take(clients.size - maxClient)
toDisconnect.forEach { dc ->
Log.d(TAG, "Disconnecting client ${dc.device.address} due to client cap")
dc.gatt?.disconnect()
}
}
// Note: server cap enforced in GattServerManager (we don't have server handle here)
// Enforce overall cap by disconnecting oldest client connections
if (connectedDevices.size > maxOverall) {
Log.i(TAG, "Enforcing overall cap: ${connectedDevices.size} > $maxOverall")
val excess = connectedDevices.size - maxOverall
val toDisconnect = connectedDevices.values
.filter { it.isClient } // only clients from here
.sortedBy { it.connectedAt }
val toDisconnect = sortedConnections.take(connectedDevices.size - maxConnections)
toDisconnect.forEach { deviceConn ->
Log.d(TAG, "Disconnecting ${deviceConn.device.address} due to connection limit")
deviceConn.gatt?.disconnect()
.take(excess)
toDisconnect.forEach { dc ->
Log.d(TAG, "Disconnecting client ${dc.device.address} due to overall cap")
dc.gatt?.disconnect()
}
}
}
@@ -341,4 +371,4 @@ class BluetoothConnectionTracker(
}
}
}
}
}
@@ -14,6 +14,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.util.*
import kotlinx.coroutines.Job
import com.bitchat.android.ui.debug.DebugSettingsManager
import com.bitchat.android.ui.debug.DebugScanResult
/**
* Manages GATT client operations, scanning, and client-side connections
@@ -44,6 +46,21 @@ class BluetoothGattClientManager(
private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
private val bleScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
/**
* Public: Connect to a device by MAC address (for debug UI)
*/
fun connectToAddress(deviceAddress: String): Boolean {
val device = bluetoothAdapter?.getRemoteDevice(deviceAddress)
return if (device != null) {
val rssi = connectionTracker.getBestRSSI(deviceAddress) ?: -50
connectToDevice(device, rssi)
true
} else {
Log.w(TAG, "connectToAddress: No device for $deviceAddress")
false
}
}
// Scan management
private var scanCallback: ScanCallback? = null
@@ -63,6 +80,18 @@ class BluetoothGattClientManager(
* Start client manager
*/
fun start(): Boolean {
// Respect debug setting
try {
if (!com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value) {
Log.i(TAG, "Client start skipped: GATT Client disabled in debug settings")
return false
}
} catch (_: Exception) { }
if (isActive) {
Log.d(TAG, "GATT client already active; start is a no-op")
return true
}
if (!permissionManager.hasBluetoothPermissions()) {
Log.e(TAG, "Missing Bluetooth permissions")
return false
@@ -98,9 +127,25 @@ class BluetoothGattClientManager(
* Stop client manager
*/
fun stop() {
if (!isActive) {
// Idempotent stop
stopScanning()
stopRSSIMonitoring()
Log.i(TAG, "GATT client manager stopped (already inactive)")
return
}
isActive = false
connectionScope.launch {
// Disconnect all client connections decisively
try {
val conns = connectionTracker.getConnectedDevices().values.filter { it.isClient && it.gatt != null }
conns.forEach { dc ->
try { dc.gatt?.disconnect() } catch (_: Exception) { }
}
} catch (_: Exception) { }
stopScanning()
stopRSSIMonitoring()
Log.i(TAG, "GATT client manager stopped")
@@ -111,7 +156,8 @@ class BluetoothGattClientManager(
* Handle scan state changes from power manager
*/
fun onScanStateChanged(shouldScan: Boolean) {
if (shouldScan) {
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
if (shouldScan && enabled) {
startScanning()
} else {
stopScanning()
@@ -158,7 +204,9 @@ class BluetoothGattClientManager(
*/
@Suppress("DEPRECATION")
private fun startScanning() {
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null || !isActive) return
// Respect debug setting
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
if (!permissionManager.hasBluetoothPermissions() || bleScanner == null || !isActive || !enabled) return
// Rate limit scan starts to prevent "scanning too frequently" errors
val currentTime = System.currentTimeMillis()
@@ -282,10 +330,34 @@ class BluetoothGattClientManager(
// Store RSSI from scan results for later use (especially for server connections)
connectionTracker.updateScanRSSI(deviceAddress, rssi)
// Publish scan result to debug UI buffer
try {
DebugSettingsManager.getInstance().addScanResult(
DebugScanResult(
deviceName = device.name,
deviceAddress = deviceAddress,
rssi = rssi,
peerID = null // peerID unknown at scan time
)
)
} catch (_: Exception) { }
// Power-aware RSSI filtering
if (rssi < powerManager.getRSSIThreshold()) {
Log.d(TAG, "Skipping device $deviceAddress due to weak signal: $rssi < ${powerManager.getRSSIThreshold()}")
// Even if we skip connecting, still publish scan result to debug UI
try {
val pid: String? = null // We don't know peerID until packet exchange
DebugSettingsManager.getInstance().addScanResult(
DebugScanResult(
deviceName = device.name,
deviceAddress = deviceAddress,
rssi = rssi,
peerID = pid
)
)
} catch (_: Exception) { }
return
}
@@ -474,7 +546,9 @@ class BluetoothGattClientManager(
* Restart scanning for power mode changes
*/
fun restartScanning() {
if (!isActive) return
// Respect debug setting
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattClientEnabled.value } catch (_: Exception) { true }
if (!isActive || !enabled) return
connectionScope.launch {
stopScanning()
@@ -489,4 +563,4 @@ class BluetoothGattClientManager(
}
}
}
}
}
@@ -47,11 +47,37 @@ class BluetoothGattServerManager(
// State management
private var isActive = false
// Enforce a server connection limit by canceling the oldest connections (best-effort)
fun enforceServerLimit(maxServer: Int) {
if (maxServer <= 0) return
try {
val subs = connectionTracker.getSubscribedDevices()
if (subs.size > maxServer) {
val excess = subs.size - maxServer
subs.take(excess).forEach { d ->
try { gattServer?.cancelConnection(d) } catch (_: Exception) { }
}
}
} catch (_: Exception) { }
}
/**
* Start GATT server
*/
fun start(): Boolean {
// Respect debug setting
try {
if (!com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value) {
Log.i(TAG, "Server start skipped: GATT Server disabled in debug settings")
return false
}
} catch (_: Exception) { }
if (isActive) {
Log.d(TAG, "GATT server already active; start is a no-op")
return true
}
if (!permissionManager.hasBluetoothPermissions()) {
Log.e(TAG, "Missing Bluetooth permissions")
return false
@@ -82,11 +108,29 @@ class BluetoothGattServerManager(
* Stop GATT server
*/
fun stop() {
if (!isActive) {
// Idempotent stop
stopAdvertising()
// Ensure server is closed if present
gattServer?.close()
gattServer = null
Log.i(TAG, "GATT server stopped (already inactive)")
return
}
isActive = false
connectionScope.launch {
stopAdvertising()
// Try to cancel any active connections explicitly before closing
try {
val devices = connectionTracker.getSubscribedDevices()
devices.forEach { d ->
try { gattServer?.cancelConnection(d) } catch (_: Exception) { }
}
} catch (_: Exception) { }
// Close GATT server
gattServer?.close()
gattServer = null
@@ -281,7 +325,9 @@ class BluetoothGattServerManager(
*/
@Suppress("DEPRECATION")
private fun startAdvertising() {
if (!permissionManager.hasBluetoothPermissions() || bleAdvertiser == null || !isActive || bluetoothAdapter == null || !bluetoothAdapter.isMultipleAdvertisementSupported()) {
// 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")
}
@@ -327,12 +373,17 @@ class BluetoothGattServerManager(
* Restart advertising (for power mode changes)
*/
fun restartAdvertising() {
if (!isActive) return
// Respect debug setting
val enabled = try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().gattServerEnabled.value } catch (_: Exception) { true }
if (!isActive || !enabled) {
stopAdvertising()
return
}
connectionScope.launch {
stopAdvertising()
delay(100)
startAdvertising()
}
}
}
}
@@ -30,6 +30,7 @@ import kotlin.random.Random
* - PacketProcessor: Incoming packet routing
*/
class BluetoothMeshService(private val context: Context) {
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
companion object {
private const val TAG = "BluetoothMeshService"
@@ -103,6 +104,10 @@ class BluetoothMeshService(private val context: Context) {
* Setup delegate connections between components
*/
private fun setupDelegates() {
// Provide nickname resolver to BLE broadcaster for detailed logs
try {
connectionManager.setNicknameResolver { pid -> peerManager.getPeerNickname(pid) }
} catch (_: Exception) { }
// PeerManager delegates to main mesh service delegate
peerManager.delegate = object : PeerManagerDelegate {
override fun onPeerListUpdated(peerIDs: List<String>) {
@@ -410,6 +415,14 @@ class BluetoothMeshService(private val context: Context) {
delay(200)
sendBroadcastAnnounce()
}
// Verbose debug: device connected
try {
val addr = device.address
val peer = connectionManager.addressPeerMap[addr]
val nick = peer?.let { peerManager.getPeerNickname(it) } ?: "unknown"
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
.logPeerConnection(peer ?: "unknown", nick, addr, isInbound = !connectionManager.isClientConnection(addr)!!)
} catch (_: Exception) { }
}
override fun onDeviceDisconnected(device: android.bluetooth.BluetoothDevice) {
@@ -424,6 +437,12 @@ class BluetoothMeshService(private val context: Context) {
// Peer might still be reachable indirectly; mark as not-direct
try { peerManager.setDirectConnection(peer, false) } catch (_: Exception) { }
}
// Verbose debug: device disconnected
try {
val nick = peerManager.getPeerNickname(peer) ?: "unknown"
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
.logPeerDisconnection(peer, nick, addr)
} catch (_: Exception) { }
}
}
@@ -7,6 +7,8 @@ import android.bluetooth.BluetoothGattServer
import android.util.Log
import com.bitchat.android.protocol.SpecialRecipients
import com.bitchat.android.model.RoutedPacket
import com.bitchat.android.protocol.MessageType
import com.bitchat.android.util.toHexString
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -44,6 +46,47 @@ class BluetoothPacketBroadcaster(
private const val TAG = "BluetoothPacketBroadcaster"
private const val CLEANUP_DELAY = 500L
}
// Optional nickname resolver injected by higher layer (peerID -> nickname?)
private var nicknameResolver: ((String) -> String?)? = null
fun setNicknameResolver(resolver: (String) -> String?) {
nicknameResolver = resolver
}
/**
* Debug logging helper - can be easily removed/disabled for production
*/
private fun logPacketRelay(
typeName: String,
senderPeerID: String,
senderNick: String?,
incomingPeer: String?,
incomingAddr: String?,
toPeer: String?,
toDeviceAddress: String,
ttl: UByte
) {
try {
val fromNick = incomingPeer?.let { nicknameResolver?.invoke(it) }
val toNick = toPeer?.let { nicknameResolver?.invoke(it) }
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().logPacketRelayDetailed(
packetType = typeName,
senderPeerID = senderPeerID,
senderNickname = senderNick,
fromPeerID = incomingPeer,
fromNickname = fromNick,
fromDeviceAddress = incomingAddr,
toPeerID = toPeer,
toNickname = toNick,
toDeviceAddress = toDeviceAddress,
ttl = ttl
)
} catch (_: Exception) {
// Silently ignore debug logging failures
}
}
// Data class to hold broadcast request information
private data class BroadcastRequest(
@@ -127,6 +170,11 @@ class BluetoothPacketBroadcaster(
) {
val packet = routed.packet
val data = packet.toBinaryData() ?: return
val typeName = MessageType.fromValue(packet.type)?.name ?: packet.type.toString()
val senderPeerID = routed.peerID ?: packet.senderID.toHexString()
val incomingAddr = routed.relayAddress
val incomingPeer = incomingAddr?.let { connectionTracker.addressPeerMap[it] }
val senderNick = senderPeerID.let { pid -> nicknameResolver?.invoke(pid) }
if (packet.recipientID != SpecialRecipients.BROADCAST) {
val recipientID = packet.recipientID?.let {
@@ -140,8 +188,11 @@ class BluetoothPacketBroadcaster(
// If found, send directly
if (targetDevice != null) {
Log.d(TAG, "Send packet type ${packet.type} directly to target device for recipient $recipientID: ${targetDevice.address}")
if (notifyDevice(targetDevice, data, gattServer, characteristic))
if (notifyDevice(targetDevice, data, gattServer, characteristic)) {
val toPeer = connectionTracker.addressPeerMap[targetDevice.address]
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDevice.address, packet.ttl)
return // Sent, no need to continue
}
}
// Try to find the recipient in client connections (connectedDevices)
@@ -151,8 +202,11 @@ class BluetoothPacketBroadcaster(
// If found, send directly
if (targetDeviceConn != null) {
Log.d(TAG, "Send packet type ${packet.type} directly to target client connection for recipient $recipientID: ${targetDeviceConn.device.address}")
if (writeToDeviceConn(targetDeviceConn, data))
if (writeToDeviceConn(targetDeviceConn, data)) {
val toPeer = connectionTracker.addressPeerMap[targetDeviceConn.device.address]
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, targetDeviceConn.device.address, packet.ttl)
return // Sent, no need to continue
}
}
}
@@ -174,7 +228,11 @@ class BluetoothPacketBroadcaster(
Log.d(TAG, "Skipping broadcast to client back to sender: ${device.address}")
return@forEach
}
notifyDevice(device, data, gattServer, characteristic)
val sent = notifyDevice(device, data, gattServer, characteristic)
if (sent) {
val toPeer = connectionTracker.addressPeerMap[device.address]
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, device.address, packet.ttl)
}
}
// Send to client connections (GATT servers we are connected to)
@@ -188,7 +246,11 @@ class BluetoothPacketBroadcaster(
Log.d(TAG, "Skipping roadcast to server back to sender: ${deviceConn.device.address}")
return@forEach
}
writeToDeviceConn(deviceConn, data)
val sent = writeToDeviceConn(deviceConn, data)
if (sent) {
val toPeer = connectionTracker.addressPeerMap[deviceConn.device.address]
logPacketRelay(typeName, senderPeerID, senderNick, incomingPeer, incomingAddr, toPeer, deviceConn.device.address, packet.ttl)
}
}
}
}
@@ -268,4 +330,4 @@ class BluetoothPacketBroadcaster(
Log.d(TAG, "BluetoothPacketBroadcaster shutdown complete")
}
}
}
@@ -16,6 +16,7 @@ import kotlinx.coroutines.channels.actor
* from the same peer simultaneously, causing session management conflicts.
*/
class PacketProcessor(private val myPeerID: String) {
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
companion object {
private const val TAG = "PacketProcessor"
@@ -128,8 +129,16 @@ class PacketProcessor(private val myPeerID: String) {
}
var validPacket = true
Log.d(TAG, "Processing packet type ${MessageType.fromValue(packet.type)} from ${formatPeerForLog(peerID)}")
val messageType = MessageType.fromValue(packet.type)
Log.d(TAG, "Processing packet type ${messageType} from ${formatPeerForLog(peerID)}")
// Verbose logging to debug manager (and chat via ChatViewModel observer)
try {
val mt = messageType?.name ?: packet.type.toString()
val routeDevice = routed.relayAddress
val nick = delegate?.getPeerNickname(peerID)
debugManager?.logIncomingPacket(peerID, nick, mt, routeDevice)
} catch (_: Exception) { }
// Handle public packet types (no address check needed)
when (messageType) {
@@ -1,4 +1,5 @@
package com.bitchat.android.mesh
import com.bitchat.android.protocol.MessageType
import android.util.Log
import com.bitchat.android.model.RoutedPacket
@@ -14,11 +15,18 @@ import kotlin.random.Random
* All packets that aren't specifically addressed to us get processed here.
*/
class PacketRelayManager(private val myPeerID: String) {
private val debugManager by lazy { try { com.bitchat.android.ui.debug.DebugSettingsManager.getInstance() } catch (e: Exception) { null } }
companion object {
private const val TAG = "PacketRelayManager"
}
private fun isRelayEnabled(): Boolean = try {
com.bitchat.android.ui.debug.DebugSettingsManager.getInstance().packetRelayEnabled.value
} catch (_: Exception) { true }
// Logging moved to BluetoothPacketBroadcaster per actual transmission target
// Delegate for callbacks
var delegate: PacketRelayManagerDelegate? = null
@@ -33,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)) {
@@ -55,15 +63,15 @@ 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}")
// Apply relay logic based on packet type
val shouldRelay = shouldRelayPacket(relayPacket, peerID)
// Apply relay logic based on packet type and debug switch
val shouldRelay = isRelayEnabled() && shouldRelayPacket(relayPacket, peerID)
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}")
}
}
@@ -95,7 +103,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
}
@@ -104,7 +112,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
}
@@ -118,7 +126,7 @@ 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
}
@@ -151,11 +159,11 @@ class PacketRelayManager(private val myPeerID: String) {
if (shouldRelay) {
val delay = Random.nextLong(50, 500) // Random delay like iOS
Log.d(TAG, "Relaying message after ${delay}ms delay")
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)")
Log.d(TAG, "Relay decision: NOT relaying message (network size: ${'$'}networkSize, prob: ${'$'}relayProb)")
}
}
@@ -163,7 +171,7 @@ class PacketRelayManager(private val myPeerID: String) {
* 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)
}
@@ -173,9 +181,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\"}")
}
}