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