From ccf26b80e8d05f1a9e2c52ffa311dea2729b2111 Mon Sep 17 00:00:00 2001 From: Matt Hills Date: Thu, 3 Jul 2025 20:38:49 -0400 Subject: [PATCH] adding connection status --- TODO.md | 10 +-- .../lchat/repository/ChatRepository.kt | 65 ++++++++++++++++++- .../com/mattintech/lchat/ui/ChatFragment.kt | 41 +++++++++++- app/src/main/res/drawable/ic_circle.xml | 5 ++ app/src/main/res/layout/fragment_chat.xml | 49 +++++++++++++- app/src/main/res/values/colors.xml | 6 ++ 6 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/ic_circle.xml diff --git a/TODO.md b/TODO.md index e93dfad..99030cf 100644 --- a/TODO.md +++ b/TODO.md @@ -17,11 +17,11 @@ ## Phase 2: Core UX Improvements -### 2.1 Connection Status Management -- [ ] Add connection state to ViewModels -- [ ] Create UI indicator for connection status -- [ ] Show real-time connection state changes -- [ ] Add connection error messages +### 2.1 Connection Status Management ✅ +- [x] Add connection state to ViewModels +- [x] Create UI indicator for connection status +- [x] Show real-time connection state changes +- [x] Add connection error messages ### 2.2 User List Feature - [ ] Track connected users in Repository diff --git a/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt b/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt index 7f6fff7..39c8044 100644 --- a/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt +++ b/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt @@ -3,6 +3,7 @@ package com.mattintech.lchat.repository import android.content.Context import com.mattintech.lchat.data.Message import com.mattintech.lchat.network.WifiAwareManager +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -35,6 +36,10 @@ class ChatRepository private constructor(context: Context) { private var messageCallback: ((String, String, String) -> Unit)? = null private var connectionCallback: ((String, Boolean) -> Unit)? = null + private var lastActivityTime = System.currentTimeMillis() + private var connectionCheckJob: Job? = null + private val connectionTimeout = 30000L // 30 seconds + init { wifiAwareManager.initialize() setupWifiAwareCallbacks() @@ -51,6 +56,19 @@ class ChatRepository private constructor(context: Context) { isOwnMessage = false ) addMessage(message) + + // Update last activity time + lastActivityTime = System.currentTimeMillis() + + // If we're receiving messages, we must be connected + if (_connectionState.value !is ConnectionState.Connected && + _connectionState.value !is ConnectionState.Hosting) { + when (_connectionState.value) { + is ConnectionState.Hosting -> {} // Keep hosting state + else -> _connectionState.value = ConnectionState.Connected("Active") + } + } + messageCallback?.invoke(userId, userName, content) } } @@ -58,11 +76,13 @@ class ChatRepository private constructor(context: Context) { fun startHostMode(roomName: String) { wifiAwareManager.startHostMode(roomName) _connectionState.value = ConnectionState.Hosting(roomName) + startConnectionMonitoring() } fun startClientMode() { wifiAwareManager.startClientMode() _connectionState.value = ConnectionState.Searching + startConnectionMonitoring() } fun sendMessage(userId: String, userName: String, content: String) { @@ -76,6 +96,15 @@ class ChatRepository private constructor(context: Context) { ) addMessage(message) wifiAwareManager.sendMessage(userId, userName, content) + + // Update last activity time + lastActivityTime = System.currentTimeMillis() + + // If we can send messages, update connection state if needed + if (_connectionState.value is ConnectionState.Disconnected || + _connectionState.value is ConnectionState.Error) { + _connectionState.value = ConnectionState.Connected("Active") + } } private fun addMessage(message: Message) { @@ -92,15 +121,49 @@ class ChatRepository private constructor(context: Context) { fun setConnectionCallback(callback: (String, Boolean) -> Unit) { connectionCallback = callback - wifiAwareManager.setConnectionCallback(callback) + wifiAwareManager.setConnectionCallback { roomName, isConnected -> + if (isConnected) { + _connectionState.value = ConnectionState.Connected(roomName) + } else { + _connectionState.value = ConnectionState.Disconnected + } + callback(roomName, isConnected) + } } fun stop() { + stopConnectionMonitoring() wifiAwareManager.stop() _connectionState.value = ConnectionState.Disconnected _connectedUsers.value = emptyList() } + private fun startConnectionMonitoring() { + connectionCheckJob?.cancel() + connectionCheckJob = GlobalScope.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 { object Disconnected : ConnectionState() object Searching : ConnectionState() diff --git a/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt b/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt index 34cdf47..1e6210b 100644 --- a/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt +++ b/app/src/main/java/com/mattintech/lchat/ui/ChatFragment.kt @@ -9,8 +9,11 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs +import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager +import com.mattintech.lchat.R import com.mattintech.lchat.databinding.FragmentChatBinding +import com.mattintech.lchat.repository.ChatRepository import com.mattintech.lchat.ui.adapters.MessageAdapter import com.mattintech.lchat.utils.LOG_PREFIX import com.mattintech.lchat.viewmodel.ChatViewModel @@ -50,6 +53,7 @@ class ChatFragment : Fragment() { setupUI() observeViewModel() + updateRoomInfo() } private fun setupUI() { @@ -84,7 +88,7 @@ class ChatFragment : Fragment() { lifecycleScope.launch { viewModel.connectionState.collect { state -> Log.d(TAG, "Connection state: $state") - // Handle connection state changes if needed + updateConnectionStatus(state) } } } @@ -97,6 +101,41 @@ class ChatFragment : Fragment() { binding.messageInput.text?.clear() } + private fun updateRoomInfo() { + val (roomName, _, isHost) = viewModel.getRoomInfo() + binding.roomNameText.text = if (isHost) "Hosting: $roomName" else "Room: $roomName" + } + + private fun updateConnectionStatus(state: ChatRepository.ConnectionState) { + when (state) { + is ChatRepository.ConnectionState.Disconnected -> { + binding.connectionStatusText.text = "Disconnected" + binding.connectionIndicator.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.disconnected_color) + } + is ChatRepository.ConnectionState.Searching -> { + binding.connectionStatusText.text = "Searching..." + binding.connectionIndicator.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.connecting_color) + } + is ChatRepository.ConnectionState.Hosting -> { + binding.connectionStatusText.text = "Hosting" + binding.connectionIndicator.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.hosting_color) + } + is ChatRepository.ConnectionState.Connected -> { + binding.connectionStatusText.text = "Connected" + binding.connectionIndicator.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.connected_color) + } + is ChatRepository.ConnectionState.Error -> { + binding.connectionStatusText.text = "Error: ${state.message}" + binding.connectionIndicator.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.disconnected_color) + } + } + } + override fun onDestroyView() { super.onDestroyView() Log.d(TAG, "onDestroyView") diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 0000000..a6f3dfa --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_chat.xml b/app/src/main/res/layout/fragment_chat.xml index 78ce89b..f9dd93a 100644 --- a/app/src/main/res/layout/fragment_chat.xml +++ b/app/src/main/res/layout/fragment_chat.xml @@ -4,13 +4,60 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..c9147a8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,4 +7,10 @@ #FF018786 #FF000000 #FFFFFFFF + + + #4CAF50 + #FFC107 + #F44336 + #2196F3 \ No newline at end of file