improving reconnection logic. added keep alive

This commit is contained in:
2025-07-04 07:47:27 -04:00
parent 8085c55c24
commit 613686989e
3 changed files with 277 additions and 79 deletions

View File

@@ -24,6 +24,12 @@ class WifiAwareManager(private val context: Context) {
private const val TAG = LOG_PREFIX + "WifiAwareManager:" private const val TAG = LOG_PREFIX + "WifiAwareManager:"
private const val SERVICE_NAME = "lchat" private const val SERVICE_NAME = "lchat"
private const val PORT = 8888 private const val PORT = 8888
// Keep-alive constants
private const val KEEP_ALIVE_INTERVAL_MS = 15000L // Send keep-alive every 15 seconds
private const val KEEP_ALIVE_TIMEOUT_MS = 30000L // Consider connection lost after 30 seconds
private const val MESSAGE_TYPE_KEEP_ALIVE = "KEEP_ALIVE"
private const val MESSAGE_TYPE_KEEP_ALIVE_ACK = "KEEP_ALIVE_ACK"
} }
private var wifiAwareManager: android.net.wifi.aware.WifiAwareManager? = null private var wifiAwareManager: android.net.wifi.aware.WifiAwareManager? = null
@@ -51,20 +57,38 @@ class WifiAwareManager(private val context: Context) {
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceptionHandler) private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceptionHandler)
// Keep-alive tracking
private val lastPeerActivity = ConcurrentHashMap<String, Long>()
private var keepAliveJob: Job? = null
fun initialize() { fun initialize() {
coroutineScope.launch { coroutineScope.launch {
initializeAsync() val result = initializeAsync()
if (result.isFailure) {
Log.e(TAG, "Failed to initialize Wi-Fi Aware: ${result.exceptionOrNull()?.message}")
}
} }
} }
suspend fun initializeAsync(): Result<Unit> = withContext(Dispatchers.IO) { suspend fun initializeAsync(): Result<Unit> = withContext(Dispatchers.IO) {
wifiAwareManager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as? android.net.wifi.aware.WifiAwareManager wifiAwareManager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as? android.net.wifi.aware.WifiAwareManager
if (wifiAwareManager?.isAvailable == true) { // Always check if Wi-Fi Aware is available
attachToWifiAware() if (wifiAwareManager?.isAvailable != true) {
} else { Log.e(TAG, "Wi-Fi Aware is not available")
Result.failure(Exception("Wi-Fi Aware is not available")) wifiAwareSession = null
return@withContext Result.failure(Exception("Wi-Fi Aware is not available"))
} }
// If we already have a session, verify it's still valid
if (wifiAwareSession != null) {
Log.d(TAG, "Wi-Fi Aware already initialized - verifying session is still valid")
// Session is likely still valid
return@withContext Result.success(Unit)
}
// Need to attach
attachToWifiAware()
} }
private suspend fun attachToWifiAware(): Result<Unit> = suspendCancellableCoroutine { continuation -> private suspend fun attachToWifiAware(): Result<Unit> = suspendCancellableCoroutine { continuation ->
@@ -102,24 +126,56 @@ class WifiAwareManager(private val context: Context) {
val messageStr = String(message) val messageStr = String(message)
Log.d(TAG, "Host: Received message: $messageStr") Log.d(TAG, "Host: Received message: $messageStr")
if (messageStr == "CONNECT_REQUEST") { when (messageStr) {
Log.d(TAG, "Host: Received connection request from peer") "CONNECT_REQUEST" -> {
coroutineScope.launch { Log.d(TAG, "Host: Received connection request from peer")
val result = acceptConnectionAsync(peerHandle) coroutineScope.launch {
if (result.isSuccess) { val result = acceptConnectionAsync(peerHandle)
Log.d(TAG, "Host: Successfully accepted connection") if (result.isSuccess) {
} else { Log.d(TAG, "Host: Successfully accepted connection")
Log.e(TAG, "Host: Failed to accept connection: ${result.exceptionOrNull()?.message}") startKeepAlive()
} else {
Log.e(TAG, "Host: Failed to accept connection: ${result.exceptionOrNull()?.message}")
}
} }
} }
} else { MESSAGE_TYPE_KEEP_ALIVE -> {
handleIncomingMessage(peerHandle, message) Log.d(TAG, "Host: Received keep-alive from peer")
handleKeepAlive(peerHandle, true)
}
MESSAGE_TYPE_KEEP_ALIVE_ACK -> {
Log.d(TAG, "Host: Received keep-alive ACK from peer")
handleKeepAlive(peerHandle, false)
}
else -> {
handleIncomingMessage(peerHandle, message)
}
}
}
override fun onSessionTerminated() {
Log.w(TAG, "Host publish session terminated")
publishDiscoverySession = null
stopKeepAlive()
// Emit disconnection event
coroutineScope.launch {
_connectionFlow.emit(Pair("", false))
} }
} }
}, null) }, null)
} }
fun startClientMode() { fun startClientMode() {
// Close any existing subscribe session before starting a new one
if (subscribeDiscoverySession != null) {
Log.d(TAG, "Closing existing subscribe session before starting new one")
subscribeDiscoverySession?.close()
subscribeDiscoverySession = null
}
// Clear any stale peer handles
peerHandles.clear()
val config = SubscribeConfig.Builder() val config = SubscribeConfig.Builder()
.setServiceName(SERVICE_NAME) .setServiceName(SERVICE_NAME)
.build() .build()
@@ -145,18 +201,47 @@ class WifiAwareManager(private val context: Context) {
Log.d(TAG, "Sending connection request to room: $roomName") Log.d(TAG, "Sending connection request to room: $roomName")
subscribeDiscoverySession?.sendMessage(peerHandle, 0, "CONNECT_REQUEST".toByteArray()) subscribeDiscoverySession?.sendMessage(peerHandle, 0, "CONNECT_REQUEST".toByteArray())
// Update peer activity when discovered
val peerId = peerHandle.toString()
lastPeerActivity[peerId] = System.currentTimeMillis()
// Wait a bit for host to prepare, then connect // Wait a bit for host to prepare, then connect
coroutineScope.launch { coroutineScope.launch {
delay(500) delay(500)
val result = connectToPeerAsync(peerHandle, roomName) val result = connectToPeerAsync(peerHandle, roomName)
if (result.isFailure) { if (result.isFailure) {
Log.e(TAG, "Failed to connect to peer: ${result.exceptionOrNull()?.message}") Log.e(TAG, "Failed to connect to peer: ${result.exceptionOrNull()?.message}")
lastPeerActivity.remove(peerId)
} }
} }
} }
override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) { override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
handleIncomingMessage(peerHandle, message) val messageStr = String(message)
when (messageStr) {
MESSAGE_TYPE_KEEP_ALIVE -> {
Log.d(TAG, "Client: Received keep-alive from host")
handleKeepAlive(peerHandle, true)
}
MESSAGE_TYPE_KEEP_ALIVE_ACK -> {
Log.d(TAG, "Client: Received keep-alive ACK from host")
handleKeepAlive(peerHandle, false)
}
else -> {
handleIncomingMessage(peerHandle, message)
}
}
}
override fun onSessionTerminated() {
Log.w(TAG, "Client subscribe session terminated")
subscribeDiscoverySession = null
stopKeepAlive()
// Don't clear wifiAwareSession - it's still valid
// Emit disconnection event
coroutineScope.launch {
_connectionFlow.emit(Pair("", false))
}
} }
}, null) }, null)
} }
@@ -197,10 +282,14 @@ class WifiAwareManager(private val context: Context) {
_connectionFlow.emit(Pair(roomName, true)) _connectionFlow.emit(Pair(roomName, true))
} }
connectionCallback?.invoke(roomName, true) connectionCallback?.invoke(roomName, true)
// Start keep-alive for client connections
startKeepAlive()
} }
override fun onLost(network: android.net.Network) { override fun onLost(network: android.net.Network) {
Log.d(TAG, "onLost: Network lost for room: $roomName") Log.d(TAG, "onLost: Network lost for room: $roomName")
// Clear peer handles when connection is lost
peerHandles.remove(roomName)
coroutineScope.launch { coroutineScope.launch {
_connectionFlow.emit(Pair(roomName, false)) _connectionFlow.emit(Pair(roomName, false))
} }
@@ -268,7 +357,9 @@ class WifiAwareManager(private val context: Context) {
val callback = object : ConnectivityManager.NetworkCallback() { val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: android.net.Network) { override fun onAvailable(network: android.net.Network) {
Log.d(TAG, "Client connected") Log.d(TAG, "Client connected")
peerHandles[peerHandle.toString()] = peerHandle val peerId = peerHandle.toString()
peerHandles[peerId] = peerHandle
lastPeerActivity[peerId] = System.currentTimeMillis()
if (!isResumed) { if (!isResumed) {
isResumed = true isResumed = true
continuation.resume(Result.success(Unit)) continuation.resume(Result.success(Unit))
@@ -298,6 +389,10 @@ class WifiAwareManager(private val context: Context) {
private fun handleIncomingMessage(peerHandle: PeerHandle, message: ByteArray) { private fun handleIncomingMessage(peerHandle: PeerHandle, message: ByteArray) {
try { try {
// Update peer activity on any message
val peerId = peerHandle.toString()
lastPeerActivity[peerId] = System.currentTimeMillis()
val messageStr = String(message) val messageStr = String(message)
val parts = messageStr.split("|", limit = 3) val parts = messageStr.split("|", limit = 3)
if (parts.size == 3) { if (parts.size == 3) {
@@ -312,18 +407,35 @@ class WifiAwareManager(private val context: Context) {
} }
} }
fun sendMessage(userId: String, userName: String, content: String) { fun sendMessage(userId: String, userName: String, content: String): Boolean {
val message = "$userId|$userName|$content".toByteArray() val message = "$userId|$userName|$content".toByteArray()
var messagesSent = 0
if (publishDiscoverySession != null) { if (publishDiscoverySession != null && peerHandles.isNotEmpty()) {
peerHandles.values.forEach { peerHandle -> peerHandles.values.forEach { peerHandle ->
publishDiscoverySession?.sendMessage(peerHandle, 0, message) try {
publishDiscoverySession?.sendMessage(peerHandle, messagesSent, message)
messagesSent++
} catch (e: Exception) {
Log.e(TAG, "Failed to send message to peer", e)
}
} }
} else if (subscribeDiscoverySession != null) { } else if (subscribeDiscoverySession != null && peerHandles.isNotEmpty()) {
peerHandles.values.forEach { peerHandle -> peerHandles.values.forEach { peerHandle ->
subscribeDiscoverySession?.sendMessage(peerHandle, 0, message) try {
subscribeDiscoverySession?.sendMessage(peerHandle, messagesSent, message)
messagesSent++
} catch (e: Exception) {
Log.e(TAG, "Failed to send message to peer", e)
}
} }
} }
if (messagesSent == 0) {
Log.w(TAG, "No messages sent - no active session or no peer handles")
}
return messagesSent > 0
} }
fun setMessageCallback(callback: (String, String, String) -> Unit) { fun setMessageCallback(callback: (String, String, String) -> Unit) {
@@ -334,11 +446,108 @@ class WifiAwareManager(private val context: Context) {
connectionCallback = callback connectionCallback = callback
} }
private fun startKeepAlive() {
keepAliveJob?.cancel()
keepAliveJob = coroutineScope.launch {
while (isActive) {
delay(KEEP_ALIVE_INTERVAL_MS)
sendKeepAlive()
checkPeerActivity()
}
}
}
private fun stopKeepAlive() {
keepAliveJob?.cancel()
keepAliveJob = null
lastPeerActivity.clear()
}
private fun sendKeepAlive() {
val message = MESSAGE_TYPE_KEEP_ALIVE.toByteArray()
var messagesSent = 0
if (publishDiscoverySession != null && peerHandles.isNotEmpty()) {
peerHandles.forEach { (peerId, peerHandle) ->
try {
publishDiscoverySession?.sendMessage(peerHandle, messagesSent, message)
messagesSent++
Log.d(TAG, "Sent keep-alive to peer: $peerId")
} catch (e: Exception) {
Log.e(TAG, "Failed to send keep-alive to peer: $peerId", e)
}
}
} else if (subscribeDiscoverySession != null && peerHandles.isNotEmpty()) {
peerHandles.forEach { (peerId, peerHandle) ->
try {
subscribeDiscoverySession?.sendMessage(peerHandle, messagesSent, message)
messagesSent++
Log.d(TAG, "Sent keep-alive to peer: $peerId")
} catch (e: Exception) {
Log.e(TAG, "Failed to send keep-alive to peer: $peerId", e)
}
}
}
}
private fun handleKeepAlive(peerHandle: PeerHandle, shouldReply: Boolean) {
// Update last activity for this peer
val peerId = peerHandle.toString()
lastPeerActivity[peerId] = System.currentTimeMillis()
// Send acknowledgment if requested
if (shouldReply) {
val ackMessage = MESSAGE_TYPE_KEEP_ALIVE_ACK.toByteArray()
try {
if (publishDiscoverySession != null) {
publishDiscoverySession?.sendMessage(peerHandle, 0, ackMessage)
} else if (subscribeDiscoverySession != null) {
subscribeDiscoverySession?.sendMessage(peerHandle, 0, ackMessage)
}
Log.d(TAG, "Sent keep-alive ACK to peer")
} catch (e: Exception) {
Log.e(TAG, "Failed to send keep-alive ACK", e)
}
}
}
private fun checkPeerActivity() {
val currentTime = System.currentTimeMillis()
val inactivePeers = mutableListOf<String>()
lastPeerActivity.forEach { (peerId, lastActivity) ->
if (currentTime - lastActivity > KEEP_ALIVE_TIMEOUT_MS) {
Log.w(TAG, "Peer $peerId inactive for too long, considering disconnected")
inactivePeers.add(peerId)
}
}
// Remove inactive peers
inactivePeers.forEach { peerId ->
lastPeerActivity.remove(peerId)
peerHandles.remove(peerId)
}
// If all peers are disconnected, emit disconnection event
if (peerHandles.isEmpty() && lastPeerActivity.isNotEmpty()) {
coroutineScope.launch {
_connectionFlow.emit(Pair("", false))
}
}
}
fun stop() { fun stop() {
Log.d(TAG, "Stopping WifiAwareManager")
stopKeepAlive()
publishDiscoverySession?.close() publishDiscoverySession?.close()
publishDiscoverySession = null
subscribeDiscoverySession?.close() subscribeDiscoverySession?.close()
subscribeDiscoverySession = null
// Close and clear the wifiAwareSession to force re-attachment
wifiAwareSession?.close() wifiAwareSession?.close()
wifiAwareSession = null
peerHandles.clear() peerHandles.clear()
coroutineScope.cancel() // Don't cancel the coroutine scope - we need it for future operations
Log.d(TAG, "WifiAwareManager stopped - session cleared for fresh start")
} }
} }

View File

@@ -49,9 +49,7 @@ class ChatRepository @Inject constructor(
private var messageCallback: ((String, String, String) -> Unit)? = null private var messageCallback: ((String, String, String) -> Unit)? = null
private var connectionCallback: ((String, Boolean) -> Unit)? = null private var connectionCallback: ((String, Boolean) -> Unit)? = null
private var lastActivityTime = System.currentTimeMillis() // Keep-alive is now handled by WifiAwareManager
private var connectionCheckJob: Job? = null
private val connectionTimeout = 30000L // 30 seconds
init { init {
wifiAwareManager.initialize() wifiAwareManager.initialize()
@@ -74,9 +72,6 @@ class ChatRepository @Inject constructor(
) )
addMessage(message) addMessage(message)
// Update last activity time
lastActivityTime = System.currentTimeMillis()
// If we're receiving messages, we must be connected // If we're receiving messages, we must be connected
if (_connectionState.value !is ConnectionState.Connected && if (_connectionState.value !is ConnectionState.Connected &&
_connectionState.value !is ConnectionState.Hosting) { _connectionState.value !is ConnectionState.Hosting) {
@@ -116,9 +111,10 @@ class ChatRepository @Inject constructor(
fun startHostMode(roomName: String) { fun startHostMode(roomName: String) {
currentRoomName = roomName currentRoomName = roomName
// Ensure WifiAwareManager is initialized before starting
wifiAwareManager.initialize()
wifiAwareManager.startHostMode(roomName) wifiAwareManager.startHostMode(roomName)
_connectionState.value = ConnectionState.Hosting(roomName) _connectionState.value = ConnectionState.Hosting(roomName)
startConnectionMonitoring()
loadMessagesFromDatabase(roomName) loadMessagesFromDatabase(roomName)
} }
@@ -137,30 +133,45 @@ class ChatRepository @Inject constructor(
} }
fun startClientMode() { fun startClientMode() {
wifiAwareManager.startClientMode() // Reset state for fresh start
_connectionState.value = ConnectionState.Searching _messages.value = emptyList()
startConnectionMonitoring() currentRoomName = ""
// Ensure WifiAwareManager is initialized before starting
repositoryScope.launch {
// Give Wi-Fi Aware time to stabilize after network changes
delay(500)
wifiAwareManager.initialize()
// Small delay to ensure initialization completes
delay(100)
wifiAwareManager.startClientMode()
_connectionState.value = ConnectionState.Searching
}
} }
fun sendMessage(userId: String, userName: String, content: String) { fun sendMessage(userId: String, userName: String, content: String) {
val message = Message( // Only allow sending messages if connected or hosting
id = UUID.randomUUID().toString(), when (_connectionState.value) {
userId = userId, is ConnectionState.Connected, is ConnectionState.Hosting -> {
userName = userName, val message = Message(
content = content, id = UUID.randomUUID().toString(),
timestamp = System.currentTimeMillis(), userId = userId,
isOwnMessage = true userName = userName,
) content = content,
addMessage(message) timestamp = System.currentTimeMillis(),
wifiAwareManager.sendMessage(userId, userName, content) isOwnMessage = true
)
// Update last activity time val sent = wifiAwareManager.sendMessage(userId, userName, content)
lastActivityTime = System.currentTimeMillis() if (sent) {
addMessage(message)
// If we can send messages, update connection state if needed } else {
if (_connectionState.value is ConnectionState.Disconnected || android.util.Log.e("ChatRepository", "Failed to send message - no active connection")
_connectionState.value is ConnectionState.Error) { _connectionState.value = ConnectionState.Disconnected
_connectionState.value = ConnectionState.Connected("Active") }
}
else -> {
android.util.Log.w("ChatRepository", "Cannot send message - not connected. State: ${_connectionState.value}")
}
} }
} }
@@ -193,37 +204,13 @@ class ChatRepository @Inject constructor(
} }
fun stop() { fun stop() {
stopConnectionMonitoring()
wifiAwareManager.stop() wifiAwareManager.stop()
_connectionState.value = ConnectionState.Disconnected _connectionState.value = ConnectionState.Disconnected
_connectedUsers.value = emptyList() _connectedUsers.value = emptyList()
repositoryScope.cancel() // Don't cancel the repository scope - we need it for future operations
} // Clear messages when stopping
_messages.value = emptyList()
private fun startConnectionMonitoring() { currentRoomName = ""
connectionCheckJob?.cancel()
connectionCheckJob = repositoryScope.launch {
while (isActive) {
delay(5000) // Check every 5 seconds
val timeSinceLastActivity = System.currentTimeMillis() - lastActivityTime
// If no activity for 30 seconds and we think we're connected, mark as disconnected
if (timeSinceLastActivity > connectionTimeout) {
when (_connectionState.value) {
is ConnectionState.Connected,
is ConnectionState.Hosting -> {
_connectionState.value = ConnectionState.Disconnected
}
else -> {} // Keep current state
}
}
}
}
}
private fun stopConnectionMonitoring() {
connectionCheckJob?.cancel()
connectionCheckJob = null
} }
sealed class ConnectionState { sealed class ConnectionState {

View File

@@ -138,6 +138,8 @@ class ChatFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
Log.d(TAG, "onDestroyView") Log.d(TAG, "onDestroyView")
// Disconnect when leaving the chat screen
viewModel.disconnect()
_binding = null _binding = null
} }
} }