mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 02:25:21 +00:00
drop unused connections
This commit is contained in:
@@ -57,6 +57,8 @@ class BluetoothConnectionManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeviceConnected(device: BluetoothDevice) {
|
override fun onDeviceConnected(device: BluetoothDevice) {
|
||||||
|
// Start ANNOUNCE watchdog generically for both client/server connections
|
||||||
|
try { connectionTracker.startAnnounceWatchdog(device.address) } catch (_: Exception) { }
|
||||||
delegate?.onDeviceConnected(device)
|
delegate?.onDeviceConnected(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +89,8 @@ class BluetoothConnectionManager(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
powerManager.delegate = this
|
powerManager.delegate = this
|
||||||
|
// When tracker avoids a device, actively drop its connection (client or server)
|
||||||
|
try { connectionTracker.setOnDeviceAvoided { addr, _ -> forceDisconnectAddress(addr, null) } } catch (_: Exception) { }
|
||||||
// Observe debug settings to enforce role state while active
|
// Observe debug settings to enforce role state while active
|
||||||
try {
|
try {
|
||||||
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
|
val dbg = com.bitchat.android.ui.debug.DebugSettingsManager.getInstance()
|
||||||
@@ -270,6 +274,26 @@ class BluetoothConnectionManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug: avoided devices snapshot (address, reason, since)
|
||||||
|
fun getAvoidedDevices(): List<Triple<String, String, Long>> {
|
||||||
|
return try {
|
||||||
|
val snap = connectionTracker.getAvoidedDevicesSnapshot()
|
||||||
|
snap.map { (addr, info) -> Triple(addr, info.reason, info.since) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: remove one entry from avoid list
|
||||||
|
fun removeFromAvoidList(address: String) {
|
||||||
|
try { connectionTracker.removeFromAvoidList(address) } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: clear the entire avoid list
|
||||||
|
fun clearAvoidList() {
|
||||||
|
try { connectionTracker.clearAvoidList() } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
// Expose local adapter address for debug UI
|
// Expose local adapter address for debug UI
|
||||||
fun getLocalAdapterAddress(): String? = try { bluetoothAdapter?.address } catch (e: Exception) { null }
|
fun getLocalAdapterAddress(): String? = try { bluetoothAdapter?.address } catch (e: Exception) { null }
|
||||||
|
|
||||||
@@ -283,6 +307,36 @@ class BluetoothConnectionManager(
|
|||||||
fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address)
|
fun connectToAddress(address: String): Boolean = clientManager.connectToAddress(address)
|
||||||
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
|
fun disconnectAddress(address: String) { connectionTracker.disconnectDevice(address) }
|
||||||
|
|
||||||
|
// Disconnect a device whether it is a client or server connection; optionally add to avoid list
|
||||||
|
fun forceDisconnectAddress(address: String, blacklistReason: String? = null) {
|
||||||
|
try {
|
||||||
|
// If client-side GATT exists, disconnect via GATT
|
||||||
|
val dc = connectionTracker.getConnectedDevices()[address]
|
||||||
|
if (dc?.gatt != null) {
|
||||||
|
try { dc.gatt.disconnect() } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
// If it's a server-side subscribed device, cancel via GATT server
|
||||||
|
val server = serverManager.getGattServer()
|
||||||
|
if (server != null) {
|
||||||
|
val sub = connectionTracker.getSubscribedDevices().firstOrNull { it.address == address }
|
||||||
|
if (sub != null) {
|
||||||
|
try { server.cancelConnection(sub) } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (blacklistReason != null) {
|
||||||
|
try { connectionTracker.addToAvoidList(address, blacklistReason) } catch (_: Exception) { }
|
||||||
|
} else {
|
||||||
|
connectionTracker.cleanupDeviceConnection(address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for Mesh to acknowledge announce mapping and cancel watchdog
|
||||||
|
fun markAnnounceReceived(deviceAddress: String, peerID: String) {
|
||||||
|
try { connectionTracker.markAnnounceReceived(deviceAddress, peerID) } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Optionally disconnect all connections (server and client)
|
// Optionally disconnect all connections (server and client)
|
||||||
fun disconnectAll() {
|
fun disconnectAll() {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class BluetoothConnectionTracker(
|
|||||||
private const val TAG = "BluetoothConnectionTracker"
|
private const val TAG = "BluetoothConnectionTracker"
|
||||||
private const val CONNECTION_RETRY_DELAY = 5000L
|
private const val CONNECTION_RETRY_DELAY = 5000L
|
||||||
private const val MAX_CONNECTION_ATTEMPTS = 3
|
private const val MAX_CONNECTION_ATTEMPTS = 3
|
||||||
|
private const val DEFAULT_ANNOUNCE_TIMEOUT_MS = 5000L
|
||||||
private const val CLEANUP_DELAY = 500L
|
private const val CLEANUP_DELAY = 500L
|
||||||
private const val CLEANUP_INTERVAL = 30000L // 30 seconds
|
private const val CLEANUP_INTERVAL = 30000L // 30 seconds
|
||||||
}
|
}
|
||||||
@@ -36,9 +37,19 @@ class BluetoothConnectionTracker(
|
|||||||
|
|
||||||
// Connection attempt tracking with automatic cleanup
|
// Connection attempt tracking with automatic cleanup
|
||||||
private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
|
private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
|
||||||
|
// Track consecutive failures per device
|
||||||
|
private val failureCounts = ConcurrentHashMap<String, Int>()
|
||||||
|
// Devices to avoid reconnecting to (blacklist)
|
||||||
|
private val avoidedDevices = ConcurrentHashMap<String, AvoidInfo>()
|
||||||
|
// Announce watchdog jobs per device address
|
||||||
|
private val announceWatchdogs = ConcurrentHashMap<String, kotlinx.coroutines.Job>()
|
||||||
|
// Configurable ANNOUNCE timeout (ms)
|
||||||
|
@Volatile private var announceTimeoutMs: Long = DEFAULT_ANNOUNCE_TIMEOUT_MS
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
private var isActive = false
|
private var isActive = false
|
||||||
|
// Optional callback to actively drop connections when a device is avoided
|
||||||
|
@Volatile private var onDeviceAvoided: ((address: String, reason: String) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consolidated device connection information
|
* Consolidated device connection information
|
||||||
@@ -67,6 +78,14 @@ class BluetoothConnectionTracker(
|
|||||||
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY
|
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata for avoided devices
|
||||||
|
*/
|
||||||
|
data class AvoidInfo(
|
||||||
|
val reason: String,
|
||||||
|
val since: Long = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the connection tracker
|
* Start the connection tracker
|
||||||
*/
|
*/
|
||||||
@@ -91,6 +110,9 @@ class BluetoothConnectionTracker(
|
|||||||
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
|
Log.d(TAG, "Tracker: Adding device connection for $deviceAddress (isClient: ${deviceConn.isClient}")
|
||||||
connectedDevices[deviceAddress] = deviceConn
|
connectedDevices[deviceAddress] = deviceConn
|
||||||
pendingConnections.remove(deviceAddress)
|
pendingConnections.remove(deviceAddress)
|
||||||
|
// Successful connection -> reset failure count and cancel any blacklist for this address
|
||||||
|
failureCounts.remove(deviceAddress)
|
||||||
|
// Do not auto-remove from avoidedDevices to keep defensive policy; only manual removal
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,6 +193,11 @@ class BluetoothConnectionTracker(
|
|||||||
* Check if connection attempt is allowed
|
* Check if connection attempt is allowed
|
||||||
*/
|
*/
|
||||||
fun isConnectionAttemptAllowed(deviceAddress: String): Boolean {
|
fun isConnectionAttemptAllowed(deviceAddress: String): Boolean {
|
||||||
|
// Never attempt to connect to avoided devices
|
||||||
|
if (avoidedDevices.containsKey(deviceAddress)) {
|
||||||
|
Log.d(TAG, "Tracker: Device $deviceAddress is in avoid list; skipping attempts")
|
||||||
|
return false
|
||||||
|
}
|
||||||
val existingAttempt = pendingConnections[deviceAddress]
|
val existingAttempt = pendingConnections[deviceAddress]
|
||||||
return existingAttempt?.let {
|
return existingAttempt?.let {
|
||||||
it.isExpired() || it.shouldRetry()
|
it.isExpired() || it.shouldRetry()
|
||||||
@@ -219,6 +246,89 @@ class BluetoothConnectionTracker(
|
|||||||
pendingConnections.remove(deviceAddress)
|
pendingConnections.remove(deviceAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a connection failure. If failures exceed threshold, avoid device.
|
||||||
|
*/
|
||||||
|
fun recordConnectionFailure(deviceAddress: String, reason: String? = null) {
|
||||||
|
val newCount = (failureCounts[deviceAddress] ?: 0) + 1
|
||||||
|
failureCounts[deviceAddress] = newCount
|
||||||
|
Log.w(TAG, "Tracker: Failure #$newCount for $deviceAddress${reason?.let { ": $it" } ?: ""}")
|
||||||
|
if (newCount >= MAX_CONNECTION_ATTEMPTS) {
|
||||||
|
addToAvoidList(deviceAddress, reason ?: "too_many_failures")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add device to avoid list and cleanup its connection state.
|
||||||
|
*/
|
||||||
|
fun addToAvoidList(deviceAddress: String, reason: String) {
|
||||||
|
Log.w(TAG, "Tracker: Adding $deviceAddress to avoid list (reason: $reason)")
|
||||||
|
avoidedDevices[deviceAddress] = AvoidInfo(reason)
|
||||||
|
// Actively drop connection via callback if provided
|
||||||
|
try { onDeviceAvoided?.invoke(deviceAddress, reason) } catch (_: Exception) { }
|
||||||
|
// Ensure connection state is removed afterwards
|
||||||
|
cleanupDeviceConnection(deviceAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the ANNOUNCE timeout in milliseconds (configurable).
|
||||||
|
*/
|
||||||
|
fun setAnnounceTimeout(timeoutMs: Long) {
|
||||||
|
announceTimeoutMs = timeoutMs.coerceAtLeast(1000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set a callback invoked when a device is added to avoid list. */
|
||||||
|
fun setOnDeviceAvoided(callback: (address: String, reason: String) -> Unit) {
|
||||||
|
onDeviceAvoided = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a watchdog that drops the connection if ANNOUNCE not mapped for this address.
|
||||||
|
*/
|
||||||
|
fun startAnnounceWatchdog(deviceAddress: String) {
|
||||||
|
// Cancel any existing watchdog first
|
||||||
|
cancelAnnounceWatchdog(deviceAddress)
|
||||||
|
val job = connectionScope.launch {
|
||||||
|
val startedAt = System.currentTimeMillis()
|
||||||
|
delay(announceTimeoutMs)
|
||||||
|
// If no ANNOUNCE mapping exists and device still connected, drop and avoid
|
||||||
|
val mapped = addressPeerMap.containsKey(deviceAddress)
|
||||||
|
val stillConnected = connectedDevices.containsKey(deviceAddress) ||
|
||||||
|
subscribedDevices.any { it.address == deviceAddress }
|
||||||
|
if (!mapped && stillConnected) {
|
||||||
|
Log.w(TAG, "Tracker: ANNOUNCE not received within ${announceTimeoutMs}ms for $deviceAddress; dropping + avoiding")
|
||||||
|
addToAvoidList(deviceAddress, "announce_timeout")
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Tracker: ANNOUNCE watchdog passed for $deviceAddress in ${System.currentTimeMillis() - startedAt}ms (mapped=$mapped, connected=$stillConnected)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
announceWatchdogs[deviceAddress] = job
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cancel ANNOUNCE watchdog for an address. */
|
||||||
|
fun cancelAnnounceWatchdog(deviceAddress: String) {
|
||||||
|
announceWatchdogs.remove(deviceAddress)?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Notify tracker that ANNOUNCE was received/mapped for this device. */
|
||||||
|
fun markAnnounceReceived(deviceAddress: String, peerID: String) {
|
||||||
|
// Map is updated externally; we just cancel watchdog here
|
||||||
|
cancelAnnounceWatchdog(deviceAddress)
|
||||||
|
Log.d(TAG, "Tracker: ANNOUNCE mapped for $deviceAddress -> $peerID; watchdog canceled")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove a single device from the avoid list (allow future attempts). */
|
||||||
|
fun removeFromAvoidList(deviceAddress: String) {
|
||||||
|
avoidedDevices.remove(deviceAddress)
|
||||||
|
Log.d(TAG, "Tracker: Removed $deviceAddress from avoid list")
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the entire avoid list (dangerous; for debug UI). */
|
||||||
|
fun clearAvoidList() {
|
||||||
|
avoidedDevices.clear()
|
||||||
|
Log.d(TAG, "Tracker: Cleared avoid list")
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get connected device count
|
* Get connected device count
|
||||||
*/
|
*/
|
||||||
@@ -280,6 +390,7 @@ class BluetoothConnectionTracker(
|
|||||||
addressPeerMap.remove(deviceAddress)
|
addressPeerMap.remove(deviceAddress)
|
||||||
}
|
}
|
||||||
pendingConnections.remove(deviceAddress)
|
pendingConnections.remove(deviceAddress)
|
||||||
|
cancelAnnounceWatchdog(deviceAddress)
|
||||||
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
Log.d(TAG, "Cleaned up device connection for $deviceAddress")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +424,10 @@ class BluetoothConnectionTracker(
|
|||||||
addressPeerMap.clear()
|
addressPeerMap.clear()
|
||||||
pendingConnections.clear()
|
pendingConnections.clear()
|
||||||
scanRSSI.clear()
|
scanRSSI.clear()
|
||||||
|
failureCounts.clear()
|
||||||
|
avoidedDevices.clear()
|
||||||
|
announceWatchdogs.values.forEach { it.cancel() }
|
||||||
|
announceWatchdogs.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -365,10 +480,26 @@ class BluetoothConnectionTracker(
|
|||||||
appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago")
|
appendLine(" - $address: ${attempt.attempts} attempts, last ${elapsed}s ago")
|
||||||
}
|
}
|
||||||
appendLine()
|
appendLine()
|
||||||
|
appendLine("Failures: ${failureCounts.size}")
|
||||||
|
failureCounts.forEach { (address, count) ->
|
||||||
|
appendLine(" - $address: $count failures")
|
||||||
|
}
|
||||||
|
appendLine()
|
||||||
|
appendLine("Avoid List: ${avoidedDevices.size}")
|
||||||
|
avoidedDevices.forEach { (address, info) ->
|
||||||
|
val age = (now - info.since) / 1000
|
||||||
|
appendLine(" - $address: ${info.reason} (${age}s)")
|
||||||
|
}
|
||||||
|
appendLine()
|
||||||
appendLine("Scan RSSI Cache: ${scanRSSI.size}")
|
appendLine("Scan RSSI Cache: ${scanRSSI.size}")
|
||||||
scanRSSI.forEach { (address, rssi) ->
|
scanRSSI.forEach { (address, rssi) ->
|
||||||
appendLine(" - $address: $rssi dBm")
|
appendLine(" - $address: $rssi dBm")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Snapshot of avoided devices for debug UI */
|
||||||
|
fun getAvoidedDevicesSnapshot(): Map<String, AvoidInfo> {
|
||||||
|
return avoidedDevices.toMap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -410,6 +410,8 @@ class BluetoothGattClientManager(
|
|||||||
if (status == 147) {
|
if (status == 147) {
|
||||||
Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress")
|
Log.e(TAG, "Client: Connection establishment failed (status 147) for $deviceAddress")
|
||||||
}
|
}
|
||||||
|
// Record failure for retry policy / blacklist
|
||||||
|
connectionTracker.recordConnectionFailure(deviceAddress, "gatt_status_$status")
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress")
|
Log.d(TAG, "Client: Cleanly disconnected from $deviceAddress")
|
||||||
connectionTracker.cleanupDeviceConnection(deviceAddress)
|
connectionTracker.cleanupDeviceConnection(deviceAddress)
|
||||||
|
|||||||
@@ -359,8 +359,26 @@ class BluetoothMeshService(private val context: Context) {
|
|||||||
// Only set mapping if not already mapped
|
// Only set mapping if not already mapped
|
||||||
if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) {
|
if (!connectionManager.addressPeerMap.containsKey(deviceAddress)) {
|
||||||
connectionManager.addressPeerMap[deviceAddress] = pid
|
connectionManager.addressPeerMap[deviceAddress] = pid
|
||||||
|
// Cancel watchdog and mark announce received
|
||||||
|
connectionManager.markAnnounceReceived(deviceAddress, pid)
|
||||||
Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE")
|
Log.d(TAG, "Mapped device $deviceAddress to peer $pid on ANNOUNCE")
|
||||||
|
|
||||||
|
// If we already have connections for this peer on other devices, drop the older ones
|
||||||
|
try {
|
||||||
|
val duplicates = connectionManager.addressPeerMap
|
||||||
|
.filter { (addr, mappedPid) -> mappedPid == pid && addr != deviceAddress }
|
||||||
|
.map { it.key }
|
||||||
|
if (duplicates.isNotEmpty()) {
|
||||||
|
Log.w(TAG, "Duplicate connections for peer $pid on devices: $duplicates — dropping old ones")
|
||||||
|
duplicates.forEach { otherAddr ->
|
||||||
|
// Blacklist the old devices to avoid reconnect storm
|
||||||
|
connectionManager.forceDisconnectAddress(otherAddr, "duplicate_peer_$pid")
|
||||||
|
// Remove mapping explicitly
|
||||||
|
connectionManager.addressPeerMap.remove(otherAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) { }
|
||||||
|
|
||||||
// Mark this peer as directly connected for UI
|
// Mark this peer as directly connected for UI
|
||||||
try {
|
try {
|
||||||
peerManager.getPeerInfo(pid)?.let {
|
peerManager.getPeerInfo(pid)?.let {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fun DebugSettingsSheet(
|
|||||||
val scanResults by manager.scanResults.collectAsState()
|
val scanResults by manager.scanResults.collectAsState()
|
||||||
val connectedDevices by manager.connectedDevices.collectAsState()
|
val connectedDevices by manager.connectedDevices.collectAsState()
|
||||||
val relayStats by manager.relayStats.collectAsState()
|
val relayStats by manager.relayStats.collectAsState()
|
||||||
|
var avoided by remember { mutableStateOf(listOf<Triple<String, String, Long>>()) }
|
||||||
|
|
||||||
// Push live connected devices from mesh service whenever sheet is visible
|
// Push live connected devices from mesh service whenever sheet is visible
|
||||||
LaunchedEffect(isPresented) {
|
LaunchedEffect(isPresented) {
|
||||||
@@ -71,6 +72,7 @@ fun DebugSettingsSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
manager.updateConnectedDevices(devices)
|
manager.updateConnectedDevices(devices)
|
||||||
|
avoided = meshService.connectionManager.getAvoidedDevices()
|
||||||
kotlinx.coroutines.delay(1000)
|
kotlinx.coroutines.delay(1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,6 +315,47 @@ fun DebugSettingsSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid list (under scan results)
|
||||||
|
item {
|
||||||
|
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||||
|
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Icon(Icons.Filled.Cancel, contentDescription = null, tint = Color(0xFFBF1A1A))
|
||||||
|
Text("avoid list", fontFamily = FontFamily.Monospace, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
Text(
|
||||||
|
"clear",
|
||||||
|
color = Color(0xFFBF1A1A),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
modifier = Modifier.clickable { meshService.connectionManager.clearAvoidList() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (avoided.isEmpty()) {
|
||||||
|
Text("none", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.6f))
|
||||||
|
} else {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
avoided.sortedByDescending { it.third }.forEach { (addr, reason, since) ->
|
||||||
|
val ageSec = ((now - since) / 1000).coerceAtLeast(0)
|
||||||
|
Surface(shape = RoundedCornerShape(8.dp), color = colorScheme.surface.copy(alpha = 0.6f)) {
|
||||||
|
Row(Modifier.fillMaxWidth().padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(addr, fontFamily = FontFamily.Monospace, fontSize = 12.sp)
|
||||||
|
Text("reason: $reason • ${ageSec}s", fontFamily = FontFamily.Monospace, fontSize = 11.sp, color = colorScheme.onSurface.copy(alpha = 0.7f))
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
"remove",
|
||||||
|
color = Color(0xFF007AFF),
|
||||||
|
fontFamily = FontFamily.Monospace,
|
||||||
|
modifier = Modifier.clickable { meshService.connectionManager.removeFromAvoidList(addr) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Debug console
|
// Debug console
|
||||||
item {
|
item {
|
||||||
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
Surface(shape = RoundedCornerShape(12.dp), color = colorScheme.surfaceVariant.copy(alpha = 0.2f)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user