moving to coroutines
This commit is contained in:
48
TODO.md
48
TODO.md
@@ -15,21 +15,21 @@
|
||||
- [x] Convert singletons to proper DI
|
||||
- [x] Inject ViewModels using Hilt
|
||||
|
||||
### 1.3 Room Database Setup
|
||||
- [ ] Add Room dependencies
|
||||
- [ ] Create Message and User entities
|
||||
- [ ] Implement DAOs for data access
|
||||
- [ ] Create database migrations
|
||||
- [ ] Store messages in Room database
|
||||
- [ ] Load message history on app restart
|
||||
- [ ] Implement message sync logic
|
||||
### 1.3 Room Database Setup ✅
|
||||
- [x] Add Room dependencies
|
||||
- [x] Create Message and User entities
|
||||
- [x] Implement DAOs for data access
|
||||
- [x] Create database migrations
|
||||
- [x] Store messages in Room database
|
||||
- [x] Load message history on app restart
|
||||
- [x] Implement message sync logic
|
||||
|
||||
### 1.4 Coroutines & Flow Optimization
|
||||
- [ ] Convert callbacks to coroutines
|
||||
- [ ] Use Flow for reactive data streams
|
||||
- [ ] Implement proper scope management
|
||||
- [ ] Replace GlobalScope with proper lifecycle scopes
|
||||
- [ ] Add proper error handling with coroutines
|
||||
### 1.4 Coroutines & Flow Optimization ✅
|
||||
- [x] Convert callbacks to coroutines
|
||||
- [x] Use Flow for reactive data streams
|
||||
- [x] Implement proper scope management
|
||||
- [x] Replace GlobalScope with proper lifecycle scopes
|
||||
- [x] Add proper error handling with coroutines
|
||||
|
||||
## Phase 2: Core UX Improvements
|
||||
|
||||
@@ -47,6 +47,16 @@
|
||||
|
||||
### 2.3 Enhanced Messaging Features
|
||||
- [ ] Message status indicators (sent/delivered/read)
|
||||
- [ ] Add status field to MessageEntity (pending/sent/delivered/failed)
|
||||
- [ ] Show status icons in message bubbles
|
||||
- [ ] Update status when delivery confirmed
|
||||
- [ ] Store-and-forward messaging pattern
|
||||
- [ ] Save messages with "pending" status initially
|
||||
- [ ] Implement acknowledgment protocol in WifiAwareManager
|
||||
- [ ] Update to "sent" only after confirmation received
|
||||
- [ ] Queue messages when offline/disconnected
|
||||
- [ ] Auto-retry failed messages with exponential backoff
|
||||
- [ ] Mark messages as failed after max retries
|
||||
- [ ] User presence indicators (online/offline/typing)
|
||||
- [ ] Message timestamps with proper formatting
|
||||
- [ ] Offline message queue
|
||||
@@ -160,18 +170,22 @@
|
||||
## Current Status
|
||||
- ✅ Phase 1.1 - MVVM Architecture - COMPLETED
|
||||
- ✅ Phase 1.2 - Dependency Injection with Hilt - COMPLETED
|
||||
- ✅ Phase 1.3 - Room Database Setup - COMPLETED
|
||||
- ✅ Phase 1.4 - Coroutines & Flow Optimization - COMPLETED
|
||||
- ✅ Phase 2.1 - Connection Status Management - COMPLETED
|
||||
- 🚀 Next Priority Options:
|
||||
- Phase 1.3 - Room Database (Foundation for persistence)
|
||||
- Phase 2.2 - User List Feature (Core UX)
|
||||
- Phase 2.3 - Enhanced Messaging (Better UX)
|
||||
- Phase 3.1 - Material 3 Update (Modern UI)
|
||||
- Phase 3.2 - Dark Theme & Theming
|
||||
|
||||
## Completed Work Summary
|
||||
1. **MVVM Architecture**: ViewModels, Repository pattern, proper separation of concerns
|
||||
2. **Dependency Injection**: Hilt integration with proper scoping and lifecycle management
|
||||
3. **Connection Status**: Visual indicator with real-time updates, activity-based detection
|
||||
4. **Sleep/Wake Handling**: Auto-recovery when messages resume after device sleep
|
||||
3. **Room Database**: Message persistence with proper DAOs and entity mapping
|
||||
4. **Coroutines & Flow**: Converted callbacks to coroutines, implemented Flow for reactive streams, proper scope management
|
||||
5. **Connection Status**: Visual indicator with real-time updates, activity-based detection
|
||||
6. **Sleep/Wake Handling**: Auto-recovery when messages resume after device sleep
|
||||
|
||||
## Development Notes
|
||||
- Architecture foundation (Phase 1) should be completed before moving to advanced features
|
||||
|
||||
@@ -9,7 +9,13 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.mattintech.lchat.utils.LOG_PREFIX
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
class WifiAwareManager(private val context: Context) {
|
||||
@@ -26,27 +32,57 @@ class WifiAwareManager(private val context: Context) {
|
||||
private var subscribeDiscoverySession: SubscribeDiscoverySession? = null
|
||||
|
||||
private val peerHandles = ConcurrentHashMap<String, PeerHandle>()
|
||||
|
||||
// Replace callbacks with Flows
|
||||
private val _messageFlow = MutableSharedFlow<Triple<String, String, String>>()
|
||||
val messageFlow: SharedFlow<Triple<String, String, String>> = _messageFlow.asSharedFlow()
|
||||
|
||||
private val _connectionFlow = MutableSharedFlow<Pair<String, Boolean>>()
|
||||
val connectionFlow: SharedFlow<Pair<String, Boolean>> = _connectionFlow.asSharedFlow()
|
||||
|
||||
// Keep legacy callbacks for backward compatibility
|
||||
private var messageCallback: ((String, String, String) -> Unit)? = null
|
||||
private var connectionCallback: ((String, Boolean) -> Unit)? = null
|
||||
|
||||
private val attachCallback = object : AttachCallback() {
|
||||
override fun onAttached(session: WifiAwareSession) {
|
||||
Log.d(TAG, "Wi-Fi Aware attached")
|
||||
wifiAwareSession = session
|
||||
}
|
||||
// Exception handler for coroutine errors
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e(TAG, "Coroutine exception: ", throwable)
|
||||
}
|
||||
|
||||
override fun onAttachFailed() {
|
||||
Log.e(TAG, "Wi-Fi Aware attach failed")
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceptionHandler)
|
||||
|
||||
fun initialize() {
|
||||
coroutineScope.launch {
|
||||
initializeAsync()
|
||||
}
|
||||
}
|
||||
|
||||
fun initialize() {
|
||||
suspend fun initializeAsync(): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
wifiAwareManager = context.getSystemService(Context.WIFI_AWARE_SERVICE) as? android.net.wifi.aware.WifiAwareManager
|
||||
|
||||
if (wifiAwareManager?.isAvailable == true) {
|
||||
wifiAwareManager?.attach(attachCallback, null)
|
||||
attachToWifiAware()
|
||||
} else {
|
||||
Log.e(TAG, "Wi-Fi Aware is not available")
|
||||
Result.failure(Exception("Wi-Fi Aware is not available"))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun attachToWifiAware(): Result<Unit> = suspendCancellableCoroutine { continuation ->
|
||||
wifiAwareManager?.attach(object : AttachCallback() {
|
||||
override fun onAttached(session: WifiAwareSession) {
|
||||
Log.d(TAG, "Wi-Fi Aware attached")
|
||||
wifiAwareSession = session
|
||||
continuation.resume(Result.success(Unit))
|
||||
}
|
||||
|
||||
override fun onAttachFailed() {
|
||||
Log.e(TAG, "Wi-Fi Aware attach failed")
|
||||
continuation.resume(Result.failure(Exception("Wi-Fi Aware attach failed")))
|
||||
}
|
||||
}, null)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
Log.d(TAG, "Wi-Fi Aware attach cancelled")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +103,15 @@ class WifiAwareManager(private val context: Context) {
|
||||
Log.d(TAG, "Host: Received message: $messageStr")
|
||||
|
||||
if (messageStr == "CONNECT_REQUEST") {
|
||||
Log.d(TAG, "Host: Received connection request")
|
||||
acceptConnection(peerHandle)
|
||||
Log.d(TAG, "Host: Received connection request from peer")
|
||||
coroutineScope.launch {
|
||||
val result = acceptConnectionAsync(peerHandle)
|
||||
if (result.isSuccess) {
|
||||
Log.d(TAG, "Host: Successfully accepted connection")
|
||||
} else {
|
||||
Log.e(TAG, "Host: Failed to accept connection: ${result.exceptionOrNull()?.message}")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleIncomingMessage(peerHandle, message)
|
||||
}
|
||||
@@ -103,9 +146,13 @@ class WifiAwareManager(private val context: Context) {
|
||||
subscribeDiscoverySession?.sendMessage(peerHandle, 0, "CONNECT_REQUEST".toByteArray())
|
||||
|
||||
// Wait a bit for host to prepare, then connect
|
||||
android.os.Handler(android.os.Looper.getMainLooper()).postDelayed({
|
||||
connectToPeer(peerHandle, roomName)
|
||||
}, 500)
|
||||
coroutineScope.launch {
|
||||
delay(500)
|
||||
val result = connectToPeerAsync(peerHandle, roomName)
|
||||
if (result.isFailure) {
|
||||
Log.e(TAG, "Failed to connect to peer: ${result.exceptionOrNull()?.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageReceived(peerHandle: PeerHandle, message: ByteArray) {
|
||||
@@ -115,6 +162,13 @@ class WifiAwareManager(private val context: Context) {
|
||||
}
|
||||
|
||||
private fun connectToPeer(peerHandle: PeerHandle, roomName: String) {
|
||||
coroutineScope.launch {
|
||||
connectToPeerAsync(peerHandle, roomName)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun connectToPeerAsync(peerHandle: PeerHandle, roomName: String): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
Log.d(TAG, "connectToPeer: Starting connection to room: $roomName")
|
||||
val networkSpecifier = WifiAwareNetworkSpecifier.Builder(subscribeDiscoverySession!!, peerHandle)
|
||||
.setPskPassphrase("lchat-secure-key")
|
||||
@@ -129,62 +183,117 @@ class WifiAwareManager(private val context: Context) {
|
||||
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
||||
try {
|
||||
connectivityManager.requestNetwork(networkRequest, object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: android.net.Network) {
|
||||
Log.d(TAG, "onAvailable: Network connected for room: $roomName")
|
||||
connectionCallback?.invoke(roomName, true)
|
||||
try {
|
||||
var isResumed = false
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: android.net.Network) {
|
||||
Log.d(TAG, "onAvailable: Network connected for room: $roomName")
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(Result.success(Unit))
|
||||
}
|
||||
// Emit to flow and legacy callback
|
||||
coroutineScope.launch {
|
||||
_connectionFlow.emit(Pair(roomName, true))
|
||||
}
|
||||
connectionCallback?.invoke(roomName, true)
|
||||
}
|
||||
|
||||
override fun onLost(network: android.net.Network) {
|
||||
Log.d(TAG, "onLost: Network lost for room: $roomName")
|
||||
coroutineScope.launch {
|
||||
_connectionFlow.emit(Pair(roomName, false))
|
||||
}
|
||||
connectionCallback?.invoke(roomName, false)
|
||||
}
|
||||
|
||||
override fun onUnavailable() {
|
||||
Log.e(TAG, "onUnavailable: Network request failed for room: $roomName")
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(Result.failure(Exception("Network unavailable for room: $roomName")))
|
||||
}
|
||||
coroutineScope.launch {
|
||||
_connectionFlow.emit(Pair(roomName, false))
|
||||
}
|
||||
connectionCallback?.invoke(roomName, false)
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: android.net.Network, networkCapabilities: NetworkCapabilities) {
|
||||
Log.d(TAG, "onCapabilitiesChanged: Capabilities changed for room: $roomName")
|
||||
}
|
||||
|
||||
override fun onLinkPropertiesChanged(network: android.net.Network, linkProperties: android.net.LinkProperties) {
|
||||
Log.d(TAG, "onLinkPropertiesChanged: Link properties changed for room: $roomName")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: android.net.Network) {
|
||||
Log.d(TAG, "onLost: Network lost for room: $roomName")
|
||||
connectionCallback?.invoke(roomName, false)
|
||||
}
|
||||
connectivityManager.requestNetwork(networkRequest, callback, android.os.Handler(android.os.Looper.getMainLooper()))
|
||||
|
||||
override fun onUnavailable() {
|
||||
Log.e(TAG, "onUnavailable: Network request failed for room: $roomName")
|
||||
connectionCallback?.invoke(roomName, false)
|
||||
}
|
||||
Log.d(TAG, "connectToPeer: Network request submitted for room: $roomName")
|
||||
|
||||
override fun onCapabilitiesChanged(network: android.net.Network, networkCapabilities: NetworkCapabilities) {
|
||||
Log.d(TAG, "onCapabilitiesChanged: Capabilities changed for room: $roomName")
|
||||
continuation.invokeOnCancellation {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
|
||||
override fun onLinkPropertiesChanged(network: android.net.Network, linkProperties: android.net.LinkProperties) {
|
||||
Log.d(TAG, "onLinkPropertiesChanged: Link properties changed for room: $roomName")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "connectToPeer: Failed to request network", e)
|
||||
continuation.resume(Result.failure(e))
|
||||
coroutineScope.launch {
|
||||
_connectionFlow.emit(Pair(roomName, false))
|
||||
}
|
||||
}, android.os.Handler(android.os.Looper.getMainLooper()), 30000) // 30 second timeout
|
||||
|
||||
Log.d(TAG, "connectToPeer: Network request submitted for room: $roomName")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "connectToPeer: Failed to request network", e)
|
||||
connectionCallback?.invoke(roomName, false)
|
||||
connectionCallback?.invoke(roomName, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun acceptConnection(peerHandle: PeerHandle) {
|
||||
Log.d(TAG, "acceptConnection: Accepting connection from client")
|
||||
val networkSpecifier = WifiAwareNetworkSpecifier.Builder(publishDiscoverySession!!, peerHandle)
|
||||
.setPskPassphrase("lchat-secure-key")
|
||||
.setPort(PORT)
|
||||
.build()
|
||||
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
.setNetworkSpecifier(networkSpecifier)
|
||||
.build()
|
||||
private suspend fun acceptConnectionAsync(peerHandle: PeerHandle): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
Log.d(TAG, "acceptConnection: Accepting connection from client")
|
||||
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
connectivityManager.requestNetwork(networkRequest, object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: android.net.Network) {
|
||||
Log.d(TAG, "Client connected")
|
||||
peerHandles[peerHandle.toString()] = peerHandle
|
||||
try {
|
||||
val networkSpecifier = WifiAwareNetworkSpecifier.Builder(publishDiscoverySession!!, peerHandle)
|
||||
.setPskPassphrase("lchat-secure-key")
|
||||
.setPort(PORT)
|
||||
.build()
|
||||
|
||||
val networkRequest = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
|
||||
.setNetworkSpecifier(networkSpecifier)
|
||||
.build()
|
||||
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
var isResumed = false
|
||||
|
||||
val callback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: android.net.Network) {
|
||||
Log.d(TAG, "Client connected")
|
||||
peerHandles[peerHandle.toString()] = peerHandle
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(Result.success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUnavailable() {
|
||||
Log.e(TAG, "Failed to accept client connection - Check if Wi-Fi is enabled")
|
||||
if (!isResumed) {
|
||||
isResumed = true
|
||||
continuation.resume(Result.failure(Exception("Failed to accept client connection")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectivityManager.requestNetwork(networkRequest, callback)
|
||||
|
||||
continuation.invokeOnCancellation {
|
||||
connectivityManager.unregisterNetworkCallback(callback)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "acceptConnection: Failed to accept connection", e)
|
||||
continuation.resume(Result.failure(e))
|
||||
}
|
||||
|
||||
override fun onUnavailable() {
|
||||
Log.e(TAG, "Failed to accept client connection - Check if Wi-Fi is enabled")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIncomingMessage(peerHandle: PeerHandle, message: ByteArray) {
|
||||
@@ -192,6 +301,10 @@ class WifiAwareManager(private val context: Context) {
|
||||
val messageStr = String(message)
|
||||
val parts = messageStr.split("|", limit = 3)
|
||||
if (parts.size == 3) {
|
||||
// Emit to flow and legacy callback
|
||||
coroutineScope.launch {
|
||||
_messageFlow.emit(Triple(parts[0], parts[1], parts[2]))
|
||||
}
|
||||
messageCallback?.invoke(parts[0], parts[1], parts[2])
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -226,5 +339,6 @@ class WifiAwareManager(private val context: Context) {
|
||||
subscribeDiscoverySession?.close()
|
||||
wifiAwareSession?.close()
|
||||
peerHandles.clear()
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,14 @@ class ChatRepository @Inject constructor(
|
||||
private val wifiAwareManager: WifiAwareManager,
|
||||
private val messageDao: MessageDao
|
||||
) {
|
||||
// Exception handler for repository operations
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
android.util.Log.e("ChatRepository", "Repository coroutine exception: ", throwable)
|
||||
_connectionState.value = ConnectionState.Error(throwable.message ?: "Unknown error")
|
||||
}
|
||||
|
||||
// Repository scope for background operations
|
||||
private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.IO + exceptionHandler)
|
||||
|
||||
private var currentRoomName: String = ""
|
||||
|
||||
@@ -47,37 +55,65 @@ class ChatRepository @Inject constructor(
|
||||
|
||||
init {
|
||||
wifiAwareManager.initialize()
|
||||
setupWifiAwareCallbacks()
|
||||
// Only use Flow-based collection, not callbacks
|
||||
collectWifiAwareFlows()
|
||||
}
|
||||
|
||||
private fun setupWifiAwareCallbacks() {
|
||||
wifiAwareManager.setMessageCallback { userId, userName, content ->
|
||||
val message = Message(
|
||||
id = UUID.randomUUID().toString(),
|
||||
userId = userId,
|
||||
userName = userName,
|
||||
content = content,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
isOwnMessage = false
|
||||
)
|
||||
addMessage(message)
|
||||
private fun collectWifiAwareFlows() {
|
||||
// Collect messages from Flow
|
||||
repositoryScope.launch {
|
||||
try {
|
||||
wifiAwareManager.messageFlow.collect { (userId, userName, content) ->
|
||||
val message = Message(
|
||||
id = UUID.randomUUID().toString(),
|
||||
userId = userId,
|
||||
userName = userName,
|
||||
content = content,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
isOwnMessage = false
|
||||
)
|
||||
addMessage(message)
|
||||
|
||||
// Update last activity time
|
||||
lastActivityTime = System.currentTimeMillis()
|
||||
// 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")
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ChatRepository", "Error collecting message flow", e)
|
||||
}
|
||||
}
|
||||
|
||||
messageCallback?.invoke(userId, userName, content)
|
||||
// Collect connection state from Flow
|
||||
repositoryScope.launch {
|
||||
try {
|
||||
wifiAwareManager.connectionFlow.collect { (roomName, isConnected) ->
|
||||
if (isConnected) {
|
||||
currentRoomName = roomName
|
||||
_connectionState.value = ConnectionState.Connected(roomName)
|
||||
loadMessagesFromDatabase(roomName)
|
||||
} else {
|
||||
_connectionState.value = ConnectionState.Disconnected
|
||||
}
|
||||
// Call the legacy callback if set
|
||||
connectionCallback?.invoke(roomName, isConnected)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ChatRepository", "Error collecting connection flow", e)
|
||||
_connectionState.value = ConnectionState.Error(e.message ?: "Connection error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Removed setupWifiAwareCallbacks - now using Flow-based collection only
|
||||
|
||||
fun startHostMode(roomName: String) {
|
||||
currentRoomName = roomName
|
||||
wifiAwareManager.startHostMode(roomName)
|
||||
@@ -87,10 +123,16 @@ class ChatRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private fun loadMessagesFromDatabase(roomName: String) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val storedMessages = messageDao.getMessagesForRoomOnce(roomName)
|
||||
.map { it.toMessage() }
|
||||
_messages.value = storedMessages
|
||||
repositoryScope.launch {
|
||||
try {
|
||||
val storedMessages = messageDao.getMessagesForRoomOnce(roomName)
|
||||
.map { it.toMessage() }
|
||||
_messages.value = storedMessages
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ChatRepository", "Error loading messages from database", e)
|
||||
// Don't crash, just continue with empty messages
|
||||
_messages.value = emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,11 +165,17 @@ class ChatRepository @Inject constructor(
|
||||
}
|
||||
|
||||
private fun addMessage(message: Message) {
|
||||
_messages.value = _messages.value + message
|
||||
// Add message and sort by timestamp to ensure proper order
|
||||
_messages.value = (_messages.value + message).sortedBy { it.timestamp }
|
||||
|
||||
// Save to database
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
messageDao.insertMessage(message.toEntity(currentRoomName))
|
||||
repositoryScope.launch {
|
||||
try {
|
||||
messageDao.insertMessage(message.toEntity(currentRoomName))
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("ChatRepository", "Error saving message to database", e)
|
||||
// Don't crash, message is already in memory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,16 +189,7 @@ class ChatRepository @Inject constructor(
|
||||
|
||||
fun setConnectionCallback(callback: (String, Boolean) -> Unit) {
|
||||
connectionCallback = callback
|
||||
wifiAwareManager.setConnectionCallback { roomName, isConnected ->
|
||||
if (isConnected) {
|
||||
currentRoomName = roomName
|
||||
_connectionState.value = ConnectionState.Connected(roomName)
|
||||
loadMessagesFromDatabase(roomName)
|
||||
} else {
|
||||
_connectionState.value = ConnectionState.Disconnected
|
||||
}
|
||||
callback(roomName, isConnected)
|
||||
}
|
||||
// Connection state is now handled by Flow collection
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
@@ -158,11 +197,12 @@ class ChatRepository @Inject constructor(
|
||||
wifiAwareManager.stop()
|
||||
_connectionState.value = ConnectionState.Disconnected
|
||||
_connectedUsers.value = emptyList()
|
||||
repositoryScope.cancel()
|
||||
}
|
||||
|
||||
private fun startConnectionMonitoring() {
|
||||
connectionCheckJob?.cancel()
|
||||
connectionCheckJob = CoroutineScope(Dispatchers.IO).launch {
|
||||
connectionCheckJob = repositoryScope.launch {
|
||||
while (isActive) {
|
||||
delay(5000) // Check every 5 seconds
|
||||
val timeSinceLastActivity = System.currentTimeMillis() - lastActivityTime
|
||||
|
||||
@@ -18,6 +18,10 @@ import com.mattintech.lchat.viewmodel.LobbyState
|
||||
import com.mattintech.lchat.viewmodel.LobbyViewModel
|
||||
import com.mattintech.lchat.utils.LOG_PREFIX
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LobbyFragment : Fragment() {
|
||||
@@ -94,48 +98,60 @@ class LobbyFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
viewModel.savedUserName.observe(viewLifecycleOwner) { savedName ->
|
||||
if (!savedName.isNullOrEmpty() && binding.nameInput.text.isNullOrEmpty()) {
|
||||
binding.nameInput.setText(savedName)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.state.observe(viewLifecycleOwner) { state ->
|
||||
when (state) {
|
||||
is LobbyState.Idle -> {
|
||||
binding.noRoomsText.visibility = View.GONE
|
||||
}
|
||||
is LobbyState.Connecting -> {
|
||||
if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) {
|
||||
binding.noRoomsText.visibility = View.VISIBLE
|
||||
binding.noRoomsText.text = getString(R.string.connecting)
|
||||
// Collect saved user name
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.savedUserName.collect { savedName ->
|
||||
if (!savedName.isNullOrEmpty() && binding.nameInput.text.isNullOrEmpty()) {
|
||||
binding.nameInput.setText(savedName)
|
||||
}
|
||||
}
|
||||
is LobbyState.Connected -> {
|
||||
if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) {
|
||||
val userName = binding.nameInput.text?.toString()?.trim() ?: ""
|
||||
viewModel.onConnectedToRoom(state.roomName, userName)
|
||||
}
|
||||
}
|
||||
is LobbyState.Error -> {
|
||||
binding.noRoomsText.visibility = View.VISIBLE
|
||||
binding.noRoomsText.text = state.message
|
||||
Toast.makeText(context, state.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { event ->
|
||||
when (event) {
|
||||
is LobbyEvent.NavigateToChat -> {
|
||||
navigateToChat(event.roomName, event.userName, event.isHost)
|
||||
viewModel.clearEvent()
|
||||
// Collect state
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.state.collect { state ->
|
||||
when (state) {
|
||||
is LobbyState.Idle -> {
|
||||
binding.noRoomsText.visibility = View.GONE
|
||||
}
|
||||
is LobbyState.Connecting -> {
|
||||
if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) {
|
||||
binding.noRoomsText.visibility = View.VISIBLE
|
||||
binding.noRoomsText.text = getString(R.string.connecting)
|
||||
}
|
||||
}
|
||||
is LobbyState.Connected -> {
|
||||
if (binding.modeRadioGroup.checkedRadioButtonId == R.id.clientRadio) {
|
||||
val userName = binding.nameInput.text?.toString()?.trim() ?: ""
|
||||
viewModel.onConnectedToRoom(state.roomName, userName)
|
||||
}
|
||||
}
|
||||
is LobbyState.Error -> {
|
||||
binding.noRoomsText.visibility = View.VISIBLE
|
||||
binding.noRoomsText.text = state.message
|
||||
Toast.makeText(context, state.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
is LobbyEvent.ShowError -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
viewModel.clearEvent()
|
||||
}
|
||||
}
|
||||
|
||||
// Collect events
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.events.collect { event ->
|
||||
when (event) {
|
||||
is LobbyEvent.NavigateToChat -> {
|
||||
navigateToChat(event.roomName, event.userName, event.isHost)
|
||||
}
|
||||
is LobbyEvent.ShowError -> {
|
||||
Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.mattintech.lchat.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mattintech.lchat.data.Message
|
||||
@@ -25,8 +23,8 @@ class ChatViewModel @Inject constructor(
|
||||
private val chatRepository: ChatRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableLiveData<ChatState>(ChatState.Connected)
|
||||
val state: LiveData<ChatState> = _state
|
||||
private val _state = MutableStateFlow<ChatState>(ChatState.Connected)
|
||||
val state: StateFlow<ChatState> = _state.asStateFlow()
|
||||
|
||||
private val _messagesFlow = MutableStateFlow<Flow<List<Message>>>(flowOf(emptyList()))
|
||||
|
||||
@@ -85,10 +83,8 @@ class ChatViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
viewModelScope.launch {
|
||||
chatRepository.stop()
|
||||
_state.value = ChatState.Disconnected
|
||||
}
|
||||
chatRepository.stop()
|
||||
_state.value = ChatState.Disconnected
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.mattintech.lchat.viewmodel
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.mattintech.lchat.repository.ChatRepository
|
||||
import com.mattintech.lchat.utils.PreferencesManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -32,14 +36,14 @@ class LobbyViewModel @Inject constructor(
|
||||
private val preferencesManager: PreferencesManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableLiveData<LobbyState>(LobbyState.Idle)
|
||||
val state: LiveData<LobbyState> = _state
|
||||
private val _state = MutableStateFlow<LobbyState>(LobbyState.Idle)
|
||||
val state: StateFlow<LobbyState> = _state.asStateFlow()
|
||||
|
||||
private val _events = MutableLiveData<LobbyEvent?>()
|
||||
val events: LiveData<LobbyEvent?> = _events
|
||||
private val _events = MutableSharedFlow<LobbyEvent>()
|
||||
val events: SharedFlow<LobbyEvent> = _events.asSharedFlow()
|
||||
|
||||
private val _savedUserName = MutableLiveData<String?>()
|
||||
val savedUserName: LiveData<String?> = _savedUserName
|
||||
private val _savedUserName = MutableStateFlow<String?>(null)
|
||||
val savedUserName: StateFlow<String?> = _savedUserName.asStateFlow()
|
||||
|
||||
init {
|
||||
setupConnectionCallback()
|
||||
@@ -64,12 +68,16 @@ class LobbyViewModel @Inject constructor(
|
||||
|
||||
fun startHostMode(roomName: String, userName: String) {
|
||||
if (roomName.isBlank()) {
|
||||
_events.value = LobbyEvent.ShowError("Please enter a room name")
|
||||
viewModelScope.launch {
|
||||
_events.emit(LobbyEvent.ShowError("Please enter a room name"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (userName.isBlank()) {
|
||||
_events.value = LobbyEvent.ShowError("Please enter your name")
|
||||
viewModelScope.launch {
|
||||
_events.emit(LobbyEvent.ShowError("Please enter your name"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -77,13 +85,15 @@ class LobbyViewModel @Inject constructor(
|
||||
_state.value = LobbyState.Connecting
|
||||
preferencesManager.saveUserName(userName)
|
||||
chatRepository.startHostMode(roomName)
|
||||
_events.value = LobbyEvent.NavigateToChat(roomName, userName, true)
|
||||
_events.emit(LobbyEvent.NavigateToChat(roomName, userName, true))
|
||||
}
|
||||
}
|
||||
|
||||
fun startClientMode(userName: String) {
|
||||
if (userName.isBlank()) {
|
||||
_events.value = LobbyEvent.ShowError("Please enter your name")
|
||||
viewModelScope.launch {
|
||||
_events.emit(LobbyEvent.ShowError("Please enter your name"))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -96,11 +106,13 @@ class LobbyViewModel @Inject constructor(
|
||||
|
||||
fun onConnectedToRoom(roomName: String, userName: String) {
|
||||
preferencesManager.saveUserName(userName)
|
||||
_events.value = LobbyEvent.NavigateToChat(roomName, userName, false)
|
||||
viewModelScope.launch {
|
||||
_events.emit(LobbyEvent.NavigateToChat(roomName, userName, false))
|
||||
}
|
||||
}
|
||||
|
||||
fun clearEvent() {
|
||||
_events.value = null
|
||||
// SharedFlow doesn't need clearing, events are consumed once
|
||||
}
|
||||
|
||||
fun saveUserName(name: String) {
|
||||
|
||||
Reference in New Issue
Block a user