adding connection status
This commit is contained in:
10
TODO.md
10
TODO.md
@@ -17,11 +17,11 @@
|
|||||||
|
|
||||||
## Phase 2: Core UX Improvements
|
## Phase 2: Core UX Improvements
|
||||||
|
|
||||||
### 2.1 Connection Status Management
|
### 2.1 Connection Status Management ✅
|
||||||
- [ ] Add connection state to ViewModels
|
- [x] Add connection state to ViewModels
|
||||||
- [ ] Create UI indicator for connection status
|
- [x] Create UI indicator for connection status
|
||||||
- [ ] Show real-time connection state changes
|
- [x] Show real-time connection state changes
|
||||||
- [ ] Add connection error messages
|
- [x] Add connection error messages
|
||||||
|
|
||||||
### 2.2 User List Feature
|
### 2.2 User List Feature
|
||||||
- [ ] Track connected users in Repository
|
- [ ] Track connected users in Repository
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.mattintech.lchat.repository
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.mattintech.lchat.data.Message
|
import com.mattintech.lchat.data.Message
|
||||||
import com.mattintech.lchat.network.WifiAwareManager
|
import com.mattintech.lchat.network.WifiAwareManager
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
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 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()
|
||||||
|
private var connectionCheckJob: Job? = null
|
||||||
|
private val connectionTimeout = 30000L // 30 seconds
|
||||||
|
|
||||||
init {
|
init {
|
||||||
wifiAwareManager.initialize()
|
wifiAwareManager.initialize()
|
||||||
setupWifiAwareCallbacks()
|
setupWifiAwareCallbacks()
|
||||||
@@ -51,6 +56,19 @@ class ChatRepository private constructor(context: Context) {
|
|||||||
isOwnMessage = false
|
isOwnMessage = false
|
||||||
)
|
)
|
||||||
addMessage(message)
|
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)
|
messageCallback?.invoke(userId, userName, content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,11 +76,13 @@ class ChatRepository private constructor(context: Context) {
|
|||||||
fun startHostMode(roomName: String) {
|
fun startHostMode(roomName: String) {
|
||||||
wifiAwareManager.startHostMode(roomName)
|
wifiAwareManager.startHostMode(roomName)
|
||||||
_connectionState.value = ConnectionState.Hosting(roomName)
|
_connectionState.value = ConnectionState.Hosting(roomName)
|
||||||
|
startConnectionMonitoring()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startClientMode() {
|
fun startClientMode() {
|
||||||
wifiAwareManager.startClientMode()
|
wifiAwareManager.startClientMode()
|
||||||
_connectionState.value = ConnectionState.Searching
|
_connectionState.value = ConnectionState.Searching
|
||||||
|
startConnectionMonitoring()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendMessage(userId: String, userName: String, content: String) {
|
fun sendMessage(userId: String, userName: String, content: String) {
|
||||||
@@ -76,6 +96,15 @@ class ChatRepository private constructor(context: Context) {
|
|||||||
)
|
)
|
||||||
addMessage(message)
|
addMessage(message)
|
||||||
wifiAwareManager.sendMessage(userId, userName, content)
|
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) {
|
private fun addMessage(message: Message) {
|
||||||
@@ -92,15 +121,49 @@ class ChatRepository private constructor(context: Context) {
|
|||||||
|
|
||||||
fun setConnectionCallback(callback: (String, Boolean) -> Unit) {
|
fun setConnectionCallback(callback: (String, Boolean) -> Unit) {
|
||||||
connectionCallback = callback
|
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() {
|
fun stop() {
|
||||||
|
stopConnectionMonitoring()
|
||||||
wifiAwareManager.stop()
|
wifiAwareManager.stop()
|
||||||
_connectionState.value = ConnectionState.Disconnected
|
_connectionState.value = ConnectionState.Disconnected
|
||||||
_connectedUsers.value = emptyList()
|
_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 {
|
sealed class ConnectionState {
|
||||||
object Disconnected : ConnectionState()
|
object Disconnected : ConnectionState()
|
||||||
object Searching : ConnectionState()
|
object Searching : ConnectionState()
|
||||||
|
|||||||
@@ -9,8 +9,11 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.mattintech.lchat.R
|
||||||
import com.mattintech.lchat.databinding.FragmentChatBinding
|
import com.mattintech.lchat.databinding.FragmentChatBinding
|
||||||
|
import com.mattintech.lchat.repository.ChatRepository
|
||||||
import com.mattintech.lchat.ui.adapters.MessageAdapter
|
import com.mattintech.lchat.ui.adapters.MessageAdapter
|
||||||
import com.mattintech.lchat.utils.LOG_PREFIX
|
import com.mattintech.lchat.utils.LOG_PREFIX
|
||||||
import com.mattintech.lchat.viewmodel.ChatViewModel
|
import com.mattintech.lchat.viewmodel.ChatViewModel
|
||||||
@@ -50,6 +53,7 @@ class ChatFragment : Fragment() {
|
|||||||
|
|
||||||
setupUI()
|
setupUI()
|
||||||
observeViewModel()
|
observeViewModel()
|
||||||
|
updateRoomInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
@@ -84,7 +88,7 @@ class ChatFragment : Fragment() {
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
viewModel.connectionState.collect { state ->
|
viewModel.connectionState.collect { state ->
|
||||||
Log.d(TAG, "Connection state: $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()
|
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() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
Log.d(TAG, "onDestroyView")
|
Log.d(TAG, "onDestroyView")
|
||||||
|
|||||||
5
app/src/main/res/drawable/ic_circle.xml
Normal file
5
app/src/main/res/drawable/ic_circle.xml
Normal 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>
|
||||||
@@ -4,13 +4,60 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/messagesRecyclerView"
|
android:id="@+id/messagesRecyclerView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
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_constraintBottom_toTopOf="@id/messageInputLayout"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|||||||
@@ -7,4 +7,10 @@
|
|||||||
<color name="teal_700">#FF018786</color>
|
<color name="teal_700">#FF018786</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</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>
|
</resources>
|
||||||
Reference in New Issue
Block a user