mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 00:25:20 +00:00
unify connection tracker for ble and wifi (#537)
* unify connection tracker for ble and wifi * cleanup nicely
This commit is contained in:
@@ -16,14 +16,11 @@ import java.util.concurrent.CopyOnWriteArrayList
|
|||||||
class BluetoothConnectionTracker(
|
class BluetoothConnectionTracker(
|
||||||
private val connectionScope: CoroutineScope,
|
private val connectionScope: CoroutineScope,
|
||||||
private val powerManager: PowerManager
|
private val powerManager: PowerManager
|
||||||
) {
|
) : MeshConnectionTracker(connectionScope, TAG) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BluetoothConnectionTracker"
|
private const val TAG = "BluetoothConnectionTracker"
|
||||||
private const val CONNECTION_RETRY_DELAY = com.bitchat.android.util.AppConstants.Mesh.CONNECTION_RETRY_DELAY_MS
|
|
||||||
private const val MAX_CONNECTION_ATTEMPTS = com.bitchat.android.util.AppConstants.Mesh.MAX_CONNECTION_ATTEMPTS
|
|
||||||
private const val CLEANUP_DELAY = com.bitchat.android.util.AppConstants.Mesh.CONNECTION_CLEANUP_DELAY_MS
|
private const val CLEANUP_DELAY = com.bitchat.android.util.AppConstants.Mesh.CONNECTION_CLEANUP_DELAY_MS
|
||||||
private const val CLEANUP_INTERVAL = com.bitchat.android.util.AppConstants.Mesh.CONNECTION_CLEANUP_INTERVAL_MS // 30 seconds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection tracking - reduced memory footprint
|
// Connection tracking - reduced memory footprint
|
||||||
@@ -36,12 +33,6 @@ class BluetoothConnectionTracker(
|
|||||||
// RSSI tracking from scan results (for devices we discover but may connect as servers)
|
// RSSI tracking from scan results (for devices we discover but may connect as servers)
|
||||||
private val scanRSSI = ConcurrentHashMap<String, Int>()
|
private val scanRSSI = ConcurrentHashMap<String, Int>()
|
||||||
|
|
||||||
// Connection attempt tracking with automatic cleanup
|
|
||||||
private val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
|
|
||||||
|
|
||||||
// State management
|
|
||||||
private var isActive = false
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consolidated device connection information
|
* Consolidated device connection information
|
||||||
*/
|
*/
|
||||||
@@ -54,37 +45,28 @@ class BluetoothConnectionTracker(
|
|||||||
val connectedAt: Long = System.currentTimeMillis()
|
val connectedAt: Long = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
override fun start() {
|
||||||
* Connection attempt tracking with automatic expiry
|
super.start()
|
||||||
*/
|
|
||||||
data class ConnectionAttempt(
|
|
||||||
val attempts: Int,
|
|
||||||
val lastAttempt: Long = System.currentTimeMillis()
|
|
||||||
) {
|
|
||||||
fun isExpired(): Boolean =
|
|
||||||
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY * 2
|
|
||||||
|
|
||||||
fun shouldRetry(): Boolean =
|
|
||||||
attempts < MAX_CONNECTION_ATTEMPTS &&
|
|
||||||
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
override fun stop() {
|
||||||
* Start the connection tracker
|
super.stop()
|
||||||
*/
|
|
||||||
fun start() {
|
|
||||||
isActive = true
|
|
||||||
startPeriodicCleanup()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the connection tracker
|
|
||||||
*/
|
|
||||||
fun stop() {
|
|
||||||
isActive = false
|
|
||||||
cleanupAllConnections()
|
cleanupAllConnections()
|
||||||
clearAllConnections()
|
clearAllConnections()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Abstract implementations
|
||||||
|
override fun isConnected(id: String): Boolean = connectedDevices.containsKey(id)
|
||||||
|
|
||||||
|
override fun disconnect(id: String) {
|
||||||
|
connectedDevices[id]?.gatt?.let {
|
||||||
|
try { it.disconnect() } catch (_: Exception) { }
|
||||||
|
}
|
||||||
|
cleanupDeviceConnection(id)
|
||||||
|
Log.d(TAG, "Requested disconnect for $id")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConnectionCount(): Int = connectedDevices.size
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a device connection
|
* Add a device connection
|
||||||
@@ -92,7 +74,7 @@ class BluetoothConnectionTracker(
|
|||||||
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
|
fun addDeviceConnection(deviceAddress: String, deviceConn: DeviceConnection) {
|
||||||
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)
|
removePendingConnection(deviceAddress)
|
||||||
// Mark as awaiting first ANNOUNCE on this connection
|
// Mark as awaiting first ANNOUNCE on this connection
|
||||||
firstAnnounceSeen[deviceAddress] = false
|
firstAnnounceSeen[deviceAddress] = false
|
||||||
}
|
}
|
||||||
@@ -167,67 +149,17 @@ class BluetoothConnectionTracker(
|
|||||||
/**
|
/**
|
||||||
* Check if device is already connected
|
* Check if device is already connected
|
||||||
*/
|
*/
|
||||||
fun isDeviceConnected(deviceAddress: String): Boolean {
|
fun isDeviceConnected(deviceAddress: String): Boolean = isConnected(deviceAddress)
|
||||||
return connectedDevices.containsKey(deviceAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if connection attempt is allowed
|
|
||||||
*/
|
|
||||||
fun isConnectionAttemptAllowed(deviceAddress: String): Boolean {
|
|
||||||
val existingAttempt = pendingConnections[deviceAddress]
|
|
||||||
return existingAttempt?.let {
|
|
||||||
it.isExpired() || it.shouldRetry()
|
|
||||||
} ?: true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a pending connection attempt
|
|
||||||
*/
|
|
||||||
fun addPendingConnection(deviceAddress: String): Boolean {
|
|
||||||
Log.d(TAG, "Tracker: Adding pending connection for $deviceAddress")
|
|
||||||
synchronized(pendingConnections) {
|
|
||||||
// Double-check inside synchronized block
|
|
||||||
val currentAttempt = pendingConnections[deviceAddress]
|
|
||||||
if (currentAttempt != null && !currentAttempt.isExpired() && !currentAttempt.shouldRetry()) {
|
|
||||||
Log.d(TAG, "Tracker: Connection attempt already in progress for $deviceAddress")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (currentAttempt != null) {
|
|
||||||
Log.d(TAG, "Tracker: current attempt: $currentAttempt")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update connection attempt atomically
|
|
||||||
// If the previous attempt window expired, reset backoff to 1; otherwise increment
|
|
||||||
val attempts = if (currentAttempt?.isExpired() == true) 1 else (currentAttempt?.attempts ?: 0) + 1
|
|
||||||
pendingConnections[deviceAddress] = ConnectionAttempt(attempts)
|
|
||||||
Log.d(TAG, "Tracker: Added pending connection for $deviceAddress (attempts: $attempts)")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect a specific device (by MAC address)
|
* Disconnect a specific device (by MAC address)
|
||||||
*/
|
*/
|
||||||
fun disconnectDevice(deviceAddress: String) {
|
fun disconnectDevice(deviceAddress: String) = disconnect(deviceAddress)
|
||||||
connectedDevices[deviceAddress]?.gatt?.let {
|
|
||||||
try { it.disconnect() } catch (_: Exception) { }
|
|
||||||
}
|
|
||||||
cleanupDeviceConnection(deviceAddress)
|
|
||||||
Log.d(TAG, "Requested disconnect for $deviceAddress")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a pending connection
|
|
||||||
*/
|
|
||||||
fun removePendingConnection(deviceAddress: String) {
|
|
||||||
pendingConnections.remove(deviceAddress)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get connected device count
|
* Get connected device count
|
||||||
*/
|
*/
|
||||||
fun getConnectedDeviceCount(): Int = connectedDevices.size
|
fun getConnectedDeviceCount(): Int = getConnectionCount()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if connection limit is reached
|
* Check if connection limit is reached
|
||||||
@@ -335,36 +267,6 @@ class BluetoothConnectionTracker(
|
|||||||
return firstAnnounceSeen[deviceAddress] == true
|
return firstAnnounceSeen[deviceAddress] == true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start periodic cleanup of expired connections
|
|
||||||
*/
|
|
||||||
private fun startPeriodicCleanup() {
|
|
||||||
connectionScope.launch {
|
|
||||||
while (isActive) {
|
|
||||||
delay(CLEANUP_INTERVAL)
|
|
||||||
|
|
||||||
if (!isActive) break
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Clean up expired pending connections
|
|
||||||
val expiredConnections = pendingConnections.filter { it.value.isExpired() }
|
|
||||||
expiredConnections.keys.forEach { pendingConnections.remove(it) }
|
|
||||||
|
|
||||||
// Log cleanup if any
|
|
||||||
if (expiredConnections.isNotEmpty()) {
|
|
||||||
Log.d(TAG, "Cleaned up ${expiredConnections.size} expired connection attempts")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log current state
|
|
||||||
Log.d(TAG, "Periodic cleanup: ${connectedDevices.size} connections, ${pendingConnections.size} pending")
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(TAG, "Error in periodic cleanup: ${e.message}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get debug information
|
* Get debug information
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package com.bitchat.android.mesh
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base tracker for mesh connections (BLE, Wi-Fi Aware, etc.)
|
||||||
|
* Encapsulates common state machine logic:
|
||||||
|
* - Connection attempt tracking (retries, backoff)
|
||||||
|
* - Pending connection management
|
||||||
|
* - Automatic cleanup of expired attempts
|
||||||
|
*/
|
||||||
|
abstract class MeshConnectionTracker(
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
protected val tag: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
const val CONNECTION_RETRY_DELAY = 5_000L
|
||||||
|
const val MAX_CONNECTION_ATTEMPTS = 3
|
||||||
|
const val CLEANUP_INTERVAL = 30_000L
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connection attempt tracking with automatic expiry
|
||||||
|
*/
|
||||||
|
protected data class ConnectionAttempt(
|
||||||
|
val attempts: Int,
|
||||||
|
val lastAttempt: Long = System.currentTimeMillis()
|
||||||
|
) {
|
||||||
|
fun isExpired(): Boolean =
|
||||||
|
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY * 2
|
||||||
|
|
||||||
|
fun shouldRetry(): Boolean =
|
||||||
|
attempts < MAX_CONNECTION_ATTEMPTS &&
|
||||||
|
System.currentTimeMillis() - lastAttempt > CONNECTION_RETRY_DELAY
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tracks in-progress or failed attempts
|
||||||
|
protected val pendingConnections = ConcurrentHashMap<String, ConnectionAttempt>()
|
||||||
|
|
||||||
|
private var isActive = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the tracker and its cleanup loop
|
||||||
|
*/
|
||||||
|
open fun start() {
|
||||||
|
isActive = true
|
||||||
|
startPeriodicCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the tracker
|
||||||
|
*/
|
||||||
|
open fun stop() {
|
||||||
|
isActive = false
|
||||||
|
pendingConnections.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a connection attempt is allowed for this peer/address
|
||||||
|
*/
|
||||||
|
fun isConnectionAttemptAllowed(id: String): Boolean {
|
||||||
|
// If already connected, usually no need to retry (subclasses can override logic if needed,
|
||||||
|
// but typically the caller checks isConnected() first).
|
||||||
|
|
||||||
|
val existingAttempt = pendingConnections[id]
|
||||||
|
return existingAttempt?.let {
|
||||||
|
it.isExpired() || it.shouldRetry()
|
||||||
|
} ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a new connection attempt.
|
||||||
|
* Returns true if the attempt was recorded (allowed), false if skipped.
|
||||||
|
*/
|
||||||
|
fun addPendingConnection(id: String): Boolean {
|
||||||
|
synchronized(pendingConnections) {
|
||||||
|
val currentAttempt = pendingConnections[id]
|
||||||
|
|
||||||
|
// If strictly not allowed right now, reject
|
||||||
|
if (currentAttempt != null && !currentAttempt.isExpired() && !currentAttempt.shouldRetry()) {
|
||||||
|
Log.d(tag, "Connection attempt already in progress for $id")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update attempt count
|
||||||
|
// Reset to 1 if expired, otherwise increment
|
||||||
|
val attempts = if (currentAttempt?.isExpired() == true) 1 else (currentAttempt?.attempts ?: 0) + 1
|
||||||
|
pendingConnections[id] = ConnectionAttempt(attempts)
|
||||||
|
Log.d(tag, "Added pending connection for $id (attempts: $attempts)")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a pending attempt (e.g., on success or fatal error)
|
||||||
|
*/
|
||||||
|
fun removePendingConnection(id: String) {
|
||||||
|
pendingConnections.remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract: Subclasses must define what "connected" means
|
||||||
|
*/
|
||||||
|
abstract fun isConnected(id: String): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract: Subclasses must implement disconnect logic
|
||||||
|
*/
|
||||||
|
abstract fun disconnect(id: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract: Subclasses report their active connection count
|
||||||
|
*/
|
||||||
|
abstract fun getConnectionCount(): Int
|
||||||
|
|
||||||
|
private fun startPeriodicCleanup() {
|
||||||
|
scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
try {
|
||||||
|
delay(CLEANUP_INTERVAL)
|
||||||
|
if (!isActive) break
|
||||||
|
|
||||||
|
// Clean up expired pending connections
|
||||||
|
val expired = pendingConnections.filter { it.value.isExpired() }
|
||||||
|
expired.keys.forEach { pendingConnections.remove(it) }
|
||||||
|
|
||||||
|
if (expired.isNotEmpty()) {
|
||||||
|
Log.d(tag, "Cleaned up ${expired.size} expired connection attempts")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(tag, "Error in periodic cleanup: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package com.bitchat.android.wifiaware
|
||||||
|
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.util.Log
|
||||||
|
import com.bitchat.android.mesh.MeshConnectionTracker
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import java.net.ServerSocket
|
||||||
|
import java.net.Socket
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tracks Wi-Fi Aware connections and manages retry logic using the shared state machine.
|
||||||
|
*/
|
||||||
|
class WifiAwareConnectionTracker(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
private val cm: ConnectivityManager
|
||||||
|
) : MeshConnectionTracker(scope, TAG) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "WifiAwareConnectionTracker"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active resources per peer
|
||||||
|
val peerSockets = ConcurrentHashMap<String, Socket>()
|
||||||
|
val serverSockets = ConcurrentHashMap<String, ServerSocket>()
|
||||||
|
val networkCallbacks = ConcurrentHashMap<String, ConnectivityManager.NetworkCallback>()
|
||||||
|
|
||||||
|
override fun isConnected(id: String): Boolean {
|
||||||
|
// We consider it connected if we have a client socket to them
|
||||||
|
return peerSockets.containsKey(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun disconnect(id: String) {
|
||||||
|
Log.d(TAG, "Disconnecting peer $id")
|
||||||
|
|
||||||
|
// 1. Close client socket
|
||||||
|
peerSockets.remove(id)?.let {
|
||||||
|
try { it.close() } catch (e: Exception) { Log.w(TAG, "Error closing socket for $id: ${e.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Close server socket
|
||||||
|
serverSockets.remove(id)?.let {
|
||||||
|
try { it.close() } catch (e: Exception) { Log.w(TAG, "Error closing server socket for $id: ${e.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Unregister network callback
|
||||||
|
networkCallbacks.remove(id)?.let {
|
||||||
|
try { cm.unregisterNetworkCallback(it) } catch (e: Exception) { Log.w(TAG, "Error unregistering callback for $id: ${e.message}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getConnectionCount(): Int = peerSockets.size
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Successfully established a client connection
|
||||||
|
*/
|
||||||
|
fun onClientConnected(peerId: String, socket: Socket) {
|
||||||
|
peerSockets[peerId] = socket
|
||||||
|
removePendingConnection(peerId) // Clear retry state on success
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addServerSocket(peerId: String, socket: ServerSocket) {
|
||||||
|
serverSockets[peerId] = socket
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNetworkCallback(peerId: String, callback: ConnectivityManager.NetworkCallback) {
|
||||||
|
networkCallbacks[peerId] = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up all resources
|
||||||
|
*/
|
||||||
|
override fun stop() {
|
||||||
|
super.stop()
|
||||||
|
val allIds = peerSockets.keys + serverSockets.keys + networkCallbacks.keys
|
||||||
|
allIds.toSet().forEach { disconnect(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDebugInfo(): String {
|
||||||
|
return buildString {
|
||||||
|
appendLine("Aware Connections: ${getConnectionCount()}")
|
||||||
|
peerSockets.keys.forEach { pid ->
|
||||||
|
appendLine(" - $pid (Socket)")
|
||||||
|
}
|
||||||
|
appendLine("Server Sockets: ${serverSockets.size}")
|
||||||
|
serverSockets.keys.forEach { pid ->
|
||||||
|
appendLine(" - $pid (Listening)")
|
||||||
|
}
|
||||||
|
appendLine("Pending Attempts: ${pendingConnections.size}")
|
||||||
|
pendingConnections.forEach { (pid, attempt) ->
|
||||||
|
appendLine(" - $pid: ${attempt.attempts} attempts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -89,21 +89,18 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Delegate
|
// Delegate
|
||||||
var delegate: WifiAwareMeshDelegate? = null
|
var delegate: WifiAwareMeshDelegate? = null
|
||||||
|
|
||||||
// Transport state
|
// Coroutines - must be initialized before tracker
|
||||||
private val peerSockets = ConcurrentHashMap<String, Socket>()
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
private val serverSockets = ConcurrentHashMap<String, ServerSocket>()
|
|
||||||
private val networkCallbacks = ConcurrentHashMap<String, ConnectivityManager.NetworkCallback>()
|
|
||||||
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
|
||||||
|
// Transport state
|
||||||
|
private val connectionTracker = WifiAwareConnectionTracker(serviceScope, cm)
|
||||||
private val handleToPeerId = ConcurrentHashMap<PeerHandle, String>() // discovery mapping
|
private val handleToPeerId = ConcurrentHashMap<PeerHandle, String>() // discovery mapping
|
||||||
private val discoveredTimestamps = ConcurrentHashMap<String, Long>() // peerID -> last seen time
|
private val discoveredTimestamps = ConcurrentHashMap<String, Long>() // peerID -> last seen time
|
||||||
private val connectionAttempts = ConcurrentHashMap<String, Long>() // peerID -> last connection attempt time
|
|
||||||
|
|
||||||
// Timestamp dedupe
|
// Timestamp dedupe
|
||||||
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
private val lastTimestamps = ConcurrentHashMap<String, ULong>()
|
||||||
|
|
||||||
// Coroutines
|
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setupDelegates()
|
setupDelegates()
|
||||||
messageHandler.packetProcessor = packetProcessor
|
messageHandler.packetProcessor = packetProcessor
|
||||||
@@ -165,7 +162,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
*/
|
*/
|
||||||
private fun broadcastRaw(bytes: ByteArray) {
|
private fun broadcastRaw(bytes: ByteArray) {
|
||||||
var sent = 0
|
var sent = 0
|
||||||
peerSockets.forEach { (pid, sock) ->
|
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
||||||
try {
|
try {
|
||||||
sock.getOutputStream().write(bytes)
|
sock.getOutputStream().write(bytes)
|
||||||
sent++
|
sent++
|
||||||
@@ -215,7 +212,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Wi-Fi Aware uses full packets; no fragmentation
|
// Wi-Fi Aware uses full packets; no fragmentation
|
||||||
val data = packet.toBinaryData() ?: return
|
val data = packet.toBinaryData() ?: return
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
val sock = peerSockets[peerID]
|
val sock = connectionTracker.peerSockets[peerID]
|
||||||
if (sock == null) {
|
if (sock == null) {
|
||||||
Log.w(TAG, "TX: no socket for ${peerID.take(8)}")
|
Log.w(TAG, "TX: no socket for ${peerID.take(8)}")
|
||||||
return@launch
|
return@launch
|
||||||
@@ -532,6 +529,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
sendPeriodicBroadcastAnnounce()
|
sendPeriodicBroadcastAnnounce()
|
||||||
startPeriodicConnectionMaintenance()
|
startPeriodicConnectionMaintenance()
|
||||||
|
connectionTracker.start()
|
||||||
gossipSyncManager.start()
|
gossipSyncManager.start()
|
||||||
|
|
||||||
// Register with cross-layer transport bridge
|
// Register with cross-layer transport bridge
|
||||||
@@ -555,18 +553,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
delay(200)
|
delay(200)
|
||||||
|
|
||||||
gossipSyncManager.stop()
|
gossipSyncManager.stop()
|
||||||
|
connectionTracker.stop() // Handles socket closing and callback unregistration
|
||||||
|
|
||||||
networkCallbacks.values.forEach { runCatching { cm.unregisterNetworkCallback(it) } }
|
|
||||||
networkCallbacks.clear()
|
|
||||||
publishSession?.close(); publishSession = null
|
publishSession?.close(); publishSession = null
|
||||||
subscribeSession?.close(); subscribeSession = null
|
subscribeSession?.close(); subscribeSession = null
|
||||||
wifiAwareSession?.close(); wifiAwareSession = null
|
wifiAwareSession?.close(); wifiAwareSession = null
|
||||||
|
|
||||||
serverSockets.values.forEach { it.closeQuietly() }
|
|
||||||
peerSockets.values.forEach { it.closeQuietly() }
|
|
||||||
handleToPeerId.clear()
|
handleToPeerId.clear()
|
||||||
serverSockets.clear()
|
|
||||||
peerSockets.clear()
|
|
||||||
|
|
||||||
peerManager.shutdown()
|
peerManager.shutdown()
|
||||||
fragmentManager.shutdown()
|
fragmentManager.shutdown()
|
||||||
@@ -608,9 +601,9 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
(now - ts) < 5 * 60 * 1000 // Seen in last 5 minutes
|
(now - ts) < 5 * 60 * 1000 // Seen in last 5 minutes
|
||||||
}.keys
|
}.keys
|
||||||
|
|
||||||
// 2. Filter out those who are already connected (either as client or server)
|
// 2. Filter out those who are already connected
|
||||||
val disconnectedPeers = recentDiscovered.filter { peerId ->
|
val disconnectedPeers = recentDiscovered.filter { peerId ->
|
||||||
!peerSockets.containsKey(peerId) && !serverSockets.containsKey(peerId)
|
!connectionTracker.isConnected(peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Attempt reconnection
|
// 3. Attempt reconnection
|
||||||
@@ -618,19 +611,18 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Find the PeerHandle for this peerId
|
// Find the PeerHandle for this peerId
|
||||||
val handle = handleToPeerId.entries.find { it.value == peerId }?.key ?: continue
|
val handle = handleToPeerId.entries.find { it.value == peerId }?.key ?: continue
|
||||||
|
|
||||||
// Rate limit attempts: max 1 per minute per peer
|
// Check tracker policy
|
||||||
val lastAttempt = connectionAttempts[peerId] ?: 0L
|
if (!connectionTracker.isConnectionAttemptAllowed(peerId)) continue
|
||||||
if (now - lastAttempt < 60_000) continue
|
|
||||||
|
|
||||||
Log.i(TAG, "🔄 Maintenance: attempting reconnect to ${peerId.take(8)}")
|
Log.i(TAG, "🔄 Maintenance: attempting reconnect to ${peerId.take(8)}")
|
||||||
connectionAttempts[peerId] = now
|
if (connectionTracker.addPendingConnection(peerId)) {
|
||||||
|
// Resend ping to trigger handshake
|
||||||
// Resend ping to trigger handshake
|
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
||||||
val msgId = (System.nanoTime() and 0x7fffffff).toInt()
|
try {
|
||||||
try {
|
subscribeSession?.sendMessage(handle, msgId, myPeerID.toByteArray())
|
||||||
subscribeSession?.sendMessage(handle, msgId, myPeerID.toByteArray())
|
} catch (e: Exception) {
|
||||||
} catch (e: Exception) {
|
Log.w(TAG, "Failed to send maintenance ping to ${peerId.take(8)}: ${e.message}")
|
||||||
Log.w(TAG, "Failed to send maintenance ping to ${peerId.take(8)}: ${e.message}")
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -654,13 +646,13 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
val peerId = handleToPeerId[peerHandle] ?: return
|
val peerId = handleToPeerId[peerHandle] ?: return
|
||||||
if (!amIServerFor(peerId)) return
|
if (!amIServerFor(peerId)) return
|
||||||
|
|
||||||
if (serverSockets.containsKey(peerId)) {
|
if (connectionTracker.serverSockets.containsKey(peerId)) {
|
||||||
Log.v(TAG, "↪ already serving $peerId, skipping")
|
Log.v(TAG, "↪ already serving $peerId, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val ss = ServerSocket(0)
|
val ss = ServerSocket(0)
|
||||||
serverSockets[peerId] = ss
|
connectionTracker.addServerSocket(peerId, ss)
|
||||||
val port = ss.localPort
|
val port = ss.localPort
|
||||||
|
|
||||||
// Ensure port is set to reuse if connection was recently closed (TIME_WAIT)
|
// Ensure port is set to reuse if connection was recently closed (TIME_WAIT)
|
||||||
@@ -688,7 +680,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
try { network.bindSocket(client) } catch (e: Exception) { Log.w(TAG, "Server bindSocket EPERM: ${e.message}") }
|
try { network.bindSocket(client) } catch (e: Exception) { Log.w(TAG, "Server bindSocket EPERM: ${e.message}") }
|
||||||
client.keepAlive = true
|
client.keepAlive = true
|
||||||
Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}")
|
Log.d(TAG, "SERVER: accepted TCP from ${peerId.take(8)} addr=${client.inetAddress?.hostAddress}")
|
||||||
peerSockets[peerId] = client
|
connectionTracker.onClientConnected(peerId, client)
|
||||||
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||||
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||||
listenerExec.execute { listenToPeer(client, peerId) }
|
listenerExec.execute { listenToPeer(client, peerId) }
|
||||||
@@ -705,12 +697,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
networkCallbacks.remove(peerId)
|
connectionTracker.networkCallbacks.remove(peerId)
|
||||||
Log.d(TAG, "SERVER: network lost for ${peerId.take(8)}")
|
Log.d(TAG, "SERVER: network lost for ${peerId.take(8)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
networkCallbacks[peerId] = cb
|
connectionTracker.addNetworkCallback(peerId, cb)
|
||||||
Log.d(TAG, "SERVER: requesting Aware network for ${peerId.take(8)}")
|
Log.d(TAG, "SERVER: requesting Aware network for ${peerId.take(8)}")
|
||||||
cm.requestNetwork(req, cb)
|
cm.requestNetwork(req, cb)
|
||||||
|
|
||||||
@@ -745,7 +737,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
val os = client.getOutputStream()
|
val os = client.getOutputStream()
|
||||||
while (peerSockets.containsKey(peerId)) {
|
while (connectionTracker.isConnected(peerId)) {
|
||||||
try { os.write(0) } catch (_: IOException) { break }
|
try { os.write(0) } catch (_: IOException) { break }
|
||||||
delay(2_000)
|
delay(2_000)
|
||||||
}
|
}
|
||||||
@@ -754,7 +746,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Discovery keep-alive
|
// Discovery keep-alive
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
var msgId = 0
|
var msgId = 0
|
||||||
while (peerSockets.containsKey(peerId)) {
|
while (connectionTracker.isConnected(peerId)) {
|
||||||
try { pubSession.sendMessage(peerHandle, msgId++, ByteArray(0)) } catch (_: Exception) { break }
|
try { pubSession.sendMessage(peerHandle, msgId++, ByteArray(0)) } catch (_: Exception) { break }
|
||||||
delay(20_000)
|
delay(20_000)
|
||||||
}
|
}
|
||||||
@@ -776,7 +768,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
|
|
||||||
val peerId = handleToPeerId[peerHandle] ?: return
|
val peerId = handleToPeerId[peerHandle] ?: return
|
||||||
if (amIServerFor(peerId)) return
|
if (amIServerFor(peerId)) return
|
||||||
if (peerSockets.containsKey(peerId)) {
|
if (connectionTracker.peerSockets.containsKey(peerId)) {
|
||||||
Log.v(TAG, "↪ already client-connected to $peerId, skipping")
|
Log.v(TAG, "↪ already client-connected to $peerId, skipping")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -797,7 +789,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Do not bind process for Aware; use per-socket binding instead
|
// Do not bind process for Aware; use per-socket binding instead
|
||||||
}
|
}
|
||||||
override fun onCapabilitiesChanged(network: Network, nc: NetworkCapabilities) {
|
override fun onCapabilitiesChanged(network: Network, nc: NetworkCapabilities) {
|
||||||
if (peerSockets.containsKey(peerId)) return
|
if (connectionTracker.peerSockets.containsKey(peerId)) return
|
||||||
val info = (nc.transportInfo as? WifiAwareNetworkInfo) ?: return
|
val info = (nc.transportInfo as? WifiAwareNetworkInfo) ?: return
|
||||||
val addr = info.peerIpv6Addr as? Inet6Address ?: return
|
val addr = info.peerIpv6Addr as? Inet6Address ?: return
|
||||||
|
|
||||||
@@ -824,7 +816,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
sock.connect(java.net.InetSocketAddress(scopedAddr, port), 7000)
|
sock.connect(java.net.InetSocketAddress(scopedAddr, port), 7000)
|
||||||
Log.d(TAG, "CLIENT: TCP connected to ${peerId.take(8)} addr=$scopedAddr:$port (iface=$iface)")
|
Log.d(TAG, "CLIENT: TCP connected to ${peerId.take(8)} addr=$scopedAddr:$port (iface=$iface)")
|
||||||
|
|
||||||
peerSockets[peerId] = sock
|
connectionTracker.onClientConnected(peerId, sock)
|
||||||
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
try { peerManager.setDirectConnection(peerId, true) } catch (_: Exception) {}
|
||||||
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
try { peerManager.addOrUpdatePeer(peerId, peerId) } catch (_: Exception) {}
|
||||||
listenerExec.execute { listenToPeer(sock, peerId) }
|
listenerExec.execute { listenToPeer(sock, peerId) }
|
||||||
@@ -841,12 +833,12 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun onLost(network: Network) {
|
override fun onLost(network: Network) {
|
||||||
networkCallbacks.remove(peerId)
|
connectionTracker.networkCallbacks.remove(peerId)
|
||||||
Log.d(TAG, "CLIENT: network lost for ${peerId.take(8)}")
|
Log.d(TAG, "CLIENT: network lost for ${peerId.take(8)}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
networkCallbacks[peerId] = cb
|
connectionTracker.addNetworkCallback(peerId, cb)
|
||||||
Log.d(TAG, "CLIENT: requesting Aware network for ${peerId.take(8)}")
|
Log.d(TAG, "CLIENT: requesting Aware network for ${peerId.take(8)}")
|
||||||
cm.requestNetwork(req, cb)
|
cm.requestNetwork(req, cb)
|
||||||
}
|
}
|
||||||
@@ -863,7 +855,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
try {
|
try {
|
||||||
val os = sock.getOutputStream()
|
val os = sock.getOutputStream()
|
||||||
while (peerSockets.containsKey(peerId)) {
|
while (connectionTracker.isConnected(peerId)) {
|
||||||
try { os.write(0) } catch (_: IOException) { break }
|
try { os.write(0) } catch (_: IOException) { break }
|
||||||
delay(2_000)
|
delay(2_000)
|
||||||
}
|
}
|
||||||
@@ -872,7 +864,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
// Discovery keep-alive
|
// Discovery keep-alive
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
var msgId = 0
|
var msgId = 0
|
||||||
while (peerSockets.containsKey(peerId)) {
|
while (connectionTracker.isConnected(peerId)) {
|
||||||
try { subscribeSession?.sendMessage(peerHandle, msgId++, ByteArray(0)) } catch (_: Exception) { break }
|
try { subscribeSession?.sendMessage(peerHandle, msgId++, ByteArray(0)) } catch (_: Exception) { break }
|
||||||
delay(20_000)
|
delay(20_000)
|
||||||
}
|
}
|
||||||
@@ -929,9 +921,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
Log.d(TAG, "Cleaning up peer: $initialId")
|
Log.d(TAG, "Cleaning up peer: $initialId")
|
||||||
|
|
||||||
peerSockets.remove(initialId)?.closeQuietly()
|
connectionTracker.disconnect(initialId)
|
||||||
serverSockets.remove(initialId)?.closeQuietly()
|
|
||||||
networkCallbacks.remove(initialId)?.let { runCatching { cm.unregisterNetworkCallback(it) } }
|
|
||||||
peerManager.removePeer(initialId)
|
peerManager.removePeer(initialId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1257,7 +1247,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
* Prefers the scoped IPv6 address format.
|
* Prefers the scoped IPv6 address format.
|
||||||
*/
|
*/
|
||||||
fun getDeviceAddressForPeer(peerID: String): String? =
|
fun getDeviceAddressForPeer(peerID: String): String? =
|
||||||
peerSockets[peerID]?.let { resolveScopedAddress(it) }
|
connectionTracker.peerSockets[peerID]?.let { resolveScopedAddress(it) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper to resolve a scoped IPv6 address from a socket for UI display.
|
* Helper to resolve a scoped IPv6 address from a socket for UI display.
|
||||||
@@ -1281,7 +1271,7 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
*/
|
*/
|
||||||
fun getDeviceAddressToPeerMapping(): Map<String, String> {
|
fun getDeviceAddressToPeerMapping(): Map<String, String> {
|
||||||
val map = mutableMapOf<String, String>()
|
val map = mutableMapOf<String, String>()
|
||||||
peerSockets.forEach { (pid, sock) ->
|
connectionTracker.peerSockets.forEach { (pid, sock) ->
|
||||||
map[pid] = resolveScopedAddress(sock) ?: "unknown"
|
map[pid] = resolveScopedAddress(sock) ?: "unknown"
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
@@ -1308,7 +1298,8 @@ class WifiAwareMeshService(private val context: Context) : TransportBridgeServic
|
|||||||
fun getDebugStatus(): String = buildString {
|
fun getDebugStatus(): String = buildString {
|
||||||
appendLine("=== Wi-Fi Aware Mesh Debug Status ===")
|
appendLine("=== Wi-Fi Aware Mesh Debug Status ===")
|
||||||
appendLine("My Peer ID: $myPeerID")
|
appendLine("My Peer ID: $myPeerID")
|
||||||
appendLine("Peers: ${peerSockets.keys}")
|
appendLine("Peers: ${connectionTracker.peerSockets.keys}")
|
||||||
|
appendLine(connectionTracker.getDebugInfo())
|
||||||
appendLine(peerManager.getDebugInfo(getDeviceAddressToPeerMapping()))
|
appendLine(peerManager.getDebugInfo(getDeviceAddressToPeerMapping()))
|
||||||
appendLine(fragmentManager.getDebugInfo())
|
appendLine(fragmentManager.getDebugInfo())
|
||||||
appendLine(securityManager.getDebugInfo())
|
appendLine(securityManager.getDebugInfo())
|
||||||
|
|||||||
Reference in New Issue
Block a user