Merge pull request #2 from mattintech/feature/connectionStatus

adding connection status
This commit is contained in:
2025-07-03 20:42:57 -04:00
committed by GitHub
6 changed files with 176 additions and 10 deletions

20
TODO.md
View File

@@ -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
- 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

View File

@@ -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()

View File

@@ -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")

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/white" />
</shape>

View File

@@ -4,13 +4,60 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/connectionStatusCard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="2dp"
app:cardBackgroundColor="?attr/colorSurfaceVariant"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<View
android:id="@+id/connectionIndicator"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/ic_circle"
android:backgroundTint="@color/disconnected_color" />
<TextView
android:id="@+id/connectionStatusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Disconnected"
android:textAppearance="?attr/textAppearanceBodyMedium" />
<TextView
android:id="@+id/roomNameText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:text=""
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textStyle="bold"
android:gravity="end" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messagesRecyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/connectionStatusCard"
app:layout_constraintBottom_toTopOf="@id/messageInputLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View File

@@ -7,4 +7,10 @@
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<!-- Connection Status Colors -->
<color name="connected_color">#4CAF50</color>
<color name="connecting_color">#FFC107</color>
<color name="disconnected_color">#F44336</color>
<color name="hosting_color">#2196F3</color>
</resources>