Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use lifecycle-process to process chat replies #31

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ dependencies {
implementation(libs.lifecycle.ktx)
implementation(libs.lifecycle.compose)
implementation(libs.lifecycle.runtime.compose)
implementation(libs.lifecycle.process)

ksp(libs.room.compiler)
implementation(libs.room.ktx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@
package com.google.android.samples.socialite

import android.app.Application
import androidx.lifecycle.ProcessLifecycleOwner
import com.google.android.samples.socialite.repository.ChatReplyHelper
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class SocialApp : Application()
class SocialApp : Application() {

@Inject
lateinit var chatReplyHelper: ChatReplyHelper

override fun onCreate() {
super.onCreate()
chatReplyHelper.start(ProcessLifecycleOwner.get().lifecycle)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,7 @@ interface MessageDao {

@Query("DELETE FROM Message")
suspend fun clearAll()

@Query("SELECT * FROM Message WHERE senderId = 0 ORDER BY timestamp DESC LIMIT 1")
fun latestOutgoingMessage(): Flow<Message>
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import java.util.concurrent.Executors
import javax.inject.Qualifier
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher

@Qualifier
annotation class AppCoroutineScope

@Module
@InstallIn(SingletonComponent::class)
Expand All @@ -63,13 +56,6 @@ object DatabaseModule {

@Provides
fun providesContactDao(database: AppDatabase): ContactDao = database.contactDao()

@Provides
@Singleton
@AppCoroutineScope
fun providesApplicationCoroutineScope(): CoroutineScope = CoroutineScope(
Executors.newSingleThreadExecutor().asCoroutineDispatcher(),
)
}

@Module
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.samples.socialite.repository

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.google.android.samples.socialite.model.Message
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class ChatReplyHelper @Inject constructor(
private val repository: ChatRepository,
) {

fun start(lifecycle: Lifecycle) {
lifecycle.coroutineScope.launch {
var first = true
// Prevents the same outgoing message to be processed twice.
var previousMessageId = 0L
// The Flow from Room is collected every time the table is modified. The same outgoing
// message can be passed multiple times.
repository.getLatestOutgoingMessage().collect { message ->
if (first) {
// Ignore the first message because it is an existing latest message.
first = false
} else if (previousMessageId != message.id) {
handleNewMessage(message)
previousMessageId = message.id
}
}
}
}

/**
* message: A [Message] sent by the user.
*/
private suspend fun handleNewMessage(message: Message) {
// The person is typing...
delay(5000L)
val replier = repository.findReplier(message) ?: return
repository.receiveMessage(
replier.reply(message.text).apply { chatId = message.chatId }.build(),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,19 @@
package com.google.android.samples.socialite.repository

import com.google.android.samples.socialite.data.ChatDao
import com.google.android.samples.socialite.data.ContactDao
import com.google.android.samples.socialite.data.MessageDao
import com.google.android.samples.socialite.di.AppCoroutineScope
import com.google.android.samples.socialite.model.ChatDetail
import com.google.android.samples.socialite.model.Contact
import com.google.android.samples.socialite.model.Message
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

@Singleton
class ChatRepository @Inject internal constructor(
private val chatDao: ChatDao,
private val messageDao: MessageDao,
private val contactDao: ContactDao,
private val notificationHelper: NotificationHelper,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
) {
private var currentChat: Long = 0L

Expand All @@ -56,6 +49,18 @@ class ChatRepository @Inject internal constructor(
return messageDao.allByChatId(chatId)
}

fun getLatestOutgoingMessage(): Flow<Message> {
return messageDao.latestOutgoingMessage()
}

suspend fun findReplier(message: Message): Contact? {
// Find the chat room.
val detail = chatDao.loadDetailById(message.chatId) ?: return null
// Take the first contact in the chat room to reply.
// TODO: Take group chat into account.
return detail.firstContact
}

suspend fun sendMessage(
chatId: Long,
text: String,
Expand All @@ -76,25 +81,22 @@ class ChatRepository @Inject internal constructor(
),
)
notificationHelper.pushShortcut(detail.firstContact, PushReason.OutgoingMessage)
// Simulate a response from the peer.
// The code here is just for demonstration purpose in this sample.
// Real apps will use their server backend and Firebase Cloud Messaging to deliver messages.
coroutineScope.launch {
// The person is typing...
delay(5000L)
// Receive a reply.
messageDao.insert(
detail.firstContact.reply(text).apply { this.chatId = chatId }.build(),
}

suspend fun receiveMessage(
message: Message,
) {
val detail = chatDao.loadDetailById(message.chatId) ?: return
// Receive a reply.
messageDao.insert(message)
notificationHelper.pushShortcut(detail.firstContact, PushReason.IncomingMessage)
// Show notification if the chat is not on the foreground.
if (message.chatId != currentChat) {
notificationHelper.showNotification(
detail.firstContact,
messageDao.loadAll(message.chatId),
false,
)
notificationHelper.pushShortcut(detail.firstContact, PushReason.IncomingMessage)
// Show notification if the chat is not on the foreground.
if (chatId != currentChat) {
notificationHelper.showNotification(
detail.firstContact,
messageDao.loadAll(chatId),
false,
)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-com
junit = { group = "junit", name = "junit", version.ref = "junit" }
lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
lifecycle-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "lifecycle" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" }
media3-effect = { group = "androidx.media3", name = "media3-effect", version.ref = "media3" }
Expand Down