diff --git a/app/build.gradle.kts b/app/build.gradle.kts index aa1e6a5..1e52234 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,12 @@ dependencies { implementation("com.google.dagger:hilt-android:2.50") kapt("com.google.dagger:hilt-compiler:2.50") + // Room dependencies + val roomVersion = "2.6.1" + implementation("androidx.room:room-runtime:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + kapt("androidx.room:room-compiler:$roomVersion") + testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") diff --git a/app/src/main/java/com/mattintech/lchat/data/db/LChatDatabase.kt b/app/src/main/java/com/mattintech/lchat/data/db/LChatDatabase.kt new file mode 100644 index 0000000..beeeab1 --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/data/db/LChatDatabase.kt @@ -0,0 +1,15 @@ +package com.mattintech.lchat.data.db + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.mattintech.lchat.data.db.dao.MessageDao +import com.mattintech.lchat.data.db.entities.MessageEntity + +@Database( + entities = [MessageEntity::class], + version = 1, + exportSchema = false +) +abstract class LChatDatabase : RoomDatabase() { + abstract fun messageDao(): MessageDao +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/data/db/dao/MessageDao.kt b/app/src/main/java/com/mattintech/lchat/data/db/dao/MessageDao.kt new file mode 100644 index 0000000..756422e --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/data/db/dao/MessageDao.kt @@ -0,0 +1,29 @@ +package com.mattintech.lchat.data.db.dao + +import androidx.room.* +import com.mattintech.lchat.data.db.entities.MessageEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface MessageDao { + @Query("SELECT * FROM messages WHERE roomName = :roomName ORDER BY timestamp ASC") + fun getMessagesForRoom(roomName: String): Flow> + + @Query("SELECT * FROM messages WHERE roomName = :roomName ORDER BY timestamp ASC") + suspend fun getMessagesForRoomOnce(roomName: String): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMessage(message: MessageEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMessages(messages: List) + + @Query("DELETE FROM messages WHERE roomName = :roomName") + suspend fun deleteMessagesForRoom(roomName: String) + + @Query("DELETE FROM messages") + suspend fun deleteAllMessages() + + @Query("SELECT COUNT(*) FROM messages WHERE roomName = :roomName") + suspend fun getMessageCountForRoom(roomName: String): Int +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/data/db/entities/MessageEntity.kt b/app/src/main/java/com/mattintech/lchat/data/db/entities/MessageEntity.kt new file mode 100644 index 0000000..92ab367 --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/data/db/entities/MessageEntity.kt @@ -0,0 +1,16 @@ +package com.mattintech.lchat.data.db.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "messages") +data class MessageEntity( + @PrimaryKey + val id: String, + val roomName: String, + val userId: String, + val userName: String, + val content: String, + val timestamp: Long, + val isOwnMessage: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/data/db/mappers/MessageMapper.kt b/app/src/main/java/com/mattintech/lchat/data/db/mappers/MessageMapper.kt new file mode 100644 index 0000000..6cf80b2 --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/data/db/mappers/MessageMapper.kt @@ -0,0 +1,27 @@ +package com.mattintech.lchat.data.db.mappers + +import com.mattintech.lchat.data.Message +import com.mattintech.lchat.data.db.entities.MessageEntity + +fun MessageEntity.toMessage(): Message { + return Message( + id = id, + userId = userId, + userName = userName, + content = content, + timestamp = timestamp, + isOwnMessage = isOwnMessage + ) +} + +fun Message.toEntity(roomName: String): MessageEntity { + return MessageEntity( + id = id, + roomName = roomName, + userId = userId, + userName = userName, + content = content, + timestamp = timestamp, + isOwnMessage = isOwnMessage + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/data/db/migrations/Migrations.kt b/app/src/main/java/com/mattintech/lchat/data/db/migrations/Migrations.kt new file mode 100644 index 0000000..7340bcb --- /dev/null +++ b/app/src/main/java/com/mattintech/lchat/data/db/migrations/Migrations.kt @@ -0,0 +1,14 @@ +package com.mattintech.lchat.data.db.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +// Placeholder for future migrations +object Migrations { + // Example migration from version 1 to 2 + // val MIGRATION_1_2 = object : Migration(1, 2) { + // override fun migrate(database: SupportSQLiteDatabase) { + // // Migration code here + // } + // } +} \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/di/AppModule.kt b/app/src/main/java/com/mattintech/lchat/di/AppModule.kt index 109c1cc..17f7e79 100644 --- a/app/src/main/java/com/mattintech/lchat/di/AppModule.kt +++ b/app/src/main/java/com/mattintech/lchat/di/AppModule.kt @@ -1,6 +1,9 @@ package com.mattintech.lchat.di import android.content.Context +import androidx.room.Room +import com.mattintech.lchat.data.db.LChatDatabase +import com.mattintech.lchat.data.db.dao.MessageDao import com.mattintech.lchat.network.WifiAwareManager import dagger.Module import dagger.Provides @@ -20,4 +23,21 @@ object AppModule { ): WifiAwareManager { return WifiAwareManager(context) } + + @Provides + @Singleton + fun provideLChatDatabase( + @ApplicationContext context: Context + ): LChatDatabase { + return Room.databaseBuilder( + context, + LChatDatabase::class.java, + "lchat_database" + ).build() + } + + @Provides + fun provideMessageDao(database: LChatDatabase): MessageDao { + return database.messageDao() + } } \ No newline at end of file diff --git a/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt b/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt index a97a8b9..ff31552 100644 --- a/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt +++ b/app/src/main/java/com/mattintech/lchat/repository/ChatRepository.kt @@ -2,26 +2,36 @@ package com.mattintech.lchat.repository import android.content.Context import com.mattintech.lchat.data.Message +import com.mattintech.lchat.data.db.dao.MessageDao +import com.mattintech.lchat.data.db.mappers.toEntity +import com.mattintech.lchat.data.db.mappers.toMessage import com.mattintech.lchat.network.WifiAwareManager import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.* import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @Singleton class ChatRepository @Inject constructor( - @ApplicationContext private val context: Context + @ApplicationContext private val context: Context, + private val wifiAwareManager: WifiAwareManager, + private val messageDao: MessageDao ) { - private val wifiAwareManager = WifiAwareManager(context) + private var currentRoomName: String = "" private val _messages = MutableStateFlow>(emptyList()) val messages: StateFlow> = _messages.asStateFlow() + // Flow that combines in-memory and database messages + fun getMessagesFlow(roomName: String): Flow> { + return messageDao.getMessagesForRoom(roomName) + .map { entities -> entities.map { it.toMessage() } } + .onStart { loadMessagesFromDatabase(roomName) } + } + private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) val connectionState: StateFlow = _connectionState.asStateFlow() @@ -69,9 +79,19 @@ class ChatRepository @Inject constructor( } fun startHostMode(roomName: String) { + currentRoomName = roomName wifiAwareManager.startHostMode(roomName) _connectionState.value = ConnectionState.Hosting(roomName) startConnectionMonitoring() + loadMessagesFromDatabase(roomName) + } + + private fun loadMessagesFromDatabase(roomName: String) { + CoroutineScope(Dispatchers.IO).launch { + val storedMessages = messageDao.getMessagesForRoomOnce(roomName) + .map { it.toMessage() } + _messages.value = storedMessages + } } fun startClientMode() { @@ -104,6 +124,11 @@ class ChatRepository @Inject constructor( private fun addMessage(message: Message) { _messages.value = _messages.value + message + + // Save to database + CoroutineScope(Dispatchers.IO).launch { + messageDao.insertMessage(message.toEntity(currentRoomName)) + } } fun clearMessages() { @@ -118,7 +143,9 @@ class ChatRepository @Inject constructor( connectionCallback = callback wifiAwareManager.setConnectionCallback { roomName, isConnected -> if (isConnected) { + currentRoomName = roomName _connectionState.value = ConnectionState.Connected(roomName) + loadMessagesFromDatabase(roomName) } else { _connectionState.value = ConnectionState.Disconnected } diff --git a/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt b/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt index 1d241e2..7337681 100644 --- a/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt +++ b/app/src/main/java/com/mattintech/lchat/viewmodel/ChatViewModel.kt @@ -7,9 +7,8 @@ import androidx.lifecycle.viewModelScope import com.mattintech.lchat.data.Message import com.mattintech.lchat.repository.ChatRepository import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import java.util.UUID import javax.inject.Inject @@ -20,6 +19,7 @@ sealed class ChatState { data class Error(val message: String) : ChatState() } +@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class ChatViewModel @Inject constructor( private val chatRepository: ChatRepository @@ -28,7 +28,10 @@ class ChatViewModel @Inject constructor( private val _state = MutableLiveData(ChatState.Connected) val state: LiveData = _state - val messages: StateFlow> = chatRepository.messages + private val _messagesFlow = MutableStateFlow>>(flowOf(emptyList())) + + val messages: StateFlow> = _messagesFlow + .flatMapLatest { it } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), @@ -60,6 +63,9 @@ class ChatViewModel @Inject constructor( this.isHost = isHost this.currentUserId = UUID.randomUUID().toString() + // Set up messages flow for this room + _messagesFlow.value = chatRepository.getMessagesFlow(roomName) + // Setup message callback if needed for additional processing chatRepository.setMessageCallback { userId, userName, content -> // Can add additional message processing here if needed