diff --git a/TODO.md b/TODO.md
index e93dfad..c472a20 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
@@ -71,5 +71,11 @@
- [ ] Message encryption improvements
## Current Status
-- 🚀 Starting with Phase 1.1 - MVVM Architecture
-- Target: Create a maintainable, testable architecture
\ No newline at end of file
+- ✅ Phase 1.1 - MVVM Architecture - COMPLETED
+- ✅ Phase 2.1 - Connection Status Management - COMPLETED
+- 🚀 Next: Phase 1.2 (Dependency Injection) or Phase 3 (Data Persistence)
+
+## Completed Work Summary
+1. **MVVM Architecture**: ViewModels, Repository pattern, proper separation of concerns
+2. **Connection Status**: Visual indicator with real-time updates, activity-based detection
+3. **Sleep/Wake Handling**: Auto-recovery when messages resume after device sleep
\ No newline at end of file
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