================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Gowtham Balamurugan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# LetsChat
LetsChat is a Sample Messaging Android application built to demonstrate the use of Modern Android development tools - (Kotlin, Coroutines, Dagger-Hilt, Architecture Components, MVVM, Room, Coil) and Firebase
- Create a firebase project and replace the google-services.json file which you get from your firebase project console
- Following firebase services need to be enabled in the firebase console
- Phone Auth
- Cloud Firestore
- Realtime Database
- Storage
- Composite indexes should be created for contact query(link for enabling indexes could be found from logcat while using the app)
***You can Install and test latest LetsChat app from below 👇***
[](https://github.com/a914-gowtham/LetsChat/blob/master/app/app-debug.apk)
## Features ✨
- One on one chat
- Group Chat
- Typing status for one on one and group chat
- Unread messages count
- Message status for failed,sent,delivered and seen
- Supported message types
- Text
- Voice
- Sticker and Gif
- Attachments
- Image
- Video - InProgress
- Notification actions for reply and mark as read
- Search users by username
## Built With 🛠
- [Kotlin](https://kotlinlang.org/) - First class and official programming language for Android development.
- [Coroutines & Flow](https://kotlinlang.org/docs/reference/coroutines-overview.html) - For asynchronous and more..
- [Android Architecture Components](https://developer.android.com/topic/libraries/architecture) - Collection of libraries that help you design quality, robust, testable, and maintainable apps.
- [Navigation Component](https://developer.android.com/guide/navigation/navigation-getting-started) - Handle everything needed for in-app navigation with a single Activity.
- [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) - Data objects that notify views when the underlying database changes.
- [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel) - Stores UI-related data that isn't destroyed on UI changes.
- [DataBinding](https://github.com/android/databinding-samples) - Generates a binding class for each XML layout file present in that module and allows you to more easily write code that interacts with views.Declaratively bind observable data to UI elements.
- [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) - WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts.
- [Room](https://developer.android.com/topic/libraries/architecture/room) - SQLite object mapping library.
- [Dependency Injection](https://developer.android.com/training/dependency-injection) -
- [Dagger-Hilt](https://dagger.dev/hilt/) - Standard library to incorporate Dagger dependency injection into an Android application.
- [Hilt-ViewModel](https://developer.android.com/training/dependency-injection/hilt-jetpack) - DI for injecting `ViewModel`.
- [Firebase](https://firebase.google.com/) -
- [Cloud Messaging](https://firebase.google.com/products/cloud-messaging) - For Sending Notification to client app.
- [Cloud Firestore](https://firebase.google.com/docs/firestore) - Flexible, scalable NoSQL cloud database to store and sync data.
- [Cloud Storage](https://firebase.google.com/docs/storage) - For Store and serve user-generated content.
- [Authentication](https://firebase.google.com/docs/auth) - For Creating account with mobile number.
- [Kotlin Serializer](https://github.com/Kotlin/kotlinx.serialization) - Convert Specific Classes to and from JSON.Runtime library with core serialization API and support libraries with various serialization formats.
- [Coil-kt](https://coil-kt.github.io/coil/) - An image loading library for Android backed by Kotlin Coroutines.
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/app-debug.apk
================================================
[File too large to display: 16.0 MB]
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.firebase.crashlytics'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlinx-serialization'
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
buildFeatures{
dataBinding = true
viewBinding = true
}
defaultConfig {
applicationId "com.gowtham.letschat"
minSdkVersion 19
targetSdkVersion 30
versionCode 1
versionName "1.0"
buildConfigField("String","SERVER_KEY",SERVER_KEY)
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
sourceSets {
main {
assets.srcDirs = ['src/main/assets', 'src/main/assets/']
res.srcDirs = ['src/main/res', 'src/main/res/drawable']
}
}
dexOptions {
javaMaxHeapSize "4g"
}
}
dependencies {
def work_version = "2.5.0"
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.11'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
// Activity KTX for viewModels()
implementation "androidx.activity:activity-ktx:1.2.3"
//Databinding compiler
kapt "com.android.databinding:compiler:3.1.4"
//dagger-hilt
implementation "com.google.dagger:hilt-android:2.36" //don't upgrade unless same version of kapt availale
kapt "com.google.dagger:hilt-android-compiler:2.36"
implementation 'androidx.hilt:hilt-work:1.0.0'
kapt "androidx.hilt:hilt-compiler:1.0.0"
//event bus
implementation 'org.greenrobot:eventbus:3.2.0'
implementation "androidx.recyclerview:recyclerview:1.2.1"
//mvvm
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
//Android Navigation Architecture
implementation "androidx.navigation:navigation-fragment-ktx:2.3.5"
implementation "androidx.navigation:navigation-ui-ktx:2.3.5"
// Room
implementation "androidx.room:room-runtime:2.4.0-alpha03"
kapt "androidx.room:room-compiler:2.4.0-alpha03"
// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:2.4.0-alpha03"
//For device to device notification sending
implementation 'com.github.a914-gowtham:fcm-sender:1.0.2'
//Lottie
implementation 'com.airbnb.android:lottie:3.7.0'
//firebase
//By using the Firebase Android BoM, your app will always use compatible versions of the Firebase Android libraries.
implementation platform('com.google.firebase:firebase-bom:26.0.0')
// When using the BoM, you don't specify versions in Firebase library dependencies
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-auth-ktx'
implementation 'com.google.firebase:firebase-firestore-ktx'
implementation 'androidx.browser:browser:1.3.0' //
implementation 'com.google.firebase:firebase-storage-ktx'
implementation 'com.google.firebase:firebase-messaging-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-database-ktx'
implementation 'com.jakewharton.timber:timber:4.7.1'
//Image loader
implementation("io.coil-kt:coil:1.2.1")
implementation("io.coil-kt:coil-gif:1.0.0")
implementation 'com.github.CanHub:Android-Image-Cropper:3.3.5'
//image zoom
implementation 'com.github.stfalcon-studio:StfalconImageViewer:v1.0.1'
//Kotlin seriler
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1")
// Work Manager
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.datastore:datastore-preferences:1.0.0-beta01"
// Local Unit Tests
implementation "androidx.test:core:1.3.0"
testImplementation "junit:junit:4.13.2"
testImplementation "org.hamcrest:hamcrest-all:1.3"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.robolectric:robolectric:4.3.1"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"
testImplementation "com.google.truth:truth:1.0.1"
testImplementation "org.mockito:mockito-core:2.21.0"
// Instrumented Unit Tests
androidTestImplementation "junit:junit:4.13.2"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.1"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
androidTestImplementation "org.mockito:mockito-core:2.28.2"
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.35'
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.36'
}
================================================
FILE: app/google-services.json
================================================
{
"project_info": {
"project_number": "713876726773",
"firebase_url": "https://letschat-31c80.firebaseio.com",
"project_id": "letschat-31c80",
"storage_bucket": "letschat-31c80.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:713876726773:android:7516967105f42edfd8b6bc",
"android_client_info": {
"package_name": "com.gowtham.letschat"
}
},
"oauth_client": [
{
"client_id": "713876726773-jefdmp8hqi7meehikl7qc0knr5elvgdg.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.gowtham.letschat",
"certificate_hash": "8f067951f97ceb66ed871e1bdf62404f32ce1101"
}
},
{
"client_id": "713876726773-mrqj2mpsm1jcavcursp6vmelglkeb7i6.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.gowtham.letschat",
"certificate_hash": "a7ee622ee9d484ca3a3f44b18bfdb642f21bba6d"
}
},
{
"client_id": "713876726773-n8oqdoievdudn72eulv0t5ebqv8mah61.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.gowtham.letschat",
"certificate_hash": "542c95c560a25358c37923ddd271a737c730c131"
}
},
{
"client_id": "713876726773-0clemuuk99ed2clrqjor4u6s8jkgl98h.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAZfALlXiAfKOOQCAbcgh9wRphqYhhnYOI"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "713876726773-0clemuuk99ed2clrqjor4u6s8jkgl98h.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
}
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/FlowUtilAndroidTest.kt
================================================
package com.gowtham.letschat
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.gowtham.letschat.utils.LogMessage
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onCompletion
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun Flow.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val scope= CoroutineScope(Dispatchers.IO).launch {
this@getOrAwaitValue.collect {
data=it
cancel()
latch.countDown()
}
}
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
if (scope.isActive)
scope.cancel()
}
@Suppress("UNCHECKED_CAST")
return data as T
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/HiltTestRunner.kt
================================================
package com.gowtham.letschat
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/LiveDataUtilAndroidTest.kt
================================================
package com.gowtham.letschat
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
/**
* Gets the value of a [LiveData] or waits for it to have one, with a timeout.
*
* Use this extension from host-side (JVM) tests. It's recommended to use it alongside
* `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
*/
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun LiveData.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/db/daos/ChatUserDaoTest.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.models.UserProfile
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import javax.inject.Inject
import javax.inject.Named
@ExperimentalCoroutinesApi
@SmallTest
@HiltAndroidTest
class ChatUserDaoTest {
@get:Rule
var hiltRule=HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule=InstantTaskExecutorRule()
@Inject
@Named("test_db")
lateinit var database: ChatUserDatabase
private lateinit var chatUserDao: ChatUserDao
@Before
fun setUp(){
hiltRule.inject()
chatUserDao=database.getChatUserDao()
}
@After
fun tearDown(){
database.close()
}
@Test
fun insert_ChatUser() = runBlockingTest {
val chatUser=ChatUser("testUser1","Gowtham", UserProfile("testUser1",13232113L,123321321L),)
chatUserDao.insertUser(chatUser)
val chatUsers=chatUserDao.getChatUserList()
assertThat(chatUsers).contains(chatUser)
}
@Test
fun get_ChatUser_ById() = runBlockingTest {
val user=ChatUser("testId","Gowtham", UserProfile("testId",13232113L,123321321L),)
chatUserDao.insertUser(user)
val chatUser=chatUserDao.getChatUserById("testId")
assertThat(chatUser).isNotNull()
}
@Test
fun delete_User_ById() = runBlockingTest {
val user=ChatUser("testDeleteUserId","Gowtham", UserProfile("testDeleteUserId",13232113L,123321321L),)
chatUserDao.insertUser(user)
chatUserDao.deleteUserById("testDeleteUserId")
val chatUsers=chatUserDao.getChatUserList()
assertThat(chatUsers).doesNotContain(user)
}
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/db/daos/GroupDaoTest.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.getOrAwaitValue
import com.gowtham.letschat.models.UserProfile
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
import javax.inject.Named
@ExperimentalCoroutinesApi
@SmallTest
@HiltAndroidTest
class GroupDaoTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule= InstantTaskExecutorRule()
@Inject
@Named("test_db")
lateinit var database: ChatUserDatabase
private lateinit var groupDao: GroupDao
@Before
fun setUp() {
hiltRule.inject()
groupDao = database.getGroupDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun insert_Group() = runBlockingTest {
val group=Group("testId",members = ArrayList(),profiles = ArrayList())
groupDao.insertGroup(group)
val groups=groupDao.getAllGroup().getOrAwaitValue()
assertThat(groups).contains(group)
}
@Test
fun insert_Multiple_Group() {
runBlockingTest {
val group1=Group("testId1",members = ArrayList(),profiles = ArrayList())
val group2=Group("testId2",members = ArrayList(),profiles = ArrayList())
groupDao.insertMultipleGroup(listOf(group1,group2))
val groups=groupDao.getAllGroup().getOrAwaitValue()
assertThat(groups).containsAtLeast(group1,group2)
}
}
@Test
fun get_Group_ById() {
runBlockingTest {
val newGroup=Group("testId8",members = ArrayList(),profiles = ArrayList())
groupDao.insertGroup(newGroup)
val group=groupDao.getGroupById(newGroup.id)
assertThat(group).isNotNull()
}
}
@Test
fun delete_Group_ById() {
runBlockingTest {
val group=Group("testId5",members = ArrayList(),profiles = ArrayList())
groupDao.insertGroup(group)
groupDao.deleteGroupById(group.id)
val groups=groupDao.getAllGroup().getOrAwaitValue()
assertThat(groups).doesNotContain(group)
}
}
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/db/daos/GroupMessageDaoTest.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.db.data.*
import com.gowtham.letschat.getOrAwaitValue
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
import javax.inject.Named
@ExperimentalCoroutinesApi
@SmallTest
@HiltAndroidTest
class GroupMessageDaoTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule= InstantTaskExecutorRule()
@Inject
@Named("test_db")
lateinit var database: ChatUserDatabase
private lateinit var groupMessageDao: GroupMessageDao
@Before
fun setUp() {
hiltRule.inject()
groupMessageDao = database.getGroupMessageDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun insert_Message() = runBlockingTest {
val message=GroupMessage(1,"testGroupId","fromMe", ArrayList(),
"gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),deliveryTime = ArrayList(),seenTime = ArrayList(),status = ArrayList()
)
groupMessageDao.insertMessage(message)
val messages=groupMessageDao.getAllMessages().getOrAwaitValue()
assertThat(messages).contains(message)
}
@Test
fun insert_Multiple_Messages() {
runBlockingTest {
val message1=GroupMessage(2,"testGroupId1","fromMe", ArrayList(),
"gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),deliveryTime = ArrayList(),seenTime = ArrayList(),status = ArrayList()
)
val message2=GroupMessage(3,"testGroupId2","fromMe", ArrayList(),
"gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),deliveryTime = ArrayList(),seenTime = ArrayList(),status = ArrayList()
)
groupMessageDao.insertMultipleMessage(listOf(message1,message2))
val messages=groupMessageDao.getAllMessages().getOrAwaitValue()
assertThat(messages).containsAtLeast(message1,message2)
}
}
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/db/daos/MessageDaoTest.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.db.data.*
import com.gowtham.letschat.getOrAwaitValue
import com.gowtham.letschat.models.UserProfile
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject
import javax.inject.Named
@ExperimentalCoroutinesApi
@SmallTest
@HiltAndroidTest
class MessageDaoTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule= InstantTaskExecutorRule()
@Inject
@Named("test_db")
lateinit var database: ChatUserDatabase
private lateinit var messageDao: MessageDao
@Before
fun setUp() {
hiltRule.inject()
messageDao = database.getMessageDao()
}
@After
fun tearDown() {
database.close()
}
@Test
fun insert_Message() = runBlockingTest {
val message=Message(2,
0,0,"fromId","toId","Gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),chatUserId = "",chatUsers = ArrayList(),
)
messageDao.insertMessage(message)
val messages=messageDao.getAllMessages().getOrAwaitValue()
assertThat(messages).contains(message)
}
@Test
fun insert_Multiple_Messages() {
runBlockingTest {
val message1=Message(3,
0,0,"fromId","toId","Gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),chatUserId = "",chatUsers = ArrayList(),
)
val message2=Message(4,
0,0,"fromId","toId","Gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),chatUserId = "",chatUsers = ArrayList(),
)
messageDao.insertMultipleMessage(listOf(message1,message2))
val messages=messageDao.getAllMessages().getOrAwaitValue()
assertThat(messages).containsAtLeast(message1,message2)
}
}
@Test
fun get_Message_ById() {
runBlockingTest {
val messageId=5L
val message=Message(messageId,
0,0,"fromId","toId","Gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),chatUserId = "",chatUsers = ArrayList(),
)
messageDao.insertMessage(message)
val msg=messageDao.getMessageById(messageId)
assertThat(msg).isNotNull()
}
}
@Test
fun delete_Message_ById() {
runBlockingTest {
val messageId=6L
val message=Message(messageId,
0,0,"fromId","toId","Gowtham","",textMessage = TextMessage(),
imageMessage = ImageMessage(),audioMessage = AudioMessage(),videoMessage = VideoMessage(),
fileMessage = FileMessage(),chatUserId = "",chatUsers = ArrayList(),
)
messageDao.insertMessage(message)
messageDao.deleteMessageByCreatedAt(messageId)
val messages=messageDao.getAllMessages().getOrAwaitValue()
assertThat(messages).doesNotContain(message)
}
}
}
================================================
FILE: app/src/androidTest/java/com/gowtham/letschat/di/TestAppModule.kt
================================================
package com.gowtham.letschat.di
import android.content.Context
import androidx.room.Room
import com.gowtham.letschat.db.ChatUserDatabase
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Named
@Module
@InstallIn(SingletonComponent::class)
class TestAppModule {
@Provides
@Named("test_db")
fun provideInMemoryDb(@ApplicationContext context: Context): ChatUserDatabase{
return Room.inMemoryDatabaseBuilder(
context,
ChatUserDatabase::class.java
).allowMainThreadQueries().build()
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/gowtham/letschat/FirebasePush.kt
================================================
package com.gowtham.letschat
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
import androidx.core.app.RemoteInput
import coil.ImageLoader
import coil.request.ImageRequest
import coil.request.SuccessResult
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.gowtham.letschat.core.ChatUserUtil
import com.gowtham.letschat.core.GroupMsgStatusUpdater
import com.gowtham.letschat.core.GroupQuery
import com.gowtham.letschat.core.MessageStatusUpdater
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.daos.GroupMessageDao
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.db.data.*
import com.gowtham.letschat.di.GroupCollection
import com.gowtham.letschat.di.MessageCollection
import com.gowtham.letschat.models.PushMsg
import com.gowtham.letschat.ui.activities.MainActivity
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.utils.Constants.ACTION_GROUP_NEW_MESSAGE
import com.gowtham.letschat.utils.Constants.ACTION_LOGGED_IN_ANOTHER_DEVICE
import com.gowtham.letschat.utils.Constants.ACTION_MARK_AS_READ
import com.gowtham.letschat.utils.Constants.ACTION_NEW_MESSAGE
import com.gowtham.letschat.utils.Constants.ACTION_REPLY
import com.gowtham.letschat.utils.Constants.CHAT_USER_DATA
import com.gowtham.letschat.utils.Constants.GROUP_DATA
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Inject
const val TYPE_LOGGED_IN = "new_logged_in"
const val TYPE_NEW_MESSAGE = "new_message"
const val TYPE_NEW_GROUP = "new_group"
const val TYPE_NEW_GROUP_MESSAGE = "new_group_message"
const val GROUP_KEY = "com.mygroupkey"
const val SUMMARY_ID = 0
const val KEY_TEXT_REPLY = "key_text_reply"
@AndroidEntryPoint
class FirebasePush : FirebaseMessagingService(), OnSuccessListener {
@Inject
lateinit var preference: MPreference
@Inject
lateinit var dbRepository: DbRepository
@Inject
lateinit var usersCollection: CollectionReference
@Inject
lateinit var messageStatusUpdater: MessageStatusUpdater
@Inject
lateinit var groupMessageStatusUpdater: GroupMsgStatusUpdater
@GroupCollection
@Inject
lateinit var groupCollection: CollectionReference
private var sentTime: Long? = null
private lateinit var pushMsg: PushMsg
private var userId: String? = null
private lateinit var messagesOfChatUser: List
override fun onCreate() {
super.onCreate()
userId = preference.getUid()
}
override fun onNewToken(token: String) {
preference.updatePushToken(token)
}
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
try {
LogMessage.v("Data Payload: ${remoteMessage.data}")
if (preference.isNotLoggedIn() || !preference.isSameDevice())
return
sentTime = remoteMessage.sentTime
val data = remoteMessage.data
pushMsg = Json.decodeFromString(data["data"].toString())
/* pushMsg.to?.let {
if (it!=userId)
return
}*/
handleNotification()
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun handleNotification() {
when (pushMsg.type) {
TYPE_LOGGED_IN -> {
preference.setLastDevice(false)
val intent = Intent(ACTION_LOGGED_IN_ANOTHER_DEVICE)
sendBroadcast(intent)
}
TYPE_NEW_MESSAGE -> {
handleNewMessage()
}
TYPE_NEW_GROUP -> {
handleNewGroup()
}
TYPE_NEW_GROUP_MESSAGE -> {
handleGroupMsg()
}
else -> {
}
}
}
private fun handleGroupMsg() {
//it would be updated by snapshot listeners when app is alive
if (!MApplication.isAppRunning) {
val message = Json.decodeFromString(pushMsg.message_body.toString())
CoroutineScope(Dispatchers.IO).launch {
dbRepository.insertMessage(message)
val group = dbRepository.getGroupById(message.groupId)
val messages = dbRepository.getChatsOfGroupList(group?.id.toString())
if (group != null) {
group.unRead = messages.filter {
it.from != userId &&
Utils.myIndexOfStatus(userId!!, it) < 3
}.size
dbRepository.insertGroup(group)
withContext(Dispatchers.Main) {
showGroupNotification(this@FirebasePush, dbRepository)
//update delivery status
groupMessageStatusUpdater.updateToDelivery(userId!!, messages, group.id)
}
} else {
val groupQuery = GroupQuery(message.groupId, dbRepository, preference)
groupQuery.getGroupData(groupCollection)
}
}
}
}
private fun handleNewGroup() {
//it would be updated by snapshot listeners when app is alive
if (!MApplication.isAppRunning) {
val group = Json.decodeFromString(pushMsg.message_body.toString())
val groupQuery = GroupQuery(group.id, dbRepository, preference)
groupQuery.getGroupData(groupCollection)
}
}
private fun handleNewMessage() {
val message = Json.decodeFromString(pushMsg.message_body.toString())
if (message.to != userId || MApplication.isAppRunning) {
Timber.v("Push notification ignored")
return
}
val chatUserId = UserUtils.getChatUserId(userId!!, message) //chatUserId from message
message.chatUserId = chatUserId
CoroutineScope(Dispatchers.IO).launch {
dbRepository.insertMessage(message)
val chatUser = dbRepository.getChatUserById(chatUserId)
messagesOfChatUser = dbRepository.getChatsOfFriend(chatUserId)
.filter { it.to == userId && it.status < 3 }
if (chatUser != null) {
chatUser.unRead = messagesOfChatUser.size //set unread msg count
dbRepository.insertUser(chatUser)
withContext(Dispatchers.Main) {
showNotification(this@FirebasePush, dbRepository)
//update delivery status
messageStatusUpdater.updateToDelivery(messagesOfChatUser, chatUser)
}
} else {
withContext(Dispatchers.Main) {
//update delivery status in listener
val util = ChatUserUtil(dbRepository, usersCollection, this@FirebasePush)
util.queryNewUserProfile(
this@FirebasePush,
chatUserId,
null,
showNotification = true
)
}
}
}
}
private suspend fun getBitmap(url: String): Bitmap {
val loader = ImageLoader(this)
val request = ImageRequest.Builder(this)
.data(url)
.build()
val result = (loader.execute(request) as SuccessResult).drawable
return (result as BitmapDrawable).bitmap
}
companion object {
//notification method for common use
var messageCount = 0
var personCount = 0
fun showGroupNotification(context: Context, dbRepository: DbRepository) {
CoroutineScope(Dispatchers.IO).launch {
var groupWithMsgs = dbRepository.getGroupWithMessagesList()
groupWithMsgs = groupWithMsgs.filter { it.group.unRead != 0 }
checkGroupMessages(context, groupWithMsgs)
}
}
fun showNotification(context: Context, dbRepository: DbRepository) {
CoroutineScope(Dispatchers.IO).launch {
var chatUserWithMessages = dbRepository.getChatUserWithMessagesList()
chatUserWithMessages = chatUserWithMessages.filter { it.user.unRead != 0 }
checkMessages(context, chatUserWithMessages)
}
}
private fun checkGroupMessages(context: Context, groupWithMsgs: List) {
messageCount = 0
personCount = 0
val myUserId = MPreference(context).getUid().toString()
val manager: NotificationManagerCompat = Utils.returnNManager(context)
val groupNotifications = ArrayList()
if (!groupWithMsgs.isNullOrEmpty()) {
for (groupMsg in groupWithMsgs) {
/* if (groupMsg.messages.last().from==myUserId)
continue*/
personCount += 1
val person: Person = Person.Builder().setIcon(null)
.setKey(groupMsg.group.id).setName(Utils.getGroupName(groupMsg.group.id))
.build()
val builder = Utils.createBuilder(context, manager)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setStyle(
NotificationUtils.getGroupStyle(
context,
myUserId,
person,
groupMsg
)
)
.setContentIntent(
NotificationUtils.getGroupMsgIntent(
context,
groupMsg.group
)
)
.setGroup(GROUP_KEY)
builder.addAction(
R.drawable.ic_drafts,
"mark as read",
NotificationUtils.getGroupMarkAsPIntent(context, groupMsg)
)
builder.addAction(NotificationUtils.getGroupReplyAction(context, groupMsg))
val notification = builder.build()
groupNotifications.add(notification)
}
}
val summaryNotification = NotificationUtils.getSummaryNotification(context, manager)
for ((index, notification) in groupNotifications.withIndex()) {
val notIdString = groupWithMsgs[index].group.createdAt.toString()
val notId = notIdString.substring(notIdString.length - 4)
.toInt() //last 4 digits as notificationId
manager.notify(notId, notification)
}
if (groupNotifications.size > 1)
manager.notify(SUMMARY_ID, summaryNotification)
}
private fun checkMessages(
context: Context,
chatUserWithMessages: List
) {
if (chatUserWithMessages.isNullOrEmpty())
return
messageCount = 0
personCount = 0
val notifications = ArrayList()
val myUserId = MPreference(context).getUid().toString()
val manager: NotificationManagerCompat = Utils.returnNManager(context)
for (user in chatUserWithMessages) {
val messages = user.messages.filter { it.status < 3 && it.from != myUserId }
if (messages.isNullOrEmpty())
continue
personCount += 1
Timber.v("DocId ${user.user.documentId}")
val person: Person = Person.Builder().setIcon(null)
.setKey(user.user.id).setName(user.user.localName).build()
val builder = Utils.createBuilder(context, manager)
.setStyle(NotificationUtils.getStyle(context, person, user))
.setContentIntent(NotificationUtils.getPIntent(context, user.user))
.setGroup(GROUP_KEY)
if (!user.user.documentId.isNullOrBlank()) {
builder.addAction(
R.drawable.ic_drafts,
"mark as read",
NotificationUtils.getMarkAsPIntent(context, user)
)
builder.addAction(NotificationUtils.getReplyAction(context, user))
}
val notification = builder.build()
notifications.add(notification)
}
val summaryNotification = NotificationUtils.getSummaryNotification(context, manager)
for ((index, notification) in notifications.withIndex()) {
val notIdString = chatUserWithMessages[index].user.user.createdAt.toString()
val notId = notIdString.substring(notIdString.length - 4)
.toInt() //last 4 digits as notificationId
manager.notify(notId, notification)
}
if (notifications.size > 1)
manager.notify(SUMMARY_ID, summaryNotification)
}
}
override fun onResult(success: Boolean, data: Any?) {
if (success) {
messageStatusUpdater.updateToDelivery(messagesOfChatUser, data as ChatUser)
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/MApplication.kt
================================================
package com.gowtham.letschat
import android.content.Context
import androidx.hilt.work.HiltWorkerFactory
import androidx.lifecycle.LifecycleObserver
import androidx.multidex.MultiDexApplication
import androidx.work.Configuration
import com.google.firebase.FirebaseApp
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
import javax.inject.Inject
@HiltAndroidApp
class MApplication : MultiDexApplication(), LifecycleObserver,Configuration.Provider {
@Inject
lateinit var preference: MPreference
@Inject
lateinit var userDao: ChatUserDao
@Inject
lateinit var messageDao: MessageDao
@Inject
lateinit var userCollection: CollectionReference
@Inject lateinit var workerFactory: HiltWorkerFactory
companion object {
lateinit var instance: MApplication
private set
var isAppRunning = false
lateinit var appContext: Context
lateinit var userDaoo: ChatUserDao
lateinit var messageDaoo: MessageDao
}
override fun onCreate() {
super.onCreate()
instance = this
appContext = this
userDaoo = userDao
messageDaoo=messageDao
FirebaseApp.initializeApp(this)
initTimber()
if (preference.isLoggedIn())
checkLastDevice() //looking for does user is logged in another device.if yes,need to shoe dialog for log in again
}
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
private fun initTimber() {
if (BuildConfig.DEBUG) {
Timber.plant(object : Timber.DebugTree() {
override fun createStackElementTag(element: StackTraceElement): String {
return "LetsChat/${element.fileName}:${element.lineNumber})#${element.methodName}"
}
})
}
}
private fun checkLastDevice() {
userCollection.document(preference.getUid()!!).get().addOnSuccessListener { data ->
Timber.v("Device Checked")
val appUser = data.toObject(UserProfile::class.java)
checkDeviceDetails(appUser)
}.addOnFailureListener { e ->
LogMessage.v(e.message.toString())
}
}
private fun checkDeviceDetails(appUser: UserProfile?) {
val device = appUser?.deviceDetails
val localDevice = UserUtils.getDeviceId(this)
if (device != null) {
val sameDevice = device.device_id.equals(localDevice)
preference.setLastDevice(sameDevice)
Timber.v("Device Checked ${device.device_id.equals(localDevice)}")
if (sameDevice)
UserUtils.updatePushToken(this,userCollection, true)
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/ChatHandler.kt
================================================
package com.gowtham.letschat.core
import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.google.firebase.firestore.*
import com.gowtham.letschat.FirebasePush
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.fragments.single_chat.toDataClass
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.getUnreadCount
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ChatHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val dbRepository: DbRepository,
private val usersCollection: CollectionReference,
private val preference: MPreference,
private val messageStatusUpdater: MessageStatusUpdater
) {
private val messagesList: MutableList by lazy { mutableListOf() }
private var fromUser = preference.getUid()
val message = MutableLiveData()
private lateinit var chatUsers: List
private val listOfDocs = ArrayList()
private lateinit var messageCollectionGroup: Query
private val chatUserUtil = ChatUserUtil(dbRepository, usersCollection, null)
private var isFirstQuery = false
companion object {
private var listenerDoc1: ListenerRegistration? = null
private var instanceCreated = false
fun removeListeners() {
instanceCreated = false
listenerDoc1?.remove()
}
}
fun initHandler() {
if (instanceCreated)
return
instanceCreated = true
fromUser = preference.getUid()
Timber.v("ChatHandler init")
messageCollectionGroup = UserUtils.getMessageSubCollectionRef()
preference.clearCurrentUser()
listenerDoc1 = messageCollectionGroup.whereArrayContains("chatUsers", fromUser!!)
.addSnapshotListener { snapShots, error ->
if (error != null || snapShots == null || snapShots.metadata.isFromCache) {
LogMessage.v("Error ${snapShots?.metadata?.isFromCache}")
if(snapShots?.metadata?.isFromCache == true)
onFetchDocuments()
return@addSnapshotListener
}else
onSnapShotChanged(snapShots)
}
}
private fun onFetchDocuments() {
messageCollectionGroup.whereArrayContains("chatUsers", fromUser!!).get().addOnSuccessListener {
isFirstQuery=true
onSnapShotChanged(it)
}
}
private fun onSnapShotChanged(snapShots: QuerySnapshot) {
messagesList.clear()
listOfDocs.clear()
val listOfIds = ArrayList()
if (isFirstQuery) {
snapShots.forEach { doc ->
val parentDoc = doc.reference.parent.parent?.id!!
val message = doc.data.toDataClass()
message.chatUserId =
if (message.from != fromUser) message.from else message.to
messagesList.add(message)
if (!listOfDocs.contains(parentDoc)) {
listOfDocs.add(doc.reference.parent.parent?.id.toString())
listOfIds.add(message.chatUserId!!)
}
}
isFirstQuery=false
} else
for (shot in snapShots.documentChanges) {
if (shot.type == DocumentChange.Type.ADDED ||
shot.type == DocumentChange.Type.MODIFIED
) {
val document = shot.document
val parentDoc = document.reference.parent.parent?.id!!
val message = document.data.toDataClass()
message.chatUserId =
if (message.from != fromUser) message.from else message.to
messagesList.add(message)
if (!listOfDocs.contains(parentDoc)) {
listOfDocs.add(document.reference.parent.parent?.id.toString())
listOfIds.add(message.chatUserId!!)
}
}
}
if (!messagesList.isNullOrEmpty())
insertMessageOnDb(listOfIds)
}
private fun insertMessageOnDb(listOfIds: ArrayList) {
CoroutineScope(Dispatchers.IO).launch {
val contacts = ArrayList()
val newContactIds =
ArrayList() //message from new user not saved in localdb yet
chatUsers = dbRepository.getChatUserList()
dbRepository.insertMultipleMessage(messagesList)
for ((index, doc) in listOfDocs.withIndex()) {
val chatUser = chatUsers.firstOrNull { it.id == listOfIds[index] }
if (chatUser == null) {
newContactIds.add(listOfIds[index])
//message from unsaved user
} else {
chatUser.unRead = if (preference.getOnlineUser() == chatUser.id) 0 else
dbRepository.getChatsOfFriend(chatUser.id).getUnreadCount(chatUser.id)
chatUser.documentId = doc
Timber.v("UserId ${chatUser.id} count ${chatUser.unRead}")
contacts.add(chatUser)
}
}
dbRepository.insertMultipleUsers(contacts)
val currentChatUser = if (preference.getOnlineUser().isNotEmpty())
contacts.firstOrNull { it.id == preference.getOnlineUser() }
else null
val allUnReadMsgs = dbRepository.getAllNonSeenMessage()
withContext(Dispatchers.Main) {
updateMsgStatus(newContactIds, currentChatUser, allUnReadMsgs)
}
}
}
private fun updateMsgStatus(
newContactIds: ArrayList,
currentChatUser: ChatUser?,
allUnReadMsgs: List
) {
showNotification(newContactIds)
if (currentChatUser != null) {
val currentUserMsgs = allUnReadMsgs.filter {
it.chatUserId == currentChatUser.id
}
val otherUserMsgs = allUnReadMsgs.filter {
it.chatUserId != currentChatUser.id
}
messageStatusUpdater.updateToDelivery(otherUserMsgs, *chatUsers.toTypedArray())
messageStatusUpdater.updateToSeen(
currentChatUser.id, currentChatUser.documentId!!, currentUserMsgs
)
} else {
messageStatusUpdater.updateToDelivery(allUnReadMsgs, *chatUsers.toTypedArray())
}
}
private fun showNotification(
newContactIds: ArrayList
) {
if (newContactIds.isEmpty()) {
val lastMsgId = messagesList.maxOf { it.createdAt }
val msg = messagesList.find { it.createdAt == lastMsgId }
if (msg != null && msg.from != fromUser)
FirebasePush.showNotification(context, dbRepository)
} else {
//unsaved new user
for (i in 0 until newContactIds.size) {
val userId = newContactIds[i]
if (userId == preference.getOnlineUser())
continue
val unreadCount = messagesList.getUnreadCount(userId)
chatUserUtil.queryNewUserProfile(
context,
userId,
listOfDocs.firstOrNull { it.contains(userId) }, unreadCount,
showNotification = i == newContactIds.lastIndex
)
}
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/ChatUserProfileListener.kt
================================================
package com.gowtham.letschat.core
import android.content.Context
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.ListenerRegistration
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.Utils
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ChatUserProfileListener @Inject
constructor(@ApplicationContext val context: Context,
private val userCollectionRef: CollectionReference,
private val preference: MPreference,
private val dbRepository: DbRepository){
private var instanceCreated=false
companion object{
private val listOfListeners=ArrayList()
fun removeListener(){
listOfListeners.forEach {
it.remove()
}
}
}
private fun getChatUsers() {
CoroutineScope(Dispatchers.IO).launch {
val users=dbRepository.getChatUserList()
withContext(Dispatchers.Main){
addSnapShotListener(users)
}
}
}
private fun addSnapShotListener(users: List) {
val myUserId=preference.getUid().toString()
for (user in users){
if (user.id==myUserId)
continue
val listener= userCollectionRef.document(user.id).addSnapshotListener { profile, error ->
if (error!=null) {
Timber.v(error)
return@addSnapshotListener
}
val userProfile = profile?.toObject(UserProfile::class.java)
userProfile?.let { pro->
val chatUser=users.firstOrNull { it.id== pro.uId }
if (chatUser!=null){
chatUser.user=pro
checkForContactSaved(chatUser,pro.mobile?.number!!)
updateInLocal(chatUser)
}
}
}
listOfListeners.add(listener)
}
}
private fun updateInLocal(chatUser: ChatUser) {
val chatUserId=chatUser.id
dbRepository.insertUser(chatUser)
//updating in groups
CoroutineScope(Dispatchers.IO).launch {
val groups=dbRepository.getGroupList()
val containingList= mutableListOf()
for (group in groups){
val members=group.members
val isContains= members?.any { it.id == chatUserId } ?: false
if (isContains){
val index=members?.indexOfFirst { it.id==chatUserId }
members!![index!!]=chatUser
containingList.add(group)
}
}
dbRepository.insertMultipleGroup(containingList)
}
}
private fun checkForContactSaved(chatUser: ChatUser, mobileNo: String) {
if (Utils.isContactPermissionOk(context)) {
val contacts = UserUtils.fetchContacts(context)
val savedContact=contacts.firstOrNull { it.mobile.contains(mobileNo) }
if (savedContact!=null){
chatUser.localName=savedContact.name
chatUser.locallySaved=true
}else{
//contact deleted
val profile=chatUser.user
val mobile = profile.mobile?.country + " " + profile.mobile?.number
chatUser.localName=mobile
chatUser.locallySaved=false
}
}
}
fun initListener() {
if (!instanceCreated) {
getChatUsers()
instanceCreated=true
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/ChatUserUtil.kt
================================================
package com.gowtham.letschat.core
import android.content.Context
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.FirebasePush
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.OnSuccessListener
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.Utils
class ChatUserUtil(private val dbRepository: DbRepository,
private val usersCollection: CollectionReference,
private val listener: OnSuccessListener?) {
fun queryNewUserProfile(context: Context,chatUserId: String,docId: String?, unReadCount: Int=1,
showNotification: Boolean=false) {
try {
usersCollection.document(chatUserId)
.get().addOnSuccessListener { profile ->
if (profile.exists()) {
val userProfile = profile.toObject(UserProfile::class.java)
val mobile = userProfile?.mobile?.country + " " + userProfile?.mobile?.number
val chatUser = ChatUser(userProfile?.uId!!, mobile, userProfile)
chatUser.unRead=unReadCount
if(docId!=null)
chatUser.documentId=docId
if (Utils.isContactPermissionOk(context)) {
val contacts = UserUtils.fetchContacts(context)
val savedContact=contacts.firstOrNull { it.mobile.contains(userProfile.mobile!!.number) }
savedContact?.let {
chatUser.localName=it.name
chatUser.locallySaved=true
}
}
listener?.onResult(true,chatUser)
dbRepository.insertUser(chatUser)
if(showNotification)
FirebasePush.showNotification(context,dbRepository)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/ContactsQuery.kt
================================================
package com.gowtham.letschat.core
import com.google.firebase.firestore.FirebaseFirestore
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.UserUtils
import timber.log.Timber
interface QueryCompleteListener{
fun onQueryCompleted(queriedList: ArrayList)
}
class ContactsQuery(val list: ArrayList,val position: Int,val listener: QueryCompleteListener){
private val usersCollection = FirebaseFirestore.getInstance().collection("Users")
fun makeQuery() {
try {
usersCollection.whereIn("mobile.number", list).get()
.addOnSuccessListener { documents ->
for (document in documents) {
val contact = document.toObject(UserProfile::class.java)
UserUtils.queriedList.add(contact)
}
UserUtils.resultCount += 1
if(UserUtils.resultCount == UserUtils.totalRecursionCount){
listener.onQueryCompleted(UserUtils.queriedList)
}
}
.addOnFailureListener { exception ->
Timber.wtf("Error getting documents: ${exception.message}")
UserUtils.resultCount += 1
if(UserUtils.resultCount == UserUtils.totalRecursionCount)
listener.onQueryCompleted(UserUtils.queriedList)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/GroupChatHandler.kt
================================================
package com.gowtham.letschat.core
import android.content.Context
import com.google.firebase.firestore.*
import com.gowtham.letschat.FirebasePush
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.di.GroupCollection
import com.gowtham.letschat.fragments.single_chat.toDataClass
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.Utils
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GroupChatHandler @Inject constructor(
@ApplicationContext private val context: Context,
private val preference: MPreference,
private val userCollection: CollectionReference,
@GroupCollection
private val groupCollection: CollectionReference,
private val dbRepository: DbRepository,
private val groupMsgStatusUpdater: GroupMsgStatusUpdater
) {
private var userId = preference.getUid()
private lateinit var messageCollectionGroup: Query
private val messagesList = mutableListOf()
private val listOfGroup = ArrayList()
private var isFirstQuery = false
companion object {
private var groupListener: ListenerRegistration? = null
private var myProfileListener: ListenerRegistration? = null
private var instanceCreated = false
fun removeListener() {
instanceCreated = false
groupListener?.remove()
myProfileListener?.remove()
}
}
fun initHandler() {
if (instanceCreated)
return
else
instanceCreated = true
userId = preference.getUid()
Timber.v("GroupChatHandler init")
preference.clearCurrentGroup()
messageCollectionGroup = UserUtils.getGroupMsgSubCollectionRef()
addGroupsSnapShotListener()
addGroupMsgListener()
}
private fun addGroupMsgListener() {
try {
groupListener = messageCollectionGroup.whereArrayContains("to", userId!!)
.addSnapshotListener { snapshots, error ->
if (error != null || snapshots == null || snapshots.metadata.isFromCache) {
LogMessage.v("Error ${error?.localizedMessage}")
return@addSnapshotListener
}
messagesList.clear()
listOfGroup.clear()
onSnapShotChanged(snapshots)
if (messagesList.isNotEmpty())
updateGroupUnReadCount()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun onSnapShotChanged(snapshots: QuerySnapshot) {
if(isFirstQuery){
snapshots.forEach { doc->
val message = doc.data.toDataClass()
if (!listOfGroup.contains(message.groupId))
listOfGroup.add(message.groupId)
messagesList.add(message)
}
isFirstQuery=false
}
else
for (shot in snapshots.documentChanges) {
if (shot.type == DocumentChange.Type.ADDED ||
shot.type == DocumentChange.Type.MODIFIED
) {
val message = shot.document.data.toDataClass()
if (!listOfGroup.contains(message.groupId))
listOfGroup.add(message.groupId)
messagesList.add(message)
}
}
}
private fun updateGroupUnReadCount() {
CoroutineScope(Dispatchers.IO).launch {
dbRepository.insertMultipleGroupMessage(messagesList)
val groupsWithMsgs = dbRepository.getGroupWithMessagesList()
messagesList.clear()
for (groupWithMsg in groupsWithMsgs) {
val unreadCount = groupWithMsg.messages.filter {
val myStatus = Utils.myMsgStatus(userId.toString(), it)
it.from != userId &&
it.groupId == groupWithMsg.group.id && myStatus < 3
}.size
groupWithMsg.group.unRead =
if (preference.getOnlineGroup() == groupWithMsg.group.id) 0
else unreadCount
messagesList.addAll(groupWithMsg.messages)
}
val groups = groupsWithMsgs.map {
it.group
}
dbRepository.insertMultipleGroup(groups)
changeMsgStatus(groups)
}
}
private fun changeMsgStatus(groups: List) {
if (groups.isNotEmpty())
FirebasePush.showGroupNotification(context, dbRepository)
val currentOnlineGroupId=preference.getOnlineGroup()
if(currentOnlineGroupId.isNotEmpty()){
val currentGroupMsgs = messagesList.filter {
it.groupId == currentOnlineGroupId
}
val otherGroupMsgs = messagesList.filter {
it.groupId != currentOnlineGroupId
}
groupMsgStatusUpdater.updateToSeen(userId!!, currentGroupMsgs,currentOnlineGroupId)
groupMsgStatusUpdater.updateToDelivery(userId!!, otherGroupMsgs, *listOfGroup.toTypedArray())
}else
groupMsgStatusUpdater.updateToDelivery(userId!!, messagesList, *listOfGroup.toTypedArray())
}
private fun addGroupsSnapShotListener() {
myProfileListener =
userCollection.document(userId.toString()).addSnapshotListener { snapshot, error ->
if (error == null) {
val groups = snapshot?.get("groups")
val listOfGroup =
if (groups == null) ArrayList() else groups as ArrayList
CoroutineScope(Dispatchers.IO).launch {
val alreadySavedGroup = dbRepository.getGroupList().map { it.id }
val removedGroups = alreadySavedGroup.toSet().minus(listOfGroup.toSet())
val newGroups = listOfGroup.toSet().minus(alreadySavedGroup.toSet())
queryNewGroups(newGroups)
}
}
}
}
private fun queryNewGroups(newGroups: Set) {
Timber.v("New groups ${newGroups.size}")
for (groupId in newGroups) {
val groupQuery = GroupQuery(groupId, dbRepository, preference)
groupQuery.getGroupData(groupCollection)
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/GroupMsgSender.kt
================================================
package com.gowtham.letschat.core
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.SetOptions
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.db.data.Message
interface OnGrpMessageResponse{
fun onSuccess(message: GroupMessage)
fun onFailed(message: GroupMessage)
}
class GroupMsgSender(private val groupCollection: CollectionReference) {
fun sendMessage(message: GroupMessage,group: Group,listener: OnGrpMessageResponse){
message.status[0]=1
groupCollection.document(group.id).collection("group_messages")
.document(message.createdAt.toString()).set(message, SetOptions.merge())
.addOnSuccessListener {
listener.onSuccess(message)
}.addOnFailureListener {
message.status[0]=4
listener.onFailed(message)
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/GroupMsgStatusUpdater.kt
================================================
package com.gowtham.letschat.core
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.di.GroupCollection
import com.gowtham.letschat.fragments.single_chat.asMap
import com.gowtham.letschat.fragments.single_chat.serializeToMap
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.Utils.myIndexOfStatus
import com.gowtham.letschat.utils.Utils.myMsgStatus
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class GroupMsgStatusUpdater @Inject constructor(
@GroupCollection
private val groupCollection: CollectionReference,
private val firestore: FirebaseFirestore) {
fun updateToDelivery(myUserId: String, messageList: List, vararg groupId: String){
try {
val batch= firestore.batch()
for (id in groupId){
val msgSubCollection=groupCollection.document(id).collection("group_messages")
val filterList= messageList
.filter { it.from!=myUserId && myMsgStatus(myUserId,it)==0 && it.groupId==id }
.map {
val myIndex=myIndexOfStatus(myUserId,it)
it.status[myIndex]=2
it.deliveryTime[myIndex]=System.currentTimeMillis()
it
}
for (msg in filterList){
LogMessage.v("message date ${msg.deliveryTime}")
batch.update(msgSubCollection
.document(msg.createdAt.toString()),msg.asMap())
}
}
batch.commit().addOnSuccessListener {
LogMessage.v("Batch update success from group")
}.addOnFailureListener {
LogMessage.v("Batch update failure ${it.message} from group")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun updateToSeen(myUserId: String, messageList: List, groupId: String){
val batch= firestore.batch()
val currentTime = System.currentTimeMillis()
val msgSubCollection=groupCollection.document(groupId).collection("group_messages")
val filterList= messageList
.filter { it.from!=myUserId && myMsgStatus(myUserId,it)<3 }
.map {
val myIndex=myIndexOfStatus(myUserId,it)
it.status[myIndex]=3
it.deliveryTime[myIndex]= if (it.deliveryTime[myIndex]==0L)
currentTime else it.deliveryTime[myIndex]
it.seenTime[myIndex]=currentTime
it
}
if (filterList.isNotEmpty()){
for (msg in filterList){
batch.update(msgSubCollection
.document(msg.createdAt.toString()),msg.serializeToMap())
}
}
batch.commit().addOnSuccessListener {
LogMessage.v("Seen Batch update success from group")
}.addOnFailureListener {
LogMessage.v("Seen Batch update failure ${it.message} from group")
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/GroupQuery.kt
================================================
package com.gowtham.letschat.core
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.utils.MPreference
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
class GroupQuery(private val groupId: String,private val dbRepository: DbRepository,private val preference: MPreference) {
private val myUserId=preference.getUid()!!
fun getGroupData(groupCollection: CollectionReference){
val userId=preference.getUid()
groupCollection.document(groupId).get().addOnSuccessListener { snapshot->
snapshot?.let { data ->
if (!data.exists())
return@addOnSuccessListener
val group=data.toObject(Group::class.java)
val profiles=group?.profiles
val index=profiles!!.indexOfFirst { it.uId==userId }
profiles.removeAt(index)
profiles.add(0,preference.getUserProfile()!!) //moving localuser to 0 th index
group.profiles=profiles
CoroutineScope(Dispatchers.IO).launch {
checkAlreadySavedMember(group, dbRepository.getChatUserList())
}
}
}.addOnFailureListener {
Timber.v("GroupDataGrtting failed ${it.message}")
}
}
private fun checkAlreadySavedMember(group: Group, list: List){
val chatUsers= ArrayList()
for (profile in group.profiles!!){
if (profile.uId==myUserId) {
chatUsers.add(ChatUser(myUserId, "You", profile))
continue
}
val chatUser=list.firstOrNull { it.id==profile.uId }
if (chatUser==null){
val localName="${profile.mobile?.country} ${profile.mobile?.number}"
val user=ChatUser(profile.uId.toString(),localName,profile)
chatUsers.add(user)
}else
chatUsers.add(chatUser)
}
group.members=chatUsers
group.profiles= ArrayList()
dbRepository.insertMultipleUser(chatUsers)
dbRepository.insertGroup(group)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/MessageSender.kt
================================================
package com.gowtham.letschat.core
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.SetOptions
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.UserUtils
import timber.log.Timber
interface OnMessageResponse{
fun onSuccess(message: Message)
fun onFailed(message: Message)
}
class MessageSender(private val msgCollection: CollectionReference,
private val dbRepo: DbRepository, private val chatUser: ChatUser,
private val listener: OnMessageResponse) {
fun checkAndSend(fromUser: String, toUser: String, message: Message) {
val docId = chatUser.documentId
if (!docId.isNullOrEmpty()){
Timber.v("Case 0 ${chatUser.documentId}")
send(docId, message)
} else {
//so we don't create multiple nodes for same chat
msgCollection.document("${fromUser}_${toUser}").get()
.addOnSuccessListener { documentSnapshot ->
if (documentSnapshot.exists()) {
//this node exists send your message
Timber.v("Case 1")
send("${fromUser}_${toUser}", message)
} else {
//senderId_receiverId node doesn't exist check receiverId_senderId
msgCollection.document("${toUser}_${fromUser}").get()
.addOnSuccessListener { documentSnapshot2 ->
if (documentSnapshot2.exists()) {
Timber.v("Case 2")
send("${toUser}_${fromUser}", message)
} else {
//no previous chat history(senderId_receiverId & receiverId_senderId both don't exist)
//so we create document senderId_receiverId then messages array then add messageMap to messages
//this node exists send your message
//add ids of chat members
Timber.v("Case 3")
msgCollection.document("${fromUser}_${toUser}")
.set(mapOf("chat_members" to FieldValue.arrayUnion(fromUser, toUser)),
SetOptions.merge()
).addOnSuccessListener {
LogMessage.v("chat member update successfully")
send("${fromUser}_${toUser}", message)
}.addOnFailureListener {
LogMessage.v("chat member update failed ${it.message}")
}
}
}
}
}
}
}
private fun send(doc: String, message: Message){
try {
chatUser.documentId=doc
dbRepo.insertUser(chatUser)
val chatUserId=message.chatUserId
message.chatUserId=null //chatUserId field is being used only for relation query,changing to null will ignore this field
message.status=1
message.chatUsers= arrayListOf(message.from,message.to)
msgCollection.document(doc).collection("messages").document(message.createdAt.toString()).set(
message,
SetOptions.merge()
).addOnSuccessListener {
LogMessage.v("Message sender Sucesss ${message.createdAt}")
message.chatUserId=chatUserId
listener.onSuccess(message)
}.addOnFailureListener {
message.chatUserId=chatUserId
message.status=4
LogMessage.v("Message sender Failed ${it.message}")
listener.onFailed(message)
}
/* msgCollection.document(doc)
.update("messages",
FieldValue.arrayUnion(message.serializeToMap())).addOnSuccessListener {
LogMessage.v("Message sender Sucesss ${message.textMessage?.text}")
listener.onSuccess(message)
}.addOnFailureListener {
message.status=4
LogMessage.v("Message sender Failed ${it.message}")
listener.onFailed(message)
}*/
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/core/MessageStatusUpdater.kt
================================================
package com.gowtham.letschat.core
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.di.MessageCollection
import com.gowtham.letschat.fragments.single_chat.asMap
import com.gowtham.letschat.fragments.single_chat.serializeToMap
import com.gowtham.letschat.utils.LogMessage
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MessageStatusUpdater @Inject constructor(
@MessageCollection
private val msgCollection: CollectionReference,
private val firebaseFirestore: FirebaseFirestore
) {
fun updateToDelivery(messageList: List, vararg chatUsers: ChatUser) {
val batch = firebaseFirestore.batch()
for (chatUser in chatUsers) {
if (chatUser.documentId.isNullOrBlank())
continue
val msgSubCollection =
msgCollection.document(chatUser.documentId!!).collection("messages")
val filterList = messageList
.filter { msg -> msg.status == 1 && msg.from == chatUser.id }
.map {
it.chatUserId = null
it.status = 2
it.deliveryTime = System.currentTimeMillis()
it
}
if (filterList.isNotEmpty()) {
for (msg in filterList) {
batch.update(
msgSubCollection
.document(msg.createdAt.toString()), msg.serializeToMap()
)
}
}
}
batch.commit().addOnSuccessListener {
LogMessage.v("Batch update success from home")
}.addOnFailureListener {
LogMessage.v("Batch update failure ${it.message} from home")
}
}
fun updateToSeen(toUser: String, docId: String?, messageList: List) {
if(docId==null)
return
val msgSubCollection = msgCollection.document(docId).collection("messages")
val batch = firebaseFirestore.batch()
val currentTime = System.currentTimeMillis()
val filterList = messageList
.filter { msg -> msg.from == toUser && msg.status != 3 }
.map {
it.status = 3
it.chatUserId = null
it.deliveryTime = it.deliveryTime
it.seenTime = currentTime
it
}
if (filterList.isNotEmpty()) {
Timber.v("Size of list ${filterList.last().createdAt}")
for (message in filterList) {
batch.update(
msgSubCollection
.document(message.createdAt.toString()), message.serializeToMap()
)
}
batch.commit().addOnSuccessListener {
LogMessage.v("All Message Seen Batch update success")
}.addOnFailureListener {
LogMessage.v("All Message Seen Batch update failure ${it.message}")
}
} else
LogMessage.v("All message already seen")
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/ChatUserDatabase.kt
================================================
package com.gowtham.letschat.db
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.daos.GroupMessageDao
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.db.data.Message
@Database(entities = [ChatUser::class, Message::class,Group::class,GroupMessage::class],
version = 1, exportSchema = false)
@TypeConverters(TypeConverter::class)
abstract class ChatUserDatabase : RoomDatabase() {
abstract fun getChatUserDao(): ChatUserDao
abstract fun getMessageDao(): MessageDao
abstract fun getGroupDao(): GroupDao
abstract fun getGroupMessageDao(): GroupMessageDao
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/DbRepository.kt
================================================
package com.gowtham.letschat.db
import android.content.Context
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.daos.GroupMessageDao
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.MPreference
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DbRepository @Inject constructor(
private val userDao: ChatUserDao,
private val groupDao: GroupDao,
private val groupMsgDao: GroupMessageDao,
private val messageDao: MessageDao) : DefaultDbRepo {
override fun insertUser(user: ChatUser) {
CoroutineScope(Dispatchers.IO).launch {
userDao.insertUser(user)
}
}
override fun insertMultipleUser(users: List) {
CoroutineScope(Dispatchers.IO).launch {
userDao.insertMultipleUser(users)
}
}
override fun getChatUserWithMessages() = userDao.getChatUserWithMessages()
override fun getChatUserList() = userDao.getChatUserList()
override fun getChatUserWithMessagesList() = userDao.getChatUserWithMessagesList()
override fun getChatUserById(id: String) = userDao.getChatUserById(id)
override fun getAllChatUser() = userDao.getAllChatUser()
override fun nukeTable() {
}
override fun deleteUserById(userId: String) {
}
fun insertMultipleUser(finalList: ArrayList) {
CoroutineScope(Dispatchers.IO).launch {
userDao.insertMultipleUser(finalList)
}
}
suspend fun insertMultipleUsers(users: ArrayList){
userDao.insertMultipleUser(users)
}
fun insertGroup(group: Group) {
CoroutineScope(Dispatchers.IO).launch {
groupDao.insertGroup(group)
}
}
suspend fun insertMultipleMessage(messagesList: MutableList) =
messageDao.insertMultipleMessage(messagesList)
suspend fun insertMultipleGroupMessage(messagesList: List) =
groupMsgDao.insertMultipleMessage(messagesList)
fun getAllNonSeenMessage() =
messageDao.getAllNotSeenMessages()
fun insertMessage(message: Message) {
CoroutineScope(Dispatchers.IO).launch {
messageDao.insertMessage(message)
}
}
fun insertMessage(message: GroupMessage) {
CoroutineScope(Dispatchers.IO).launch {
groupMsgDao.insertMessage(message)
}
}
suspend fun insertMultipleGroup(groups: List) =
groupDao.insertMultipleGroup(groups)
fun getGroupWithMessages() = groupDao.getGroupWithMessages()
fun getMessagesByChatUserId(chatUserId: String) = messageDao.getMessagesByChatUserId(chatUserId)
fun getChatsOfFriend(toUser: String) = messageDao.getChatsOfFriend(toUser)
fun getGroupById(groupId: String) = groupDao.getGroupById(groupId)
fun getChatsOfGroupList(groupId: String) = groupMsgDao.getChatsOfGroupList(groupId)
fun getChatsOfGroup(groupId: String) = groupMsgDao.getChatsOfGroup(groupId)
fun getGroupWithMessagesList() = groupDao.getGroupWithMessagesList()
fun getMessageList() = messageDao.getMessageList()
fun getGroupList() = groupDao.getGroupList()
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/DefaultDbRepo.kt
================================================
package com.gowtham.letschat.db
import androidx.lifecycle.LiveData
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.ChatUserWithMessages
import kotlinx.coroutines.flow.Flow
interface DefaultDbRepo {
fun insertUser(user: ChatUser)
fun insertMultipleUser(users: List)
fun getAllChatUser(): LiveData>
fun getChatUserList(): List
fun getChatUserById(id: String): ChatUser?
fun deleteUserById(userId: String)
fun getChatUserWithMessages(): Flow>
fun getChatUserWithMessagesList(): List
fun nukeTable()
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/TypeConverter.kt
================================================
package com.gowtham.letschat.db
import androidx.room.TypeConverter
import com.gowtham.letschat.db.data.*
import com.gowtham.letschat.models.UserProfile
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
class TypeConverter {
@TypeConverter
fun fromProfileToString(userProfile: UserProfile): String {
return Json.encodeToString(userProfile)
}
@TypeConverter
fun fromStringToProfile(userProfile: String): UserProfile {
return Json.decodeFromString(userProfile)
}
@TypeConverter
fun fromTextMessageToString(textMessage: TextMessage?): String {
return Json.encodeToString(textMessage ?: TextMessage())
}
@TypeConverter
fun fromStringToTextMessage(messageData: String): TextMessage {
return Json.decodeFromString(messageData)
}
@TypeConverter
fun fromImageMessageToString(imageMessage: ImageMessage?): String {
return Json.encodeToString(imageMessage ?: ImageMessage())
}
@TypeConverter
fun fromStringToImageMessage(messageData: String): ImageMessage {
return Json.decodeFromString(messageData)
}
@TypeConverter
fun fromAudioMessageToString(audioMessage: AudioMessage?): String {
return Json.encodeToString(audioMessage ?: AudioMessage())
}
@TypeConverter
fun fromStringToAudioMessage(messageData: String): AudioMessage {
return Json.decodeFromString(messageData)
}
@TypeConverter
fun fromVideoMessageToString(videoMessage: VideoMessage?): String {
return Json.encodeToString(videoMessage ?: VideoMessage())
}
@TypeConverter
fun fromStringToVideoMessage(messageData: String): VideoMessage {
return Json.decodeFromString(messageData)
}
@TypeConverter
fun fromFileMessageToString(fileMessage: FileMessage?): String {
return Json.encodeToString(fileMessage ?: FileMessage())
}
@TypeConverter
fun fromStringToFileMessage(messageData: String): FileMessage {
return Json.decodeFromString(messageData)
}
@TypeConverter
fun fromChatUserToString(chatUser: ChatUser): String {
return Json.encodeToString(chatUser)
}
@TypeConverter
fun fromStringToChatUser(chatUser: String): ChatUser {
return Json.decodeFromString(chatUser)
}
@TypeConverter
fun fromGroupToString(group: Group): String {
return Json.encodeToString(group)
}
@TypeConverter
fun fromStringToGroup(group: String): Group {
return Json.decodeFromString(group)
}
@TypeConverter
fun fromGroupMessageToString(groupMessage: GroupMessage): String {
return Json.encodeToString(groupMessage)
}
@TypeConverter
fun fromStringToGroupMessage(groupMessage: String): GroupMessage {
return Json.decodeFromString(groupMessage)
}
@TypeConverter
fun fromToMembersToString(to: ArrayList): String {
return Json.encodeToString(to)
}
@TypeConverter
fun fromStringToMembers(to: String): ArrayList {
return Json.decodeFromString(to)
}
@TypeConverter
fun fromProfilesToString(profiles: ArrayList): String {
return Json.encodeToString(profiles)
}
@TypeConverter
fun fromStringToProfiles(profilesString: String): ArrayList {
return Json.decodeFromString(profilesString)
}
@TypeConverter
fun fromGroupMembersToString(members: ArrayList): String {
return Json.encodeToString(members)
}
@TypeConverter
fun fromStringToGroupMembers(members: String): ArrayList {
return Json.decodeFromString(members)
}
@TypeConverter
fun fromGroupMsgStatusToString(status: ArrayList): String {
return Json.encodeToString(status)
}
@TypeConverter
fun fromStringToGroupMsgStatus(status: String): ArrayList {
return Json.decodeFromString(status)
}
@TypeConverter
fun fromSeenStatusListToString(status: ArrayList): String {
return Json.encodeToString(status)
}
@TypeConverter
fun fromStringToSeenStatusList(status: String): ArrayList {
return Json.decodeFromString(status)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/daos/ChatUserDao.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.lifecycle.LiveData
import androidx.room.*
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.ChatUserWithMessages
import kotlinx.coroutines.flow.Flow
@Dao
interface ChatUserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: ChatUser)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMultipleUser(users: List)
@Query("SELECT * FROM ChatUser ORDER BY localName ASC")
fun getAllChatUser(): LiveData>
@Query("SELECT * FROM ChatUser ORDER BY localName ASC")
fun getChatUserList(): List
@Query("SELECT * FROM ChatUser WHERE id=:id")
fun getChatUserById(id: String): ChatUser?
@Query("SELECT * FROM ChatUser WHERE id=:id")
suspend fun getChatUserById2(id: String): ChatUser?
@Query("DELETE FROM ChatUser WHERE id=:userId")
suspend fun deleteUserById(userId: String)
@Query("DELETE FROM ChatUser")
fun nukeTable()
@Transaction
@Query("SELECT * FROM ChatUser")
fun getChatUserWithMessages(): Flow>
@Transaction
@Query("SELECT * FROM ChatUser")
fun getChatUserWithMessagesList(): List
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/daos/GroupDao.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.lifecycle.LiveData
import androidx.room.*
import com.gowtham.letschat.db.data.ChatUserWithMessages
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupWithMessages
import kotlinx.coroutines.flow.Flow
@Dao
interface GroupDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertGroup(group: Group)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMultipleGroup(listOfGroup: List)
@Query("SELECT * FROM `Group` ORDER BY id ASC")
fun getAllGroup(): LiveData>
@Query("SELECT * FROM `Group` ORDER BY id ASC")
fun getGroupList(): List
@Query("SELECT * FROM `Group` WHERE id=:groupId")
fun getGroupById(groupId: String): Group?
@Query("DELETE FROM `Group` WHERE id=:groupId")
suspend fun deleteGroupById(groupId: String)
@Query("DELETE FROM `Group`")
fun nukeTable()
@Transaction
@Query("SELECT * FROM `Group`")
fun getGroupWithMessagesList(): List
@Transaction
@Query("SELECT * FROM `Group`")
fun getGroupWithMessages(): Flow>
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/daos/GroupMessageDao.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.db.data.GroupWithMessages
import com.gowtham.letschat.db.data.Message
import kotlinx.coroutines.flow.Flow
@Dao
interface GroupMessageDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMessage(message: GroupMessage)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMultipleMessage(users: List)
@Query("SELECT * FROM GroupMessage")
fun getAllMessages(): LiveData>
@Query("SELECT * FROM GroupMessage")
fun getMessageList(): List
@Query("SELECT * FROM GroupMessage WHERE groupId=:groupId")
fun getChatsOfGroupList(groupId: String): List
@Query("SELECT * FROM GroupMessage WHERE groupId=:groupId")
fun getChatsOfGroup(groupId: String): Flow>
@Query("DELETE FROM GroupMessage WHERE createdAt=:createdAt")
suspend fun deleteMessageByCreatedAt(createdAt: Long)
@Query("DELETE FROM GroupMessage WHERE groupId=:groupId")
suspend fun deleteMessagesByGroupId(groupId: String)
@Query("DELETE FROM GroupMessage")
fun nukeTable()
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/daos/MessageDao.kt
================================================
package com.gowtham.letschat.db.daos
import androidx.lifecycle.LiveData
import androidx.room.*
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.utils.LogMessage
import kotlinx.coroutines.flow.Flow
@Dao
interface MessageDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMessage(message: Message)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMultipleMessage(users: List)
@Query("SELECT * FROM Message")
fun getAllMessages(): Flow>
@Query("SELECT * FROM Message")
fun getMessageList(): List
@Query("SELECT * FROM Message WHERE `chatUserId`=:chatUserId")
fun getChatsOfFriend(chatUserId: String): List
@Query("SELECT * FROM Message WHERE `chatUserId`=:chatUserId")
suspend fun getChatsOfFriend2(chatUserId: String): List
@Query("SELECT * FROM Message WHERE `to`=:chatUserId OR `from`=:chatUserId")
fun getMessagesByChatUserId(chatUserId: String): Flow>
@Query("SELECT * FROM Message WHERE createdAt=:createdAt")
suspend fun getMessageById(createdAt: Long): Message?
@Query("SELECT * FROM Message WHERE status<3")
fun getAllNotSeenMessages() : List
@Query("DELETE FROM Message WHERE createdAt=:createdAt")
suspend fun deleteMessageByCreatedAt(createdAt: Long)
@Query("DELETE FROM Message WHERE `to`=:userId")
suspend fun deleteMessagesByUserId(userId: String)
@Query("DELETE FROM Message")
fun nukeTable()
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/data/ChatUser.kt
================================================
package com.gowtham.letschat.db.data
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.firebase.firestore.IgnoreExtraProperties
import com.gowtham.letschat.models.UserProfile
import kotlinx.serialization.Serializable
@IgnoreExtraProperties
@Serializable
@kotlinx.parcelize.Parcelize
@Entity
data class ChatUser(
@PrimaryKey
var id: String,var localName: String,var user: UserProfile,
var documentId: String?=null,var locallySaved: Boolean=false,
var unRead: Int=0,var isSearchedUser: Boolean=false,var isSelected: Boolean=false): Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/data/ChatUserWithMessages.kt
================================================
package com.gowtham.letschat.db.data
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
@kotlinx.parcelize.Parcelize
class ChatUserWithMessages(
@Embedded
val user: ChatUser,
@Relation(
parentColumn = "id",
entityColumn = "chatUserId"
)
val messages: List) : Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/data/Group.kt
================================================
package com.gowtham.letschat.db.data
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.firebase.firestore.Exclude
import com.google.firebase.firestore.IgnoreExtraProperties
import com.gowtham.letschat.models.UserProfile
import kotlinx.serialization.Serializable
@IgnoreExtraProperties
@Serializable
@kotlinx.parcelize.Parcelize
@Entity
data class Group(@PrimaryKey
var id: String="",var createdBy: String="",
var createdAt: Long=0,
var about: String="", var image: String="",
@set:Exclude @get:Exclude
var members: ArrayList?=null, //only for storing in localdb
var profiles: ArrayList?=null,
@set:Exclude @get:Exclude
var unRead: Int=0): Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/data/GroupMessage.kt
================================================
package com.gowtham.letschat.db.data
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.firebase.firestore.IgnoreExtraProperties
import kotlinx.serialization.Serializable
@IgnoreExtraProperties
@Serializable
@kotlinx.parcelize.Parcelize
@Entity
data class GroupMessage(@PrimaryKey
val createdAt: Long, var groupId: String,
val from: String, val to: ArrayList,
val senderName: String,
val senderImage: String,
val status: ArrayList,//0 th index is status of from user
val deliveryTime: ArrayList,
val seenTime: ArrayList,
var type: String="text",//0=text,1=audio,2=image,3=video,4=file,5=s_image
var textMessage: TextMessage?=null,
var imageMessage: ImageMessage?=null,
var audioMessage: AudioMessage?=null,
var videoMessage: VideoMessage?=null,
var fileMessage: FileMessage?=null): Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/data/GroupWithMessages.kt
================================================
package com.gowtham.letschat.db.data
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
@kotlinx.parcelize.Parcelize
class GroupWithMessages (
@Embedded
val group: Group,
@Relation(
parentColumn = "id",
entityColumn = "groupId"
)
val messages: List) : Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/db/data/Message.kt
================================================
package com.gowtham.letschat.db.data
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.google.firebase.Timestamp
import com.google.firebase.firestore.Exclude
import com.google.firebase.firestore.IgnoreExtraProperties
import com.google.firebase.firestore.ServerTimestamp
import kotlinx.serialization.Serializable
@IgnoreExtraProperties
@Serializable
@kotlinx.parcelize.Parcelize
@Entity
data class Message(
@PrimaryKey
val createdAt: Long, var deliveryTime: Long=0L,
var seenTime: Long=0L,
val from: String, val to: String,
val senderName: String,
val senderImage: String,
var type: String="text",//0=text,1=audio,2=image,3=video,4=file
var status: Int=0,//0=sending,1=sent,2=delivered,3=seen,4=failed
var textMessage: TextMessage?=null,
var imageMessage: ImageMessage?=null,
var audioMessage: AudioMessage?=null,
var videoMessage: VideoMessage?=null,
var fileMessage: FileMessage?=null,
var chatUsers: ArrayList?=null,
@set:Exclude @get:Exclude
var chatUserId: String?=null): Parcelable
@Serializable
@kotlinx.parcelize.Parcelize
data class TextMessage(val text: String?=null): Parcelable
@Serializable
@kotlinx.parcelize.Parcelize
data class AudioMessage(var uri: String?=null,val duration: Int=0): Parcelable
@Serializable
@kotlinx.parcelize.Parcelize
data class ImageMessage(var uri: String?=null,var imageType: String="image"): Parcelable
@Serializable
@kotlinx.parcelize.Parcelize
data class VideoMessage(val uri: String?=null,val duration: Int=0): Parcelable
@Serializable
@kotlinx.parcelize.Parcelize
data class FileMessage(val name: String?=null,
val uri: String?=null,val duration: Int=0): Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/di/AppModule.kt
================================================
package com.gowtham.letschat.di
import android.content.Context
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.gowtham.letschat.core.MessageStatusUpdater
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.DefaultDbRepo
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.daos.GroupMessageDao
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.ui.activities.MainActivity
import com.gowtham.letschat.utils.MPreference
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.internal.managers.ApplicationComponentManager
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Qualifier
import javax.inject.Singleton
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MessageCollection
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GroupCollection
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Singleton
@Provides
fun provideFireStoreInstance(): FirebaseFirestore {
return FirebaseFirestore.getInstance()
}
@Singleton
@Provides
fun provideUsersCollectionRef(firestore: FirebaseFirestore): CollectionReference {
return firestore.collection("Users")
}
@MessageCollection
@Singleton
@Provides
fun provideMessagesCollectionRef(firestore: FirebaseFirestore): CollectionReference {
return firestore.collection("Messages")
}
@GroupCollection
@Singleton
@Provides
fun provideGroupCollectionRef(firestore: FirebaseFirestore): CollectionReference {
return firestore.collection("Groups")
}
@Provides
fun provideMainActivity(): MainActivity {
return MainActivity()
}
@Provides
fun provideDefaultDbRepo(userDao: ChatUserDao,
groupDao: GroupDao,
groupMsgDao: GroupMessageDao,
messageDao: MessageDao): DefaultDbRepo {
return DbRepository(userDao, groupDao, groupMsgDao, messageDao)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/di/DbModule.kt
================================================
package com.gowtham.letschat.di
import android.content.Context
import androidx.room.Room
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.utils.Constants.CHAT_USER_DB_NAME
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DbModule {
@Singleton
@Provides
fun provideChatUserDb(@ApplicationContext context: Context): ChatUserDatabase{
return Room.databaseBuilder(context,ChatUserDatabase::class.java,
CHAT_USER_DB_NAME).build()
}
@Singleton
@Provides
fun provideChatUserDao(db: ChatUserDatabase) = db.getChatUserDao()
@Singleton
@Provides
fun provideMessageDao(db: ChatUserDatabase) = db.getMessageDao()
@Singleton
@Provides
fun provideGroupDao(db: ChatUserDatabase) = db.getGroupDao()
@Singleton
@Provides
fun provideGroupMessageDao(db: ChatUserDatabase) = db.getGroupMessageDao()
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/FAttachment.kt
================================================
package com.gowtham.letschat.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.gowtham.letschat.databinding.FAttachmentBinding
import com.gowtham.letschat.databinding.FImageSrcSheetBinding
import com.gowtham.letschat.utils.BottomSheetEvent
import org.greenrobot.eventbus.EventBus
class FAttachment : BottomSheetDialogFragment() {
private lateinit var binding: FAttachmentBinding
companion object{
fun newInstance(bundle : Bundle) : FAttachment{
val fragment = FAttachment()
fragment.arguments=bundle
return fragment
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = FAttachmentBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.imgCamera.setOnClickListener {
EventBus.getDefault().post(BottomSheetEvent(0))
dismiss()
}
binding.imgGallery.setOnClickListener {
EventBus.getDefault().post(BottomSheetEvent(1))
dismiss()
}
binding.videoGallery.setOnClickListener {
EventBus.getDefault().post(BottomSheetEvent(2))
dismiss()
}
binding.videoCamera.setOnClickListener {
EventBus.getDefault().post(BottomSheetEvent(3))
dismiss()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/FImageSrcSheet.kt
================================================
package com.gowtham.letschat.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.gowtham.letschat.databinding.FImageSrcSheetBinding
interface SheetListener{
fun selectedItem(index: Int)
}
class FImageSrcSheet constructor() : BottomSheetDialogFragment() {
private lateinit var binding: FImageSrcSheetBinding
private lateinit var listener: SheetListener
companion object{
fun newInstance(bundle : Bundle) : FImageSrcSheet{
val fragment = FImageSrcSheet()
fragment.arguments=bundle
return fragment
}
}
fun addListener(listener: SheetListener){
this.listener=listener
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = FImageSrcSheetBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.txtCamera.setOnClickListener {
listener.selectedItem(0)
dismiss()
}
binding.txtGallery.setOnClickListener {
listener.selectedItem(1)
dismiss()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/MainFragmentFactory.kt
================================================
package com.gowtham.letschat.fragments
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentFactory
import com.gowtham.letschat.fragments.contacts.FContacts
import com.gowtham.letschat.utils.MPreference
import javax.inject.Inject
class MainFragmentFactory @Inject constructor(
private val preference: MPreference
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when(className) {
FContacts::class.java.name -> FContacts(preference)
else -> super.instantiate(classLoader, className)
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/add_group_members/AdAddMembers.kt
================================================
package com.gowtham.letschat.fragments.add_group_members
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.databinding.RowAddMemberBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.utils.DiffCallbackChatUser
import com.gowtham.letschat.utils.ItemClickListener
import java.util.*
import kotlin.collections.ArrayList
class AdAddMembers(private val context: Context) :
ListAdapter(DiffCallbackChatUser()) {
companion object {
var allContacts = ArrayList()
lateinit var listener: ItemClickListener
}
fun filter(query: String) {
val list = ArrayList()
if (query.isEmpty()) {
list.addAll(allContacts)
} else {
val queryList = allContacts.filter {
it.localName.toLowerCase(Locale.getDefault())
.contains(query.toLowerCase(Locale.getDefault()))
}
list.addAll(queryList)
}
submitList(list as MutableList)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RowAddMemberBinding.inflate(layoutInflater, parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder as MyViewHolder
holder.bind(getItem(position))
}
override fun submitList(list: List?) {
super.submitList(list?.let { ArrayList(it) })
}
class MyViewHolder(val binding: RowAddMemberBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ChatUser) {
binding.chatUser = item
binding.viewRoot.setOnClickListener {
listener.onItemClicked(it, bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/add_group_members/AdChip.kt
================================================
package com.gowtham.letschat.fragments.add_group_members
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.databinding.RowChipBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.utils.DiffCallbackChatUser
import com.gowtham.letschat.utils.ItemClickListener
import kotlin.collections.ArrayList
class AdChip (private val context: Context) :
ListAdapter(DiffCallbackChatUser()) {
companion object{
var allAddedContacts=ArrayList()
lateinit var listener: ItemClickListener
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding= RowChipBinding.inflate(layoutInflater, parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder as MyViewHolder
holder.bind(getItem(position))
}
class MyViewHolder(val binding: RowChipBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ChatUser) {
binding.chatUser = item
binding.chip.setOnCloseIconClickListener {
listener.onItemClicked(it,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/add_group_members/AddGroupViewModel.kt
================================================
package com.gowtham.letschat.fragments.add_group_members
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.gowtham.letschat.core.QueryCompleteListener
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.LoadState
import com.gowtham.letschat.utils.UserUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class AddGroupViewModel @Inject
constructor(@ApplicationContext context: Context,
private val dbRepository: DbRepository
) : ViewModel() {
private val chipList= MutableLiveData>()
private val allContacts= ArrayList()
val queryState = MutableLiveData()
private lateinit var chatUsers: List
var isFirstCall=true
init {
Timber.v("AddGroupViewModel init")
viewModelScope.launch(Dispatchers.IO) {
chatUsers=dbRepository.getChatUserList().filter { it.locallySaved }
if (chatUsers.isNullOrEmpty())
startQuery()
}
}
fun getChatList() = dbRepository.getAllChatUser()
fun getChipList(): LiveData>{
return chipList
}
fun setChipList(list: List){
val newList=ArrayList(list.filter { it.isSelected })
chipList.value=newList
}
fun setContactList(list: List) {
allContacts.clear()
allContacts.addAll(list)
}
fun getContactList() = allContacts
private fun startQuery() {
try {
queryState.postValue(LoadState.OnLoading)
val success= UserUtils.updateContactsProfiles(onQueryCompleted)
if (!success) {
Timber.v("Recursion error")
queryState.postValue(LoadState.OnFailure(java.lang.Exception("Recursion exception")))
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onCleared() {
AdChip.allAddedContacts.clear()
allContacts.clear()
isFirstCall=true
Timber.v("OnClear AddGroup")
super.onCleared()
}
private val onQueryCompleted=object : QueryCompleteListener {
override fun onQueryCompleted(queriedList: ArrayList) {
try {
val localContacts=UserUtils.fetchContacts(context)
val finalList = ArrayList()
val queriedList=UserUtils.queriedList
//set localsaved name to queried users
for(doc in queriedList){
val savedNumber=localContacts.firstOrNull { it.mobile == doc.mobile?.number }
if(savedNumber!=null){
val chatUser= UserUtils.getChatUser(doc, chatUsers, savedNumber.name)
finalList.add(chatUser)
}
}
queryState.value=LoadState.OnSuccess(finalList)
CoroutineScope(Dispatchers.IO).launch {
dbRepository.insertMultipleUser(finalList)
}
setDefaultValues()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun setDefaultValues() {
//set default values
UserUtils.totalRecursionCount=0
UserUtils.resultCount=0
UserUtils.queriedList.clear()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/add_group_members/FAddGroupMembers.kt
================================================
package com.gowtham.letschat.fragments.add_group_members
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FAddGroupMembersBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.utils.*
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class FAddGroupMembers : Fragment(), ItemClickListener {
private lateinit var binding: FAddGroupMembersBinding
@Inject
lateinit var preference: MPreference
private lateinit var searchView: SearchView
private var contactList = ArrayList()
private val adContact: AdAddMembers by lazy {
AdAddMembers(requireContext())
}
private val adChip: AdChip by lazy {
AdChip(requireContext())
}
private val viewModel: AddGroupViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FAddGroupMembersBinding.inflate(layoutInflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
setHasOptionsMenu(true)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Timber.v("onViewCreated")
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
binding.fab.setOnClickListener {
val addedContacts = AdChip.allAddedContacts
if (addedContacts.isNotEmpty()) {
val action = FAddGroupMembersDirections.actionFAddGroupMembersToFCreateGroup(
addedContacts.toTypedArray()
)
findNavController().navigate(action)
}
}
setToolbar()
setDataInView()
subscribeObservers()
}
private fun setToolbar() {
binding.toolbar.inflateMenu(R.menu.menu_search)
val searchItem: MenuItem? = binding.toolbar.menu.findItem(R.id.action_search)
searchView = searchItem?.actionView as SearchView
searchView.apply {
maxWidth = Integer.MAX_VALUE
queryHint = getString(R.string.txt_search)
}
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
adContact.filter(newText.toString())
return true
}
})
}
private fun subscribeObservers() {
viewModel.getChatList().observe(viewLifecycleOwner, { contacts ->
val allContacts = contacts.filter { it.locallySaved }
if (allContacts.isNotEmpty()) {
if (viewModel.isFirstCall) {
viewModel.setContactList(allContacts)
viewModel.isFirstCall=false
}
Timber.v("allContacts ->${viewModel.getContactList().first().localName}")
contactList.clear()
contactList.addAll(viewModel.getContactList())
AdAddMembers.allContacts = contactList
adContact.submitList(contactList)
if (!searchView.isIconified)
adContact.filter(searchView.query.toString())
}
})
viewModel.getChipList().observe(viewLifecycleOwner, { addedList ->
AdChip.allAddedContacts = addedList
adChip.submitList(addedList.toList())
adChip.notifyDataSetChanged()
if (addedList.isEmpty()) {
binding.txtEmptyMembers.show()
binding.fab.hide()} else {
binding.txtEmptyMembers.hide()
binding.fab.show()
binding.listChip.post {
binding.listChip.smoothScrollToPosition(addedList.lastIndex)
}
}
})
viewModel.queryState.observe(viewLifecycleOwner, {
searchView.isEnabled = it !is LoadState.OnLoading
when (it) {
is LoadState.OnSuccess -> {
val emptyList = it.data as ArrayList<*>
if (emptyList.isEmpty()) {
binding.viewEmpty.show()
binding.progress.hide()
binding.viewEmpty.playAnimation()
}else{
binding.viewHolder.show()
binding.progress.hide()
}
}
is LoadState.OnFailure -> {
binding.viewHolder.hide()
binding.progress.hide()
binding.viewEmpty.playAnimation()
}
is LoadState.OnLoading -> {
binding.viewEmpty.hide()
binding.viewHolder.hide()
binding.progress.show()
}
}
})
}
private fun setDataInView() {
binding.listContact.adapter = adContact
binding.listChip.adapter = adChip
AdAddMembers.listener = this
AdChip.listener = chipListener
adContact.addRestorePolicy()
adChip.addRestorePolicy()
}
override fun onItemClicked(v: View, position: Int) {
val currentList = ArrayList(adContact.currentList)
val user = currentList[position]
user.apply {
isSelected = !isSelected
}
currentList.set(position, user)
adContact.submitList(currentList)
adContact.notifyItemChanged(position)
val allContact = AdAddMembers.allContacts
val user1 = allContact.find { it.id == user.id }
val index = allContact.indexOf(user1)
allContact.set(index, user1!!)
viewModel.setContactList(allContact) //update in allContacts list
val chipList = AdChip.allAddedContacts
val contains = chipList.find { it.id == user.id }
if (contains == null)
chipList.add(user)
else
chipList.set(chipList.indexOf(contains), user)
viewModel.setChipList(chipList) //update chip list
}
private val chipListener: ItemClickListener = object : ItemClickListener {
override fun onItemClicked(v: View, position: Int) {
val added = AdChip.allAddedContacts
var index: Int?
val clickedUser = added.get(position)
val currentList = ArrayList(adContact.currentList)
val user = currentList.find { it.id == clickedUser.id }
if (user != null) { //update in current list
index = currentList.indexOf(user)
user.isSelected = false
currentList.set(index, user)
adContact.submitList(currentList)
adContact.notifyItemChanged(index)
}
val allUsers = AdAddMembers.allContacts //update allContactList
val user1 = allUsers.find { it.id == clickedUser.id }
val indexAllList = allUsers.indexOf(user1)
user1?.isSelected = false
allUsers.set(indexAllList, user1!!)
viewModel.setContactList(allUsers)
added.removeAt(position)
viewModel.setChipList(added)
if (!searchView.isIconified) //remove from chip list
adContact.filter(searchView.query.toString())
}
}
override fun onResume() {
super.onResume()
val chipList = AdChip.allAddedContacts
val allUsers = AdAddMembers.allContacts
for (user in allUsers)
user.isSelected = chipList.contains(user)
viewModel.setContactList(allUsers)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/contacts/AdContact.kt
================================================
package com.gowtham.letschat.fragments.contacts
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.databinding.RowContactBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.utils.ItemClickListener
import java.util.*
import kotlin.collections.ArrayList
class AdContact(context: Context, allUsers: ArrayList) :
RecyclerView.Adapter() {
private var users: ArrayList = allUsers
private var allUsers: ArrayList = ArrayList()
init {
this.allUsers.addAll(users)
}
companion object {
var itemClickListener: ItemClickListener? = null
}
fun filter(query: String) {
try {
users.clear()
if (query.isEmpty())
users.addAll(allUsers)
else {
for (country in allUsers) {
if (country.localName.toLowerCase(Locale.getDefault())
.contains(query.toLowerCase(Locale.getDefault())))
users.add(country)
}
}
notifyDataSetChanged()
} catch (e: Exception) {
e.stackTrace
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewModel {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RowContactBinding.inflate(layoutInflater, parent, false)
return UserViewModel(binding)
}
override fun onBindViewHolder(holder: UserViewModel, position: Int) {
holder.bind(users[position])
}
class UserViewModel(val binding: RowContactBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: ChatUser) {
binding.chatUser = item
binding.viewRoot.setOnClickListener { v ->
itemClickListener?.onItemClicked(v, bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
override fun getItemCount() = users.size
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/contacts/ContactsViewModel.kt
================================================
package com.gowtham.letschat.fragments.contacts
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.gowtham.letschat.core.QueryCompleteListener
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.LoadState
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.toast
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class ContactsViewModel @Inject constructor(
@ApplicationContext context: Context,
private val dbRepo: DbRepository) : ViewModel() {
val queryState = MutableLiveData()
val list= MutableLiveData>()
val contactsCount = MutableLiveData("0 Contacts")
private lateinit var chatUsers: List
init {
LogMessage.v("ContactsViewModel init")
CoroutineScope(Dispatchers.IO).launch{
chatUsers=dbRepo.getChatUserList()
}
}
fun getContacts()=dbRepo.getAllChatUser()
fun setContactCount(size: Int) {
contactsCount.value="$size Contacts"
}
fun startQuery() {
try {
queryState.value=LoadState.OnLoading
val success=UserUtils.updateContactsProfiles(onQueryCompleted)
if (!success)
queryState.value=LoadState.OnFailure(java.lang.Exception("Recursion exception"))
} catch (e: Exception) {
e.printStackTrace()
}
}
private val onQueryCompleted=object : QueryCompleteListener {
override fun onQueryCompleted(queriedList: ArrayList) {
try {
LogMessage.v("Query Completed ${UserUtils.queriedList.size}")
val localContacts=UserUtils.fetchContacts(context)
val finalList = ArrayList()
val list=UserUtils.queriedList
//set localsaved name to queried users
for(doc in list){
val savedNumber=localContacts.firstOrNull { it.mobile == doc.mobile?.number }
if(savedNumber!=null){
val chatUser= UserUtils.getChatUser(doc, chatUsers, savedNumber.name)
Timber.v("Contact ${chatUser.documentId}")
finalList.add(chatUser)
}
}
contactsCount.value="${finalList.size} Contacts"
queryState.value=LoadState.OnSuccess(finalList)
CoroutineScope(Dispatchers.IO).launch {
dbRepo.insertMultipleUser(finalList)
}
context.toast("Contacts refreshed")
setDefaultValues()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private fun setDefaultValues() {
//set default values
UserUtils.totalRecursionCount=0
UserUtils.resultCount=0
UserUtils.queriedList.clear()
}
override fun onCleared() {
LogMessage.v("ContactsViewModel OnCleared")
super.onCleared()
}
fun setUnReadCountZero(chatUser: ChatUser) {
UserUtils.setUnReadCountZero(dbRepo,chatUser)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/contacts/FContacts.kt
================================================
package com.gowtham.letschat.fragments.contacts
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.datastore.preferences.core.Preferences
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FContactsBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.utils.*
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class FContacts @Inject constructor(private val preference: MPreference) : Fragment(), ItemClickListener {
private lateinit var binding: FContactsBinding
private lateinit var context: Activity
private lateinit var searchView: SearchView
private lateinit var searchItem: MenuItem
private lateinit var menuRefresh: MenuItem
private val viewModel: ContactsViewModel by viewModels()
private var contactList = ArrayList()
private lateinit var adContact: AdContact
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = FContactsBinding.inflate(layoutInflater, container, false)
setHasOptionsMenu(true)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context = requireActivity()
binding.lifecycleOwner = viewLifecycleOwner
binding.viewmodel = viewModel
setToolbar()
setDataInView()
subscribeObservers()
}
private fun subscribeObservers() {
viewModel.getContacts().observe(viewLifecycleOwner, { contacts->
LogMessage.v("Size ${contacts.size}")
val allContacts=contacts.filter { it.locallySaved }
if (allContacts.isEmpty() && viewModel.queryState.value == null)
viewModel.startQuery()
else {
viewModel.setContactCount(allContacts.size)
contactList.clear()
contactList= allContacts as ArrayList
adContact = AdContact(requireContext(), contactList)
binding.listContact.adapter = adContact
if(searchItem.isActionViewExpanded)
adContact.filter(searchView.query.toString())
}
})
viewModel.queryState.observe(viewLifecycleOwner,{
searchItem.isEnabled = it !is LoadState.OnLoading
menuRefresh.isEnabled = it !is LoadState.OnLoading
})
}
private fun setDataInView() {
try {
adContact = AdContact(requireContext(), contactList)
binding.listContact.adapter = adContact
AdContact.itemClickListener = this
adContact.stateRestorationPolicy =
RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setToolbar() {
try {
binding.toolbar.setNavigationIcon(R.drawable.ic_arrow_back)
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
binding.toolbar.inflateMenu(R.menu.menu_contacts)
searchItem = binding.toolbar.menu.findItem(R.id.action_search)
menuRefresh = binding.toolbar.menu.findItem(R.id.action_refresh)
menuRefresh.setOnMenuItemClickListener {
viewModel.startQuery()
true
}
searchView = searchItem.actionView as SearchView
searchView.apply {
maxWidth = Integer.MAX_VALUE
queryHint = getString(R.string.txt_search)
}
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
menuRefresh.isVisible = false
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
menuRefresh.isVisible = true
return true
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
adContact.filter(newText.toString())
return true
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onItemClicked(v: View, position: Int) {
viewModel.setUnReadCountZero(contactList[position])
preference.setCurrentUser(contactList[position].user.uId!!)
val action = FContactsDirections.actionFContactsToChat(
contactList[position]
)
findNavController().navigate(action)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/countries/AdCountries.kt
================================================
package com.gowtham.letschat.fragments.countries
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.RowCountryBinding
import com.gowtham.letschat.models.Country
import com.gowtham.letschat.utils.Countries
import com.gowtham.letschat.utils.ItemClickListener
import java.util.*
import kotlin.collections.ArrayList
class AdCountries : RecyclerView.Adapter() {
lateinit var countries: ArrayList
private lateinit var allCountries: ArrayList
fun setData() {
this.countries = Countries.getCountries() as ArrayList
allCountries= ArrayList()
allCountries.addAll(countries)
}
companion object {
var itemClickListener: ItemClickListener? = null
}
fun filter(query: String) {
try {
countries.clear()
if (query.isEmpty())
countries.addAll(allCountries)
else {
for (country in allCountries) {
if (country.name.toLowerCase(Locale.getDefault())
.contains(query.toLowerCase(Locale.getDefault()))
)
countries.add(country)
}
}
notifyDataSetChanged()
} catch (e: Exception) {
e.stackTrace
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewModel {
val binding: RowCountryBinding = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.row_country, parent, false
)
return UserViewModel(binding)
}
override fun onBindViewHolder(holder: UserViewModel, position: Int) {
holder.bind(countries[position])
}
class UserViewModel(val binding: RowCountryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: Country) {
binding.country = item
binding.viewRoot.setOnClickListener { v ->
itemClickListener?.onItemClicked(v, bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
override fun getItemCount() = countries.size
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/countries/FCountries.kt
================================================
package com.gowtham.letschat.fragments.countries
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FCountriesBinding
import com.gowtham.letschat.ui.activities.SharedViewModel
import com.gowtham.letschat.utils.ItemClickListener
import com.gowtham.letschat.utils.Utils
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class FCountries : Fragment(), ItemClickListener {
private lateinit var binding: FCountriesBinding
private lateinit var recyclerView: RecyclerView
private lateinit var searchView: SearchView
private lateinit var adCountry: AdCountries
private lateinit var sharedViewModel: SharedViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FCountriesBinding.inflate(layoutInflater, container, false)
setHasOptionsMenu(true)
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView = binding.listCountry
sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)
setDataInView()
}
private fun setDataInView() {
try {
binding.toolbar.inflateMenu(R.menu.menu_search)
val searchItem: MenuItem? = binding.toolbar.menu.findItem(R.id.action_search)
searchView = searchItem?.actionView as SearchView
searchView.apply {
maxWidth = Integer.MAX_VALUE
queryHint = getString(R.string.txt_search)
}
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
adCountry.filter(newText.toString())
return true
}
})
AdCountries.itemClickListener = this
adCountry = AdCountries()
adCountry.setData()
recyclerView.adapter = adCountry
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onItemClicked(v: View, position: Int) {
findNavController().popBackStack()
sharedViewModel.setCountry(adCountry.countries[position])
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/create_group/CreateGroupViewModel.kt
================================================
package com.gowtham.letschat.fragments.create_group
import android.content.Context
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.android.gms.tasks.OnFailureListener
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.SetOptions
import com.gowtham.letschat.TYPE_NEW_GROUP
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.di.GroupCollection
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import javax.inject.Inject
import kotlin.random.Random
@HiltViewModel
class CreateGroupViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val preference: MPreference,
private val userCollection: CollectionReference,
private val dbRepo: DbRepository,
@GroupCollection
private val groupCollection: CollectionReference) : ViewModel() {
val progressProPic = MutableLiveData(false)
val groupName = MutableLiveData("")
val imageUrl = MutableLiveData("")
val groupCreateStatus = MutableLiveData()
private val storageRef=UserUtils.getStorageRef(context)
fun uploadProfileImage(imagePath: Uri) {
try {
progressProPic.value = true
val child = storageRef.child("group_${System.currentTimeMillis()}.jpg")
val task = child.putFile(imagePath)
task.addOnSuccessListener {
child.downloadUrl.addOnCompleteListener { taskResult ->
progressProPic.value = false
imageUrl.value = taskResult.result.toString()
}.addOnFailureListener {
OnFailureListener { e ->
progressProPic.value = false
context.toast(e.message.toString())
}
}
}.addOnProgressListener { taskSnapshot ->
val progress: Double =
100.0 * taskSnapshot.bytesTransferred / taskSnapshot.totalByteCount
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun createGroup(memberList: ArrayList) {
groupCreateStatus.value=LoadState.OnLoading
val gName=groupName.value+"_${Random.nextInt(0,100)}" //
memberList.add(0,ChatUser(preference.getUid()!!,"You",preference.getUserProfile()!!))
val listOfProfiles=memberList.map { it.user } as ArrayList
val groupData=Group(gName, preference.getUid()!!,
System.currentTimeMillis(),"",imageUrl.value.toString(),null,listOfProfiles)
groupCollection.document(gName).set(groupData, SetOptions.merge()).
addOnSuccessListener {
updateGroupInEveryUserProfile(groupData,memberList)
}.addOnFailureListener { exception->
groupCreateStatus.value=LoadState.OnFailure(exception)
context.toast(exception.message.toString())
}
}
private fun updateGroupInEveryUserProfile(group: Group, memberList: ArrayList) {
group.members = memberList
group.profiles = ArrayList()
val listOfIds = memberList.map { it.id }
val batch = FirebaseFirestore.getInstance().batch()
for (id in listOfIds) {
val userDoc = userCollection.document(id)
batch.set(
userDoc, mapOf("groups" to FieldValue.arrayUnion(group.id)),
SetOptions.merge()
)
}
batch.commit().addOnSuccessListener {
LogMessage.v("Batch update success for group Creation")
groupCreateStatus.value = LoadState.OnSuccess(group)
dbRepo.insertGroup(group)
val groupdata = Group(group.id)
for (user in group.members!!) {
val token = user.user.token
if (token.isNotEmpty())
UserUtils.sendPush(context, TYPE_NEW_GROUP,
Json.encodeToString(groupdata), token, user.id)
}
}.addOnFailureListener { exception ->
LogMessage.v("Batch update failure ${exception.message} for group Creation")
groupCreateStatus.value = LoadState.OnFailure(exception)
context.toast(exception.message.toString())
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/create_group/FCreateGroup.kt
================================================
package com.gowtham.letschat.fragments.create_group
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.canhub.cropper.CropImage
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FCreateGroupBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.fragments.add_group_members.AdAddMembers
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.views.CustomProgressView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class FCreateGroup : Fragment() {
@Inject
lateinit var chatUserDao: ChatUserDao
@Inject
lateinit var preference: MPreference
private val viewModel: CreateGroupViewModel by viewModels()
private lateinit var binding: FCreateGroupBinding
val args by navArgs()
private lateinit var memberList: List
private var progressView: CustomProgressView?=null
private val adMembers: AdAddMembers by lazy {
AdAddMembers(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding=FCreateGroupBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner=viewLifecycleOwner
binding.viewmodel=viewModel
progressView= CustomProgressView(requireContext())
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}
binding.imageAddImage.setOnClickListener {
ImageUtils.askPermission(this)
}
binding.fab.setOnClickListener {
validate()
}
setDataInView()
subscribeObservers()
}
private fun subscribeObservers() {
viewModel.groupCreateStatus.observe(viewLifecycleOwner,{
when (it) {
is LoadState.OnSuccess -> {
if (findNavController().isValidDestination(R.id.FCreateGroup)) {
progressView?.dismiss()
val group=it.data as Group
preference.setCurrentGroup(group.id)
val action=FCreateGroupDirections.actionFCreateGroupToFGroupChat(group)
findNavController().navigate(action)
}
}
is LoadState.OnFailure -> {
progressView?.dismiss()
}
is LoadState.OnLoading -> {
progressView?.show()
}
}
})
}
private fun validate() {
val groupName=viewModel.groupName.value.toString().trim()
if (groupName.isNotEmpty() && !viewModel.progressProPic.value!!)
viewModel.createGroup(memberList as ArrayList)
}
private fun setDataInView() {
binding.edtGroupName.requestFocus()
Utils.showSoftKeyboard(requireActivity(),binding.edtGroupName)
memberList=args.memberList.toList().map {
it.isSelected=false
it
}
val memberCount=memberList.size
binding.memberCount=if(memberCount==1) "$memberCount member" else "$memberCount members"
binding.listMembers.adapter = adMembers
adMembers.addRestorePolicy()
adMembers.submitList(memberList)
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
ImageUtils.onImagePerResult(this, *grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE)
onCropResult(data)
else
ImageUtils.cropImage(requireActivity(), data)
}
private fun onCropResult(data: Intent?) {
try {
val imagePath: Uri? = ImageUtils.getCroppedImage(data)
imagePath?.let {
viewModel.uploadProfileImage(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onDestroy() {
super.onDestroy()
progressView?.dismissIfShowing()
Utils.closeKeyBoard(requireActivity())
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/group_chat/AdGroupChat.kt
================================================
package com.gowtham.letschat.fragments.group_chat
import android.content.Context
import android.media.MediaPlayer
import android.net.Uri
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.*
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.fragments.single_chat.AdChat
import com.gowtham.letschat.utils.Events.EventAudioMsg
import com.gowtham.letschat.utils.ItemClickListener
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.gone
import com.gowtham.letschat.utils.show
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.io.IOException
class AdGroupChat (private val context: Context,
private val msgClickListener: ItemClickListener) :
ListAdapter(DiffCallbackMessages()) {
private val preference = MPreference(context)
companion object {
private const val TYPE_TXT_SENT = 0
private const val TYPE_TXT_RECEIVED = 1
private const val TYPE_IMG_SENT = 2
private const val TYPE_IMG_RECEIVE = 3
private const val TYPE_STICKER_SENT = 4
private const val TYPE_STICKER_RECEIVE = 5
private const val TYPE_AUDIO_SENT = 6
private const val TYPE_AUDIO_RECEIVE = 7
lateinit var messageList: MutableList
lateinit var chatUserList: MutableList
private var player = MediaPlayer()
private var lastPlayedHolder: RowGroupAudioSentBinding?=null
private var lastReceivedPlayedHolder: RowGroupAudioReceiveBinding?=null
private var lastPlayedAudioId : Long=-1
fun stopPlaying() {
if(player.isPlaying) {
lastReceivedPlayedHolder?.progressBar?.abandon()
lastPlayedHolder?.progressBar?.abandon()
lastReceivedPlayedHolder?.imgPlay?.setImageResource(R.drawable.ic_action_play)
lastPlayedHolder?.imgPlay?.setImageResource(R.drawable.ic_action_play)
player.apply {
stop()
reset()
EventBus.getDefault().post(EventAudioMsg(false))
}
}
}
fun isPlaying() = player.isPlaying
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_TXT_SENT -> {
val binding = RowGroupTxtSentBinding.inflate(layoutInflater, parent, false)
TxtSentMsgHolder(binding)
}
TYPE_TXT_RECEIVED-> {
val binding = RowGrpTxtReceiveBinding.inflate(layoutInflater, parent, false)
TxtReceivedMsgHolder(binding)
}
TYPE_IMG_SENT -> {
val binding = RowGroupImageSentBinding.inflate(layoutInflater, parent, false)
ImgSentMsgHolder(binding)
}
TYPE_IMG_RECEIVE->{
val binding = RowGroupImageReceiveBinding.inflate(layoutInflater, parent, false)
ImgReceivedMsgHolder(binding)
}
TYPE_STICKER_SENT -> {
val binding = RowGroupStickerSentBinding.inflate(layoutInflater, parent, false)
StickerSentMsgHolder(binding)
}
TYPE_STICKER_RECEIVE-> {
val binding = RowGroupStickerReceiveBinding.inflate(layoutInflater, parent, false)
StickerReceivedMsgHolder(binding)
}
TYPE_AUDIO_SENT -> {
val binding = RowGroupAudioSentBinding.inflate(layoutInflater, parent, false)
AudioSentVHolder(binding)
}
else-> {
val binding = RowGroupAudioReceiveBinding.inflate(layoutInflater, parent, false)
AudioReceiveVHolder(binding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder){
is TxtSentMsgHolder ->
holder.bind(getItem(position))
is TxtReceivedMsgHolder ->
holder.bind(getItem(position))
is ImgSentMsgHolder ->
holder.bind(getItem(position),msgClickListener)
is ImgReceivedMsgHolder ->
holder.bind(getItem(position),msgClickListener)
is StickerSentMsgHolder ->
holder.bind(getItem(position))
is StickerReceivedMsgHolder ->
holder.bind(getItem(position))
is AudioSentVHolder ->
holder.bind(context,getItem(position))
is AudioReceiveVHolder ->
holder.bind(context,getItem(position))
}
}
override fun getItemViewType(position: Int): Int {
val message = getItem(position)
val fromMe=message.from == preference.getUid()
if (fromMe && message.type == "text")
return TYPE_TXT_SENT
else if (!fromMe && message.type == "text")
return TYPE_TXT_RECEIVED
else if (fromMe && message.type == "image" && message.imageMessage?.imageType=="image")
return TYPE_IMG_SENT
else if (!fromMe && message.type == "image" && message.imageMessage?.imageType=="image")
return TYPE_IMG_RECEIVE
else if (fromMe && message.type == "image" && (message.imageMessage?.imageType=="sticker"
|| message.imageMessage?.imageType=="gif"))
return TYPE_STICKER_SENT
else if (!fromMe && message.type == "image" && (message.imageMessage?.imageType=="sticker"
|| message.imageMessage?.imageType=="gif"))
return TYPE_STICKER_RECEIVE
else if (fromMe && message.type == "audio")
return TYPE_AUDIO_SENT
else if (!fromMe && message.type == "audio")
return TYPE_AUDIO_RECEIVE
return super.getItemViewType(position)
}
class TxtSentMsgHolder(val binding: RowGroupTxtSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupMessage) {
binding.message = item
if (bindingAdapterPosition>0) {
val message = messageList[bindingAdapterPosition - 1]
if (message.from == item.from) {
binding.txtMsg.setBackgroundResource(R.drawable.shape_send_msg_corned)
}
}
binding.executePendingBindings()
}
}
class TxtReceivedMsgHolder(val binding: RowGrpTxtReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupMessage) {
binding.message = item
binding.chatUsers= chatUserList.toTypedArray()
if (bindingAdapterPosition>0) {
val lastMsg = messageList[bindingAdapterPosition - 1]
if (lastMsg.from == item.from) {
binding.apply {
viewDetail.gone()
}
binding.viewMsgHolder.setBackgroundResource(R.drawable.shape_receive_msg_corned)
}
}
binding.executePendingBindings()
}
}
class ImgSentMsgHolder(val binding: RowGroupImageSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupMessage, msgClickListener: ItemClickListener) {
binding.message = item
binding.imageMsg.setOnClickListener {
msgClickListener.onItemClicked(it,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
class ImgReceivedMsgHolder(val binding: RowGroupImageReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupMessage, msgClickListener: ItemClickListener) {
binding.message = item
binding.chatUsers= chatUserList.toTypedArray()
binding.imageMsg.setOnClickListener {
msgClickListener.onItemClicked(it,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
class StickerSentMsgHolder(val binding: RowGroupStickerSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupMessage) {
binding.message = item
binding.executePendingBindings()
}
}
class StickerReceivedMsgHolder(val binding: RowGroupStickerReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupMessage) {
binding.message = item
binding.chatUsers= chatUserList.toTypedArray()
binding.executePendingBindings()
}
}
class AudioReceiveVHolder(val binding: RowGroupAudioReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(context: Context, item: GroupMessage) {
binding.message = item
binding.progressBar.setStoriesCountDebug(1, 0)
binding.progressBar.setAllStoryDuration(item.audioMessage?.duration!!.toLong() * 1000)
binding.imgPlay.setOnClickListener {
startPlaying(
context,
item,
binding
)
}
binding.executePendingBindings()
}
private fun startPlaying(
context: Context,
item: GroupMessage,
currentHolder: RowGroupAudioReceiveBinding
) {
if (player.isPlaying) {
stopPlaying()
if (lastPlayedAudioId == item.createdAt)
return
}
player = MediaPlayer()
lastReceivedPlayedHolder = currentHolder
lastPlayedAudioId = item.createdAt
currentHolder.progressBuffer.show()
currentHolder.imgPlay.gone()
player.apply {
try {
setDataSource(context, Uri.parse(item.audioMessage?.uri))
prepareAsync()
setOnPreparedListener {
Timber.v("Started..")
start()
currentHolder.progressBuffer.gone()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_stop)
currentHolder.imgPlay.show()
currentHolder.progressBar.startStories()
EventBus.getDefault().post(EventAudioMsg(true))
}
setOnCompletionListener {
currentHolder.progressBar.abandon()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_play)
EventBus.getDefault().post(EventAudioMsg(false))
}
} catch (e: IOException) {
println("ChatFragment.startPlaying:prepare failed")
}
}
}
}
class AudioSentVHolder(val binding: RowGroupAudioSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
context: Context,
item: GroupMessage) {
binding.message = item
binding.progressBar.setStoriesCountDebug(1, 0)
binding.progressBar.setAllStoryDuration(item.audioMessage?.duration!!.toLong() * 1000)
binding.imgPlay.setOnClickListener {
startPlaying(
context,
item,
binding
)
}
binding.executePendingBindings()
}
private fun startPlaying(
context: Context,
item: GroupMessage,
currentHolder: RowGroupAudioSentBinding) {
if (player.isPlaying) {
stopPlaying()
if (lastPlayedAudioId == item.createdAt)
return
}
player = MediaPlayer()
lastPlayedHolder = currentHolder
lastPlayedAudioId = item.createdAt
currentHolder.progressBuffer.show()
currentHolder.imgPlay.gone()
player.apply {
try {
setDataSource(context, Uri.parse(item.audioMessage?.uri))
prepareAsync()
setOnPreparedListener {
Timber.v("Started..")
start()
currentHolder.progressBuffer.gone()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_stop)
currentHolder.imgPlay.show()
currentHolder.progressBar.startStories()
EventBus.getDefault().post(EventAudioMsg(true))
}
setOnCompletionListener {
currentHolder.progressBar.abandon()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_play)
EventBus.getDefault().post(EventAudioMsg(false))
}
} catch (e: IOException) {
println("ChatFragment.startPlaying:prepare failed")
}
}
}
}
class DiffCallbackMessages : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: GroupMessage, newItem: GroupMessage): Boolean {
return oldItem.createdAt == newItem.createdAt
}
override fun areContentsTheSame(oldItem: GroupMessage, newItem: GroupMessage): Boolean {
return oldItem == newItem
}
}}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/group_chat/FGroupChat.kt
================================================
package com.gowtham.letschat.fragments.group_chat
import android.Manifest
import android.animation.Animator
import android.content.Intent
import android.content.pm.ActivityInfo
import android.media.MediaRecorder
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.inputmethod.InputContentInfoCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.canhub.cropper.CropImage
import com.gowtham.letschat.databinding.FGroupChatBinding
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.data.*
import com.gowtham.letschat.fragments.FAttachment
import com.gowtham.letschat.models.MyImage
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.utils.Events.EventAudioMsg
import com.gowtham.letschat.views.CustomEditText
import com.stfalcon.imageviewer.StfalconImageViewer
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
import java.io.IOException
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
@AndroidEntryPoint
class FGroupChat : Fragment(), ItemClickListener, CustomEditText.KeyBoardInputCallbackListener {
@Inject
lateinit var groupDao: GroupDao
@Inject
lateinit var preference: MPreference
private val viewModel: GroupChatViewModel by viewModels()
private lateinit var binding: FGroupChatBinding
val args by navArgs()
lateinit var group: Group
private var messageList = mutableListOf()
private lateinit var manager: LinearLayoutManager
private lateinit var localUserId: String
private lateinit var fromUser: UserProfile
private var lastAudioFile=""
private var msgPostponed=false
var isRecording = false //whether is recoding now or not
private var recordStart = 0L
private var recorder: MediaRecorder? = null
private val REQ_AUDIO_PERMISSION=29
private var recordDuration = 0L
private val adChat: AdGroupChat by lazy {
AdGroupChat(requireContext(), this)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = FGroupChatBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewmodel = viewModel
group = args.group
binding.group = group
setViewListeners()
binding.viewChatBtm.edtMsg.setKeyBoardInputCallbackListener(this)
UserUtils.setUnReadCountGroup(groupDao, group)
setDataInView()
subscribeObservers()
lifecycleScope.launch {
viewModel.getGroupMessages(group.id).collect { message ->
if(message.isEmpty())
return@collect
messageList = message as MutableList
if(AdGroupChat.isPlaying()){
msgPostponed=true
return@collect
}
AdGroupChat.messageList = messageList
adChat.submitList(messageList)
Timber.v("Message list ${messageList.last()}")
//scroll to last items in recycler (recent messages)
if (messageList.isNotEmpty()) {
if (viewModel.getCanScroll()) //scroll only if new message arrived
binding.listMessage.smoothScrollToPos(messageList.lastIndex)
else
viewModel.canScroll(true)
}
}
}
}
private fun setViewListeners() {
binding.viewChatBtm.lottieSend.setOnClickListener {
sendMessage()
}
binding.viewChatHeader.viewBack.setOnClickListener {
findNavController().popBackStack()
}
binding.viewChatBtm.imgRecord.setOnClickListener {
AdGroupChat.stopPlaying()
if(Utils.checkPermission(this, Manifest.permission.RECORD_AUDIO,reqCode = REQ_AUDIO_PERMISSION))
startRecording()
}
binding.viewChatBtm.imageAdd.setOnClickListener {
val fragment= FAttachment.newInstance(Bundle())
fragment.show(childFragmentManager,"")
}
binding.lottieVoice.setOnClickListener {
if (isRecording){
stopRecording()
val duration=(recordDuration/1000).toInt()
if (duration<=1) {
requireContext().toast("Nothing is recorded!")
return@setOnClickListener
}
val msg=createMessage()
msg.type="audio"
msg.audioMessage= AudioMessage(lastAudioFile,duration)
viewModel.uploadToCloud(msg,lastAudioFile)
}
}
}
private fun sendMessage() {
val msg = binding.viewChatBtm.edtMsg.text?.trim().toString()
if (msg.isEmpty())
return
binding.viewChatBtm.lottieSend.playAnimation()
val messageData = TextMessage(msg)
val message = createMessage()
message.textMessage = messageData
viewModel.sendMessage(message)
binding.viewChatBtm.edtMsg.setText("")
}
private fun createMessage(): GroupMessage {
val toUsers = group.members?.map { it.id } as ArrayList
val groupSize = group.members!!.size
val statusList = ArrayList()
val deliveryTimeList = ArrayList()
for (index in 0 until groupSize) {
statusList.add(0)
deliveryTimeList.add(0L)
}
return GroupMessage(
System.currentTimeMillis(), group.id, from = localUserId,
to = toUsers, fromUser.userName, fromUser.image, statusList, deliveryTimeList,
deliveryTimeList
)
}
private fun subscribeObservers() {
viewModel.getChatUsers().observe(viewLifecycleOwner, { chatUsers ->
AdGroupChat.chatUserList = chatUsers.toMutableList()
})
viewModel.typingUsers.observe(viewLifecycleOwner, { typingUser ->
if (typingUser.isEmpty())
BindingAdapters.setMemberNames(binding.viewChatHeader.txtMembers, group)
else
binding.viewChatHeader.txtMembers.text = typingUser
})
}
private fun setDataInView() {
fromUser = preference.getUserProfile()!!
localUserId = fromUser.uId!!
manager = LinearLayoutManager(context)
binding.listMessage.apply {
manager.stackFromEnd = true
layoutManager = manager
setHasFixedSize(true)
isNestedScrollingEnabled = false
itemAnimator = null
}
binding.listMessage.adapter = adChat
adChat.addRestorePolicy()
viewModel.setGroup(group)
binding.viewChatBtm.edtMsg.addTextChangedListener(msgTxtChangeListener)
binding.viewChatBtm.lottieSend.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
super.onAnimationEnd(animation, isReverse)
}
override fun onAnimationEnd(p0: Animator?) {
if (Utils.edtValue(binding.viewChatBtm.edtMsg).isEmpty()) {
binding.viewChatBtm.imgRecord.show()
binding.viewChatBtm.lottieSend.gone()
}
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationRepeat(p0: Animator?) {
}
})
binding.lottieVoice.addAnimatorListener(object : Animator.AnimatorListener{
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
super.onAnimationEnd(animation, isReverse)
}
override fun onAnimationEnd(p0: Animator?) {
binding.viewChatBtm.imgRecord.show()
binding.lottieVoice.gone()
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationRepeat(p0: Animator?) {
}
})
}
private val msgTxtChangeListener = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
viewModel.sendTyping(binding.viewChatBtm.edtMsg.trim())
if(binding.viewChatBtm.lottieSend.isAnimating)
return
if(s.isNullOrBlank()) {
binding.viewChatBtm.imgRecord.show()
binding.viewChatBtm.lottieSend.hide()
}
else{
binding.viewChatBtm.lottieSend.show()
binding.viewChatBtm.imgRecord.hide()
}
}
override fun afterTextChanged(s: Editable?) {
}
}
override fun onItemClicked(v: View, position: Int) {
binding.fullSizeImageView.show()
StfalconImageViewer.Builder(
context,
listOf(MyImage(messageList.get(position).imageMessage?.uri!!))) { imageView, myImage ->
ImageUtils.loadGalleryImage(myImage.url,imageView)
}
.withDismissListener { binding.fullSizeImageView.visibility = View.GONE }
.show()
}
override fun onResume() {
preference.setCurrentGroup(group.id)
viewModel.sendCachedTxtMesssages()
Utils.removeNotification(requireContext())
super.onResume()
}
override fun onCommitContent(
inputContentInfo: InputContentInfoCompat?,
flags: Int,
opts: Bundle?) {
val imageMsg = createMessage()
val image = ImageMessage("${inputContentInfo?.contentUri}")
image.imageType = if (image.uri.toString().endsWith(".png")) "sticker" else "gif"
imageMsg.apply {
type = "image"
imageMessage = image
}
viewModel.uploadToCloud(imageMsg,image.toString())
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE)
onCropResult(data)
else
ImageUtils.cropImage(requireActivity(), data, true)
}
private fun onCropResult(data: Intent?) {
try {
val imagePath: Uri? = ImageUtils.getCroppedImage(data)
if (imagePath!=null){
val message=createMessage()
message.type="image"
message.imageMessage=ImageMessage(imagePath.toString())
viewModel.uploadToCloud(message,imagePath.toString())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode==REQ_AUDIO_PERMISSION){
if (Utils.isPermissionOk(*grantResults))
startRecording()
else
requireActivity().toast("Audio permission is needed!")
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onAttachmentItemClicked(event: BottomSheetEvent) {
when (event.position) {
0 -> {
ImageUtils.takePhoto(requireActivity())
}
1 -> {
ImageUtils.chooseGallery(requireActivity())
}
2 -> {
//create intent for gallery video
}
3 -> {
//create intent for camera video
}
}
}
private fun startRecording() {
binding.lottieVoice.show()
binding.lottieVoice.playAnimation()
binding.viewChatBtm.edtMsg.apply {
isEnabled=false
hint="Recording..."
}
onAudioEvent(EventAudioMsg(true))
//name of the file where record will be stored
lastAudioFile=
"${requireActivity().externalCacheDir?.absolutePath}/audiorecord${System.currentTimeMillis()}.mp3"
recorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.DEFAULT)
setOutputFile(lastAudioFile)
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
try {
prepare()
} catch (e: IOException) {
println("ChatFragment.startRecording${e.message}")
}
start()
isRecording=true
recordStart = Date().time
}
Handler(Looper.getMainLooper()).postDelayed({
binding.lottieVoice.pauseAnimation()
},800)
}
private fun stopRecording() {
onAudioEvent(EventAudioMsg(false))
binding.viewChatBtm.edtMsg.apply {
isEnabled=true
hint="Type Something..."
}
Handler(Looper.getMainLooper()).postDelayed({
binding.lottieVoice.resumeAnimation()
},200)
recorder?.apply {
stop()
release()
recorder = null
}
isRecording=false
recordDuration = Date().time - recordStart
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
}
override fun onDestroyView() {
super.onDestroyView()
stopRecording()
AdGroupChat.stopPlaying()
EventBus.getDefault().unregister(this)
}
@Subscribe
fun onAudioEvent(audioEvent: EventAudioMsg){
if (audioEvent.isPlaying){
//lock current orientation
val currentOrientation=requireActivity().resources.configuration.orientation
requireActivity().requestedOrientation = currentOrientation
}else {
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
if (msgPostponed){
//refresh list
AdGroupChat.messageList = messageList
adChat.submitList(messageList)
msgPostponed=false
}
}
}
override fun onStop() {
super.onStop()
preference.clearCurrentGroup()
}
override fun onDestroy() {
super.onDestroy()
Utils.closeKeyBoard(requireActivity())
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/group_chat/GroupChatViewModel.kt
================================================
package com.gowtham.letschat.fragments.group_chat
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkRequest
import com.google.firebase.database.DatabaseReference
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FieldValue
import com.google.firebase.firestore.ListenerRegistration
import com.gowtham.letschat.TYPE_NEW_GROUP_MESSAGE
import com.gowtham.letschat.core.GroupMsgSender
import com.gowtham.letschat.core.GroupMsgStatusUpdater
import com.gowtham.letschat.core.OnGrpMessageResponse
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.daos.GroupMessageDao
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.di.GroupCollection
import com.gowtham.letschat.fragments.single_chat.toDataClass
import com.gowtham.letschat.services.GroupUploadWorker
import com.gowtham.letschat.utils.Constants
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
class GroupChatViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val preference: MPreference,
private val groupMsgDao: GroupMessageDao,
private val chatUserDao: ChatUserDao,
private val dbRepository: DbRepository,
private val groupMsgStatusUpdater: GroupMsgStatusUpdater,
@GroupCollection
private val groupCollection: CollectionReference
) : ViewModel() {
val message = MutableLiveData()
val typingUsers = MutableLiveData()
private val currentGroup = preference.getOnlineGroup()
private val fromUser = preference.getUid()
private var isTyping = false
private var groupListener: ListenerRegistration? = null
private val typingHandler = Handler(Looper.getMainLooper())
private var canScroll = false
private var cleared = false
private lateinit var group: Group
init {
groupCollection.document(currentGroup).addSnapshotListener { value, error ->
try {
if (error == null) {
val list = value?.get("typing_users")
val users = if (list == null) ArrayList()
else list as ArrayList
val names = group.members?.filter { users.contains(it.id) && it.id != fromUser }
?.map { //get locally saved name
it.localName + " is typing..."
}
if (users.isNullOrEmpty())
typingUsers.postValue("")
else
typingUsers.postValue(TextUtils.join(",", names!!))
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
fun getGroupMessages(groupId: String) = groupMsgDao.getChatsOfGroup(groupId)
fun getChatUsers() = chatUserDao.getAllChatUser()
fun setGroup(group: Group) {
if (!this::group.isInitialized) {
this.group = group
setSeenAllMessage()
}
}
private fun setSeenAllMessage() {
if (this::group.isInitialized) {
group.unRead=0
dbRepository.insertGroup(group)
viewModelScope.launch(Dispatchers.IO) {
val messageList = dbRepository.getChatsOfGroupList(group.id)
withContext(Dispatchers.Main){
groupMsgStatusUpdater.updateToSeen(fromUser!!, messageList, group.id)
}
}
}
}
fun canScroll(can: Boolean) {
canScroll = can
}
fun getCanScroll() = canScroll
fun sendTyping(edtValue: String) {
if (edtValue.isEmpty()) {
if (isTyping)
sendTypingStatus(false, fromUser!!, currentGroup)
isTyping = false
} else if (!isTyping) {
sendTypingStatus(true, fromUser!!, currentGroup)
isTyping = true
removeTypingCallbacks()
typingHandler.postDelayed(typingThread, 4000)
}
}
private fun sendTypingStatus(
isTyping: Boolean,
fromUser: String, currentGroup: String
) {
val value =
if (isTyping) FieldValue.arrayUnion(fromUser) else FieldValue.arrayRemove(fromUser)
groupCollection.document(currentGroup).update("typing_users", value)
}
private val typingThread = Runnable {
isTyping = false
sendTypingStatus(false, fromUser!!, currentGroup)
removeTypingCallbacks()
}
private fun removeTypingCallbacks() {
typingHandler.removeCallbacks(typingThread)
}
fun sendCachedTxtMesssages() {
CoroutineScope(Dispatchers.IO).launch {
updateCacheMessges(groupMsgDao.getChatsOfGroupList(currentGroup))
}
}
private suspend fun updateCacheMessges(chatsOfGroup: List) {
withContext(Dispatchers.Main) {
val nonSendMsgs = chatsOfGroup.filter {
it.from == fromUser
&& it.status[0] == 0 && it.type == "text"
}
LogMessage.v("nonSendMsgs Group Size ${nonSendMsgs.size}")
for (cachedMsg in nonSendMsgs) {
val messageSender = GroupMsgSender(groupCollection)
messageSender.sendMessage(cachedMsg, group, messageListener)
}
}
}
override fun onCleared() {
super.onCleared()
cleared = true
groupListener?.remove()
}
fun sendMessage(message: GroupMessage) {
Handler(Looper.getMainLooper()).postDelayed({
val messageSender = GroupMsgSender(groupCollection)
messageSender.sendMessage(message, group, messageListener)
}, 300)
UserUtils.insertGroupMsg(groupMsgDao, message)
}
fun uploadToCloud(message: GroupMessage, fileUri: String) {
try {
UserUtils.insertGroupMsg(groupMsgDao, message)
removeTypingCallbacks()
val messageData = Json.encodeToString(message)
val groupData = Json.encodeToString(group)
val data = Data.Builder()
.putString(Constants.MESSAGE_FILE_URI, fileUri)
.putString(Constants.MESSAGE_DATA, messageData)
.putString(Constants.GROUP_DATA, groupData)
.build()
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder()
.setInputData(data)
.build()
WorkManager.getInstance(context).enqueue(uploadWorkRequest)
} catch (e: Exception) {
e.printStackTrace()
}
}
private val messageListener = object : OnGrpMessageResponse {
override fun onSuccess(message: GroupMessage) {
LogMessage.v("messageListener OnSuccess ${message.textMessage?.text}")
UserUtils.insertGroupMsg(groupMsgDao, message)
val users = group.members?.filter { it.user.token.isNotEmpty() }?.map {
it.user.token
it
}
users?.forEach {
UserUtils.sendPush(
context, TYPE_NEW_GROUP_MESSAGE,
Json.encodeToString(message), it.user.token.toString(), it.id
)
}
}
override fun onFailed(message: GroupMessage) {
LogMessage.v("messageListener onFailed ${message.createdAt}")
UserUtils.insertGroupMsg(groupMsgDao, message)
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/group_chat_home/AdGroupChatHome.kt
================================================
package com.gowtham.letschat.fragments.group_chat_home
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.databinding.RowChatBinding
import com.gowtham.letschat.databinding.RowGroupChatBinding
import com.gowtham.letschat.databinding.RowReceiveMessageBinding
import com.gowtham.letschat.databinding.RowSentMessageBinding
import com.gowtham.letschat.db.data.ChatUserWithMessages
import com.gowtham.letschat.db.data.GroupWithMessages
import com.gowtham.letschat.fragments.single_chat_home.AdSingleChatHome
import com.gowtham.letschat.utils.ItemClickListener
import com.gowtham.letschat.utils.MPreference
import java.util.*
class AdGroupChatHome(private val context: Context) :
ListAdapter(DiffCallbackChats()) {
private val preference = MPreference(context)
companion object {
lateinit var allList: MutableList
lateinit var itemClickListener: ItemClickListener
}
fun filter(query: String) {
try {
val list= mutableListOf()
if (query.isEmpty())
list.addAll(allList)
else {
for (group in allList) {
if (group.group.id.toLowerCase(Locale.getDefault())
.contains(query.toLowerCase(Locale.getDefault()))) {
list.add(group)
}
}
}
submitList(null)
submitList(list)
} catch (e: Exception) {
e.stackTrace
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RowGroupChatBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val viewHolder=holder as ViewHolder
viewHolder.bind(getItem(position))
}
class ViewHolder(val binding: RowGroupChatBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: GroupWithMessages) {
binding.groupChat = item
binding.viewRoot.setOnClickListener { v ->
itemClickListener.onItemClicked(v,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
}
class DiffCallbackChats : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: GroupWithMessages, newItem: GroupWithMessages): Boolean {
return oldItem.group.id == oldItem.group.id
}
override fun areContentsTheSame(oldItem: GroupWithMessages, newItem: GroupWithMessages): Boolean {
return oldItem.messages == newItem.messages && oldItem.group==newItem.group
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/group_chat_home/FGroupChatHome.kt
================================================
package com.gowtham.letschat.fragments.group_chat_home
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FGroupChatHomeBinding
import com.gowtham.letschat.db.data.GroupWithMessages
import com.gowtham.letschat.ui.activities.SharedViewModel
import com.gowtham.letschat.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@AndroidEntryPoint
class FGroupChatHome : Fragment(),ItemClickListener{
private val viewModel: GroupChatHomeViewModel by viewModels()
private lateinit var binding: FGroupChatHomeBinding
private val sharedViewModel by activityViewModels()
@Inject
lateinit var preference: MPreference
private lateinit var activity: Activity
private val groups= mutableListOf()
private val adGroupHome by lazy {
AdGroupChatHome(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FGroupChatHomeBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity=requireActivity()
binding.lifecycleOwner = viewLifecycleOwner
setDataInView()
subscribeObservers()
}
private fun subscribeObservers() {
lifecycleScope.launch {
viewModel.getGroupMessages().collect { groupWithmsgs ->
updateList(groupWithmsgs)
}
}
sharedViewModel.getState().observe(viewLifecycleOwner,{state->
if (state is ScreenState.IdleState){
CoroutineScope(Dispatchers.IO).launch {
updateList(viewModel.getGroupMessagesAsList())
}
}
})
sharedViewModel.lastQuery.observe(viewLifecycleOwner,{
if (sharedViewModel.getState().value is ScreenState.SearchState)
adGroupHome.filter(it)
})
}
private suspend fun updateList(groupWithmsgs: List) {
withContext(Dispatchers.Main){
if (!groupWithmsgs.isNullOrEmpty()) {
val list1= groupWithmsgs.filter { it.messages.isEmpty() }
.sortedByDescending { it.group.createdAt }.toMutableList()
val groupHasMsgsList=groupWithmsgs.filter { it.messages.isNotEmpty() }.
sortedBy { it.messages.last().createdAt }
for (a in groupHasMsgsList)
list1.add(0,a)
adGroupHome.submitList(list1)
AdGroupChatHome.allList=list1
groups.clear()
groups.addAll(list1)
if(sharedViewModel.getState().value is ScreenState.SearchState)
adGroupHome.filter(sharedViewModel.lastQuery.value.toString())
}else
binding.imageEmpty.show()
}
}
private fun setDataInView() {
binding.listGroup.adapter = adGroupHome
binding.listGroup.itemAnimator = null
AdGroupChatHome.itemClickListener=this
adGroupHome.addRestorePolicy()
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (Utils.isPermissionOk(*grantResults) &&
findNavController().isValidDestination(R.id.FGroupChatHome)){
findNavController().navigate(R.id.action_FGroupChatHome_to_FAddGroupMembers)
}
else
activity.toast("Permission is needed!")
}
override fun onItemClicked(v: View, position: Int) {
sharedViewModel.setState(ScreenState.IdleState)
val group = adGroupHome.currentList[position].group
preference.setCurrentGroup(group.id)
val action = FGroupChatHomeDirections.actionFGroupChatHomeToFGroupChat(group)
findNavController().navigate(action)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/group_chat_home/GroupChatHomeViewModel.kt
================================================
package com.gowtham.letschat.fragments.group_chat_home
import androidx.lifecycle.ViewModel
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.utils.MPreference
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class GroupChatHomeViewModel @Inject constructor(
private val preference: MPreference,
private val dbRepository: DbRepository,
private val usersCollection: CollectionReference) : ViewModel() {
fun getGroupMessages() = dbRepository.getGroupWithMessages()
fun getGroupMessagesAsList() = dbRepository.getGroupWithMessagesList()
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/login/FLogin.kt
================================================
package com.gowtham.letschat.fragments.login
import android.content.Context
import android.os.Bundle
import android.telephony.TelephonyManager
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FLoginBinding
import com.gowtham.letschat.models.Country
import com.gowtham.letschat.ui.activities.SharedViewModel
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.views.CustomProgressView
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class FLogin : Fragment() {
private var country: Country? = null
private lateinit var binding: FLoginBinding
private val sharedViewModel by activityViewModels()
private var progressView: CustomProgressView?=null
private val viewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FLoginBinding.inflate(layoutInflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
progressView = CustomProgressView(requireContext())
setDataInView()
subscribeObservers()
}
private fun setDataInView() {
binding.viewmodel = viewModel
setDefaultCountry()
binding.txtCountryCode.setOnClickListener {
Utils.closeKeyBoard(requireActivity())
findNavController().navigate(R.id.action_FLogIn_to_FCountries)
}
binding.btnGetOtp.setOnClickListener {
validate()
}
}
private fun validate() {
try {
Utils.closeKeyBoard(requireActivity())
val mobileNo = viewModel.mobile.value?.trim()
val country = viewModel.country.value
when {
Validator.isMobileNumberEmpty(mobileNo) -> snack(requireActivity(), "Enter valid mobile number")
country == null -> snack(requireActivity(), "Select a country")
!Validator.isValidNo(country.code, mobileNo!!) -> snack(
requireActivity(),
"Enter valid mobile number"
)
Utils.isNoInternet(requireContext()) -> snackNet(requireActivity())
else -> {
viewModel.setMobile()
viewModel.setProgress(true)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun setDefaultCountry() {
try {
country = Utils.getDefaultCountry()
val manager =
requireActivity().getSystemService(Context.TELEPHONY_SERVICE) as (TelephonyManager)?
manager?.let {
val countryCode = Utils.clearNull(manager.networkCountryIso)
if (countryCode.isEmpty())
return
val countries = Countries.getCountries()
for (i in countries) {
if (i.code.equals(countryCode, true))
country = i
}
viewModel.setCountry(country!!)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun subscribeObservers() {
try {
sharedViewModel.country.observe(viewLifecycleOwner, {
viewModel.setCountry(it)
})
viewModel.getProgress().observe(viewLifecycleOwner, {
progressView?.toggle(it)
})
viewModel.getVerificationId().observe(viewLifecycleOwner, { vCode ->
vCode?.let {
viewModel.setProgress(false)
viewModel.resetTimer()
viewModel.setVCodeNull()
viewModel.setEmptyText()
if (findNavController().isValidDestination(R.id.FLogIn))
findNavController().navigate(R.id.action_FLogIn_to_FVerify)
}
})
viewModel.getFailed().observe(viewLifecycleOwner, {
progressView?.dismiss()
})
viewModel.getTaskResult().observe(viewLifecycleOwner, { taskId ->
if (taskId!=null && viewModel.getCredential().value?.smsCode.isNullOrEmpty())
viewModel.fetchUser(taskId)
})
viewModel.userProfileGot.observe(viewLifecycleOwner, { success ->
if (success && viewModel.getCredential().value?.smsCode.isNullOrEmpty()
&& findNavController().isValidDestination(R.id.FLogIn)) {
requireActivity().toastLong("Authenticated successfully using Instant verification")
findNavController().navigate(R.id.action_FLogIn_to_FProfile)
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
/* val action = FMobileDirections.actionFMobileToFVerify(
Country(
code = "sd",
name = "sda",
noCode = "+83",
money = "mon"
)
)
findNavController().navigate(action)*/
override fun onDestroy() {
try {
progressView?.dismissIfShowing()
super.onDestroy()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/login/FVerify.kt
================================================
package com.gowtham.letschat.fragments.login
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.firebase.auth.PhoneAuthProvider
import com.gowtham.letschat.BuildConfig
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FVerifyBinding
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.views.CustomProgressView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class FVerify : Fragment() {
private lateinit var binding: FVerifyBinding
private val viewModel by activityViewModels()
private lateinit var edtTexts: ArrayList
@Inject
lateinit var preferences: MPreference
private var progressView: CustomProgressView?=null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FVerifyBinding.inflate(layoutInflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewmodel = viewModel
progressView = CustomProgressView(requireContext())
setDataInView()
subscribeObservers()
/*
if (arguments != null) {
val s = FVerifyArgs.fromBundle(requireArguments()).country
s.name.printMeD()
} else
"argument is null".printMeD()*/
}
private fun setDataInView() {
try {
edtTexts = ArrayList()
edtTexts.add(binding.edtOne)
edtTexts.add(binding.edtTwo)
edtTexts.add(binding.edtThree)
edtTexts.add(binding.edtFour)
edtTexts.add(binding.edtFive)
edtTexts.add(binding.edtSix)
addListener()
if (viewModel.resendTxt.value.isNullOrEmpty())
viewModel.startTimer()
binding.btnVerify.setOnClickListener {
validateOtp()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun validateOtp() {
try {
val otp = getOtpValue()
when {
otp.length < 6 -> snack(requireActivity(), "Enter valid otp")
Utils.isNoInternet(requireContext()) -> {
snackNet(requireActivity())
}
else -> {
"VCode:: ${viewModel.verifyCode}".printMeD()
"OTP:: $otp".printMeD()
val credential = PhoneAuthProvider.getCredential(viewModel.verifyCode, otp)
viewModel.setCredential(credential)
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun getOtpValue(): String {
try {
var otp = ""
for (edtTxt in edtTexts)
otp += edtTxt.trim()
return otp
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
private fun addListener() {
try {
for (editText in edtTexts) {
editText.addTextChangedListener(OtpWatcher(editText))
editText.setOnKeyListener { _, keyCode: Int, _ ->
if (keyCode == KeyEvent.KEYCODE_DEL)
onKeyListener()
false
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun onKeyListener() {
try {
val edtView: EditText = edtTexts[viewModel.ediPosition]
if (edtView.trim().isEmpty() && viewModel.ediPosition > 0) {
viewModel.ediPosition -= 1
edtTexts[viewModel.ediPosition].requestFocus()
} else edtView.requestFocus()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
private fun subscribeObservers() {
try {
viewModel.getCredential().observe(viewLifecycleOwner, { credential ->
credential?.let {
val otp = credential.smsCode
edtTexts.forEachIndexed { i, editText ->
editText.text = otp?.get(i)?.toEditable()
}
viewModel.setVProgress(true)
}
})
viewModel.getVProgress().observe(viewLifecycleOwner, { show ->
progressView?.toggle(show)
})
viewModel.getFailed().observe(viewLifecycleOwner, {
viewModel.setVProgress(false)
})
viewModel.getVerificationId().observe(viewLifecycleOwner, { vCode ->
vCode?.let {
viewModel.setVProgress(false)
viewModel.setVCodeNull()
viewModel.startTimer()
}
})
viewModel.getTaskResult().observe(viewLifecycleOwner, { taskId ->
taskId?.let {
viewModel.fetchUser(taskId)
}
})
viewModel.userProfileGot.observe(viewLifecycleOwner, { success ->
if (success && findNavController().isValidDestination(R.id.FVerify))
findNavController().navigate(R.id.action_FVerify_to_FProfile)
})
} catch (e: Exception) {
e.printStackTrace()
}
}
inner class OtpWatcher(private val v: View) : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
val text = s.toString()
when (v.id) {
R.id.edt_one ->
changeFocus(text, 0, 1)
R.id.edt_two ->
changeFocus(text, 0, 2)
R.id.edt_three ->
changeFocus(text, 1, 3)
R.id.edt_four ->
changeFocus(text, 2, 4)
R.id.edt_five ->
changeFocus(text, 3, 5)
R.id.edt_six ->
changeFocus(text, 4, 5)
else -> {
if (text.isEmpty())
edtTexts[5].requestFocus()
}
}
}
}
private fun changeFocus(text: String, previous1: Int, next: Int) {
viewModel.ediPosition = next - 1
edtTexts[if (text.isEmpty()) previous1 else next].requestFocus()
}
override fun onDestroy() {
progressView?.dismissIfShowing()
viewModel.clearAll()
super.onDestroy()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/login/LogInViewModel.kt
================================================
package com.gowtham.letschat.fragments.login
import android.content.Context
import android.os.CountDownTimer
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.PhoneAuthCredential
import com.google.firebase.firestore.FirebaseFirestore
import com.gowtham.letschat.R
import com.gowtham.letschat.TYPE_LOGGED_IN
import com.gowtham.letschat.models.Country
import com.gowtham.letschat.models.ModelMobile
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
import dagger.hilt.android.scopes.ActivityScoped
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@HiltViewModel
class LogInViewModel @Inject
constructor(@ApplicationContext private val context: Context,
private val logInRepo: LoginRepo, private val preference: MPreference) :
ViewModel() {
val country = MutableLiveData()
val mobile = MutableLiveData()
val userProfileGot=MutableLiveData()
private val progress = MutableLiveData(false)
private val verifyProgress = MutableLiveData(false)
var canResend: Boolean = false
val resendTxt = MutableLiveData()
val otpOne = MutableLiveData()
val otpTwo = MutableLiveData()
val otpThree = MutableLiveData()
val otpFour = MutableLiveData()
val otpFive = MutableLiveData()
val otpSix = MutableLiveData()
var ediPosition = 0
var verifyCode: String = ""
private lateinit var timer: CountDownTimer
init {
"LogInViewModel init".printMeD()
}
fun setCountry(country: Country) {
this.country.value = country
}
fun setMobile() {
logInRepo.clearOldAuth()
saveMobile()
logInRepo.setMobile(country.value!!, mobile.value!!)
}
fun setProgress(show: Boolean) {
progress.value = show
}
fun getProgress(): LiveData {
return progress
}
fun resendClicked() {
"Resend Clicked".printMeD()
if (canResend) {
setVProgress(true)
setMobile()
}
}
fun startTimer() {
try {
canResend = false
timer = object : CountDownTimer(60000, 1000) {
override fun onTick(millisUntilFinished: Long) {
setTimerTxt(millisUntilFinished / 1000)
}
override fun onFinish() {
canResend = true
resendTxt.value = "Resend"
}
}
timer.start()
} catch (e: Exception) {
e.printStackTrace()
}
}
fun resetTimer() {
canResend = false
resendTxt.value = ""
if (this::timer.isInitialized)
timer.cancel()
}
private fun setTimerTxt(seconds: Long) {
try {
val s = seconds % 60
val m = seconds / 60 % 60
if (s == 0L && m == 0L) return
val resend: String =
context.getString(R.string.txt_resend) + " in " + String.format(
Locale.getDefault(),
"%02d:%02d",
m,
s
)
resendTxt.value = resend
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
fun setEmptyText(){
otpOne.value=""
otpTwo.value=""
otpThree.value=""
otpFour.value=""
otpFive.value=""
otpSix.value=""
}
fun setVProgress(show: Boolean) {
verifyProgress.value = show
}
fun getVProgress(): LiveData {
return verifyProgress
}
fun getCredential(): LiveData {
return logInRepo.getCredential()
}
fun setCredential(credential: PhoneAuthCredential) {
setVProgress(true)
logInRepo.setCredential(credential)
}
fun setVCodeNull(){
verifyCode=logInRepo.getVCode().value!!
logInRepo.setVCodeNull()
}
fun getVerificationId(): MutableLiveData {
return logInRepo.getVCode()
}
fun getTaskResult(): LiveData> {
return logInRepo.getTaskResult()
}
fun getFailed(): LiveData {
return logInRepo.getFailed()
}
private fun saveMobile() =
preference.saveMobile(ModelMobile(country.value!!.noCode,mobile.value!!))
fun fetchUser(taskId: Task) {
val db = FirebaseFirestore.getInstance()
val user = taskId.result?.user
Timber.v("FetchUser:: ${user?.uid}")
val noteRef = db.document("Users/" + user?.uid)
noteRef.get()
.addOnSuccessListener { data ->
Timber.v("Uss:: ${preference.getUid()}")
preference.setUid(user?.uid.toString())
Timber.v("Uss11:: ${preference.getUid()}")
preference.setLogin()
preference.setLogInTime()
setVProgress(false)
progress.value=false
if (data.exists()) {
val appUser = data.toObject(UserProfile::class.java)
Timber.v("UserId ${appUser?.uId}")
preference.saveProfile(appUser!!)
//if device id is not same,send new_user_logged type notification to the token
checkLastDevice(appUser)
}
userProfileGot.value=true
}.addOnFailureListener { e ->
setVProgress(false)
progress.value=false
context.toast(e.message.toString())
}
}
private fun checkLastDevice(appUser: UserProfile?) {
try {
if (appUser!=null){
val localDevice = UserUtils.getDeviceId(context)
val deviceDetails=appUser.deviceDetails
val sameDevice=deviceDetails?.device_id.equals(localDevice)
if (!sameDevice)
UserUtils.sendPush(context,TYPE_LOGGED_IN,"", appUser.token,appUser.uId!!)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun clearAll(){
userProfileGot.value=false
logInRepo.clearOldAuth()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/login/LoginRepo.kt
================================================
package com.gowtham.letschat.fragments.login
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.google.android.gms.tasks.Task
import com.google.firebase.FirebaseException
import com.google.firebase.auth.*
import com.gowtham.letschat.models.Country
import com.gowtham.letschat.ui.activities.MainActivity
import com.gowtham.letschat.utils.LogInFailedState
import com.gowtham.letschat.utils.printMeD
import com.gowtham.letschat.utils.toast
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class LoginRepo @Inject constructor(@ActivityRetainedScoped val actContxt: MainActivity,
@ApplicationContext val context: Context) :
PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
private val verificationId: MutableLiveData = MutableLiveData()
private val credential: MutableLiveData = MutableLiveData()
private val taskResult: MutableLiveData> = MutableLiveData()
private val failedState: MutableLiveData = MutableLiveData()
private val auth = FirebaseAuth.getInstance()
init {
"LoginRepo init".printMeD()
}
fun setMobile(country: Country, mobile: String) {
Timber.v("Mobile $mobile")
val number = country.noCode + " " + mobile
val options = PhoneAuthOptions.newBuilder(auth)
.setPhoneNumber(number)
.setTimeout(60L, TimeUnit.SECONDS)
.setActivity(actContxt)
.setCallbacks(this)
.build()
PhoneAuthProvider.verifyPhoneNumber(options)
}
override fun onVerificationCompleted(credential: PhoneAuthCredential) {
Timber.v("onVerificationCompleted:$credential")
this.credential.value = credential
Handler(Looper.getMainLooper()).postDelayed({
signInWithPhoneAuthCredential(credential)
}, 1000)
}
override fun onVerificationFailed(exp: FirebaseException) {
"onVerficationFailed:: ${exp.message}".printMeD()
failedState.value = LogInFailedState.Verification
when (exp) {
is FirebaseAuthInvalidCredentialsException ->
context.toast("Invalid Request")
else -> context.toast(exp.message.toString())
}
}
override fun onCodeSent(verificationId: String, token: PhoneAuthProvider.ForceResendingToken) {
Timber.v("onCodeSent:$verificationId")
this.verificationId.value = verificationId
context.toast("Verification code sent successfully")
}
private fun signInWithPhoneAuthCredential(credential: PhoneAuthCredential) {
FirebaseAuth.getInstance().signInWithCredential(credential)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
Timber.v("signInWithCredential:success")
taskResult.value = task
} else {
Timber.v("signInWithCredential:failure ${task.exception}")
if (task.exception is FirebaseAuthInvalidCredentialsException)
context.toast("Invalid verification code!")
failedState.value = LogInFailedState.SignIn
}
}
}
fun setCredential(credential: PhoneAuthCredential) {
signInWithPhoneAuthCredential(credential)
}
fun getVCode(): MutableLiveData {
return verificationId
}
fun setVCodeNull() {
verificationId.value = null
}
fun clearOldAuth(){
credential.value=null
taskResult.value=null
}
fun getCredential(): LiveData {
return credential
}
fun getTaskResult(): LiveData> {
return taskResult
}
fun getFailed(): LiveData {
return failedState
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/myprofile/FMyProfile.kt
================================================
package com.gowtham.letschat.fragments.myprofile
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.canhub.cropper.CropImage
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.AlertLogoutBinding
import com.gowtham.letschat.databinding.FMyProfileBinding
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.views.CustomProgressView
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class FMyProfile : Fragment(R.layout.f_my_profile) {
private lateinit var binding: FMyProfileBinding
@Inject
lateinit var preferenec: MPreference
@Inject
lateinit var db: ChatUserDatabase
private lateinit var dialog: Dialog
private val viewModel: FMyProfileViewModel by viewModels()
private lateinit var context: Activity
private var progressView: CustomProgressView? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FMyProfileBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context = requireActivity()
progressView = CustomProgressView(context)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.imageProfile.setOnClickListener {
ImageUtils.askPermission(this)
}
binding.btnSaveChanges.setOnClickListener {
val newName = viewModel.userName.value
val about = viewModel.about.value
val image=viewModel.imageUrl.value
when {
viewModel.isUploading.value!! -> context.toast("Profile picture is uploading!")
newName.isNullOrBlank() -> context.toast("User name can't be empty!")
else -> {
context.window.decorView.clearFocus()
viewModel.saveChanges(newName,about ?: "" ,image ?: "")
}
}
}
binding.btnLogout.setOnClickListener {
dialog.show()
}
initDialog()
subscribeObservers()
}
private fun subscribeObservers() {
viewModel.profileUpdateState.observe(viewLifecycleOwner, {
if (it is LoadState.OnLoading) {
progressView?.show()
} else
progressView?.dismiss()
})
}
private fun initDialog() {
try {
dialog = Dialog(requireContext())
val layoutBinder = AlertLogoutBinding.inflate(layoutInflater)
dialog.setContentView(layoutBinder.root)
dialog.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
layoutBinder.txtOk.setOnClickListener {
dialog.dismiss()
UserUtils.logOut(requireActivity(), preferenec, db)
}
layoutBinder.txtCancel.setOnClickListener {
dialog.dismiss()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE)
onCropResult(data)
else
ImageUtils.cropImage(context, data, true)
}
private fun onCropResult(data: Intent?) {
try {
val imagePath: Uri? = ImageUtils.getCroppedImage(data)
imagePath?.let {
viewModel.uploadProfileImage(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
ImageUtils.onImagePerResult(this, *grantResults)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/myprofile/FMyProfileViewModel.kt
================================================
package com.gowtham.letschat.fragments.myprofile
import android.content.Context
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.android.gms.tasks.OnFailureListener
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.SetOptions
import com.google.firebase.storage.UploadTask
import com.gowtham.letschat.utils.LoadState
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.toast
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
import java.util.*
import javax.inject.Inject
@HiltViewModel
class FMyProfileViewModel @Inject constructor(
@ApplicationContext private val context: Context,
private val preference: MPreference
) : ViewModel() {
private var userProfile = preference.getUserProfile()
val userName = MutableLiveData(userProfile?.userName)
val imageUrl = MutableLiveData(userProfile?.image)
val about = MutableLiveData(userProfile?.about)
val isUploading = MutableLiveData(false)
private val mobileData = userProfile?.mobile
private val storageRef = UserUtils.getStorageRef(context)
private val docuRef = UserUtils.getDocumentRef(context)
val mobile = MutableLiveData("${mobileData?.country} ${mobileData?.number}")
val profileUpdateState = MutableLiveData()
private lateinit var uploadTask: UploadTask
init {
Timber.v("FMyProfileViewModel init")
}
fun uploadProfileImage(imagePath: Uri) {
try {
isUploading.value = true
val child = storageRef.child("profile_picture_${System.currentTimeMillis()}.jpg")
if (this::uploadTask.isInitialized && uploadTask.isInProgress)
uploadTask.cancel()
uploadTask = child.putFile(imagePath)
uploadTask.addOnSuccessListener {
child.downloadUrl.addOnCompleteListener { taskResult ->
isUploading.value = false
imageUrl.value = taskResult.result.toString()
}.addOnFailureListener {
OnFailureListener { e ->
isUploading.value = false
context.toast(e.message.toString())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun saveChanges(name: String, strAbout: String, image: String) {
name.toLowerCase(Locale.getDefault())
updateProfileData(name, strAbout, image)
}
private fun updateProfileData(name: String, strAbout: String, image: String) {
try {
profileUpdateState.value = LoadState.OnLoading
val profile = userProfile!!
profile.userName = name
profile.about = strAbout
profile.image = image
profile.updatedAt = System.currentTimeMillis()
docuRef.set(profile, SetOptions.merge()).addOnSuccessListener {
context.toast("Profile updated!")
userProfile = profile
preference.saveProfile(profile)
profileUpdateState.value = LoadState.OnSuccess()
}.addOnFailureListener { e ->
context.toast(e.message.toString())
profileUpdateState.value = LoadState.OnFailure(e)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onCleared() {
super.onCleared()
if (this::uploadTask.isInitialized && uploadTask.isInProgress)
uploadTask.cancel()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/profile/FProfile.kt
================================================
package com.gowtham.letschat.fragments.profile
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.canhub.cropper.CropImage
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FProfileBinding
import com.gowtham.letschat.databinding.FVerifyBinding
import com.gowtham.letschat.models.UserStatus
import com.gowtham.letschat.ui.activities.MainActivity
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.views.CustomProgressView
import dagger.hilt.android.AndroidEntryPoint
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
@AndroidEntryPoint
class FProfile : Fragment() {
private lateinit var binding: FProfileBinding
private lateinit var context: Activity
@Inject
lateinit var preference: MPreference
@Inject
lateinit var userCollection: CollectionReference
private var progressView: CustomProgressView? = null
private val viewModel: ProfileViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = FProfileBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context = requireActivity()
UserUtils.updatePushToken(context,userCollection,true)
EventBus.getDefault().post(UserStatus())
binding.lifecycleOwner = viewLifecycleOwner
binding.viewmodel = viewModel
progressView = CustomProgressView(context)
binding.imgProPic.setOnClickListener { ImageUtils.askPermission(this) }
binding.fab.setOnClickListener { validate() }
subscribeObservers()
}
private fun subscribeObservers() {
viewModel.progressProPic.observe(viewLifecycleOwner, { uploaded ->
binding.progressPro.toggle(uploaded)
})
viewModel.profileUpdateState.observe(viewLifecycleOwner, {
when (it) {
is LoadState.OnSuccess -> {
if (findNavController().isValidDestination(R.id.FProfile)) {
progressView?.dismiss()
findNavController().navigate(R.id.action_FProfile_to_FSingleChatHome)
}
}
is LoadState.OnFailure -> {
progressView?.dismiss()
}
is LoadState.OnLoading -> {
progressView?.show()
}
}
})
viewModel.checkUserNameState.observe(viewLifecycleOwner,{
when (it) {
is LoadState.OnFailure -> {
progressView?.dismiss()
}
is LoadState.OnLoading -> {
progressView?.show()
}
}
})
}
private fun validate() {
val name = viewModel.name.value
if (!name.isNullOrEmpty() && name.length > 1 && !viewModel.progressProPic.value!!)
viewModel.storeProfileData()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE)
onCropResult(data)
else
ImageUtils.cropImage(context, data, true)
}
private fun onCropResult(data: Intent?) {
try {
val imagePath: Uri? = ImageUtils.getCroppedImage(data)
imagePath?.let {
viewModel.uploadProfileImage(it)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
ImageUtils.onImagePerResult(this, *grantResults)
}
override fun onDestroy() {
try {
progressView?.dismissIfShowing()
super.onDestroy()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/profile/ProfileViewModel.kt
================================================
package com.gowtham.letschat.fragments.profile
import android.content.Context
import android.net.Uri
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.google.android.gms.tasks.OnFailureListener
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.SetOptions
import com.gowtham.letschat.models.ModelDeviceDetails
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.*
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.util.*
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject
constructor(
@ApplicationContext private val context: Context,
private val preference: MPreference
) : ViewModel() {
val progressProPic = MutableLiveData(false)
val profileUpdateState = MutableLiveData()
val checkUserNameState = MutableLiveData()
val name = MutableLiveData("")
private val storageRef = UserUtils.getStorageRef(context)
private val docuRef = UserUtils.getDocumentRef(context)
val profilePicUrl = MutableLiveData("")
private var about = ""
private var createdAt: Long = System.currentTimeMillis()
init {
LogMessage.v("ProfileViewModel")
val userProfile = preference.getUserProfile()
userProfile?.let {
name.value = userProfile.userName
profilePicUrl.value = userProfile.image
about = userProfile.about
createdAt = userProfile.createdAt ?: System.currentTimeMillis()
}
}
fun uploadProfileImage(imagePath: Uri) {
try {
progressProPic.value = true
val child = storageRef.child("profile_picture_${System.currentTimeMillis()}.jpg")
val task = child.putFile(imagePath)
task.addOnSuccessListener {
child.downloadUrl.addOnCompleteListener { taskResult ->
progressProPic.value = false
profilePicUrl.value = taskResult.result.toString()
}.addOnFailureListener {
OnFailureListener { e ->
progressProPic.value = false
context.toast(e.message.toString())
}
}
}.addOnProgressListener { taskSnapshot ->
val progress: Double =
100.0 * taskSnapshot.bytesTransferred / taskSnapshot.totalByteCount
}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun storeProfileData() {
try {
profileUpdateState.value = LoadState.OnLoading
val profile = UserProfile(
preference.getUid()!!,
createdAt,
System.currentTimeMillis(),
profilePicUrl.value!!,
name.value!!.toLowerCase(Locale.getDefault()),
about,
mobile = preference.getMobile(),
token = preference.getPushToken().toString(),
deviceDetails =
Json.decodeFromString(
UserUtils.getDeviceInfo(context).toString()
)
)
docuRef.set(profile, SetOptions.merge()).addOnSuccessListener {
preference.saveProfile(profile)
profileUpdateState.value = LoadState.OnSuccess()
}.addOnFailureListener { e ->
context.toast(e.message.toString())
profileUpdateState.value = LoadState.OnFailure(e)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onCleared() {
LogMessage.v("ProfileViewModel Cleared")
super.onCleared()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/search/FSearch.kt
================================================
package com.gowtham.letschat.fragments.search
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.FSearchBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.fragments.contacts.AdContact
import com.gowtham.letschat.fragments.single_chat_home.FSingleChatHomeDirections
import com.gowtham.letschat.ui.activities.SharedViewModel
import com.gowtham.letschat.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.util.*
import javax.inject.Inject
@AndroidEntryPoint
class FSearch : Fragment(R.layout.f_search), ItemClickListener {
private lateinit var binding: FSearchBinding
private val sharedViewModel by activityViewModels()
private val viewModel: FSearchViewModel by viewModels()
private val userList = arrayListOf()
@Inject
lateinit var preference: MPreference
private val adapter: AdContact by lazy {
AdContact(requireContext(), userList)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FSearchBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
setDataInView()
subscribeObservers()
}
private fun setDataInView() {
AdContact.itemClickListener=this
binding.apply {
listUsers.setHasFixedSize(true)
listUsers.itemAnimator = null
listUsers.adapter = adapter
}
}
private fun subscribeObservers() {
sharedViewModel.getState().observe(viewLifecycleOwner, { state ->
if (state is ScreenState.IdleState) {
//show recent list
binding.txtNoUser.gone()
binding.viewEmpty.show()
} else {
if (sharedViewModel.lastQuery.value.isNullOrBlank()) {
binding.viewEmpty.show()
binding.txtNoUser.gone()
}
}
})
lifecycleScope.launch {
viewModel.getCachedList().collect { listData ->
Timber.v("List data $listData")
//can be used to show recently searched user list
}
}
viewModel.getLoadState().observe(viewLifecycleOwner, { state ->
userList.clear()
adapter.notifyDataSetChanged()
when (state) {
is LoadState.OnLoading -> {
binding.apply {
txtNoUser.gone()
viewEmpty.gone()
progressBar.show()
}
}
is LoadState.OnSuccess -> {
binding.progressBar.gone()
val list = state.data as List
if (list.isEmpty()) {
binding.apply {
txtNoUser.show()
viewEmpty.gone()
}
} else {
binding.apply {
txtNoUser.gone()
viewEmpty.gone()
}
}
userList.addAll(list)
adapter.notifyDataSetChanged()
}
is LoadState.OnFailure -> {
binding.apply {
progressBar.gone()
txtNoUser.show()
}
}
}
})
sharedViewModel.lastQuery.observe(viewLifecycleOwner, {
if (sharedViewModel.getState().value is ScreenState.SearchState) {
if (it.isBlank()) {
binding.apply {
viewEmpty.show()
txtNoUser.gone()
userList.clear()
userList.addAll(emptyList())
adapter.notifyDataSetChanged()
}
} else
viewModel.makeQuery(it.toLowerCase(Locale.getDefault()))
}
})
}
override fun onItemClicked(v: View, position: Int) {
val chatUser=userList[position]
preference.setCurrentUser(chatUser.id)
val action=FSearchDirections.actionFSearchToFSingleChat(chatUser)
findNavController().navigate(action)
}
override fun onDestroyView() {
super.onDestroyView()
// viewModel.clearCachedUser()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/search/FSearchViewModel.kt
================================================
package com.gowtham.letschat.fragments.search
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.gowtham.letschat.utils.Constants
import com.gowtham.letschat.utils.DataStorePreference
import com.gowtham.letschat.utils.LoadState
import com.gowtham.letschat.utils.LogMessage
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class FSearchViewModel @Inject constructor(repository: SearchRepo,
private val dataStorePreference: DataStorePreference): ViewModel() {
private val searchHandler = Handler(Looper.getMainLooper())
private var lastQuery=""
private var currentQuery=MutableLiveData()
private var _loadState=MutableLiveData()
val loadState get() = _loadState
init {
LogMessage.v("FSearchViewModel")
}
/*
val users= Transformations.switchMap(currentQuery){ query->
callMe(query)
}
private fun callMe(query: String?): LiveData {
return users
}*/
fun getCachedList() = dataStorePreference.getList()
fun makeQuery(query: String){
if(lastQuery==query)
return
lastQuery=query
removeTypingCallbacks()
searchHandler.postDelayed(queryThread, 400)
}
fun setLoadState(state: LoadState){
_loadState.value=state
}
fun getLoadState(): LiveData{
return _loadState
}
private val queryThread = Runnable {
currentQuery.value=lastQuery
repository.makeQuery(lastQuery,_loadState)
removeTypingCallbacks()
}
private fun removeTypingCallbacks() {
searchHandler.removeCallbacks(queryThread)
}
fun clearCachedUser() {
dataStorePreference.storeList(Constants.KEY_LAST_QUERIED_LIST, emptyList())
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/search/SearchRepo.kt
================================================
package com.gowtham.letschat.fragments.search
import androidx.lifecycle.MutableLiveData
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.Constants
import com.gowtham.letschat.utils.DataStorePreference
import com.gowtham.letschat.utils.LoadState
import com.gowtham.letschat.utils.MPreference
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class SearchRepo @Inject constructor(
private val usersCollection: CollectionReference,private val dataStore: DataStorePreference,
private val preference: MPreference){
fun makeQuery(query: String, loadState: MutableLiveData) {
try {
loadState.value=LoadState.OnLoading
usersCollection.whereEqualTo("userName", query).get()
.addOnSuccessListener { documents ->
val list= arrayListOf()
for (document in documents) {
val profile = document.toObject(UserProfile::class.java)
if (profile.uId==preference.getUid())
continue
val chatUser=ChatUser(profile.uId.toString(),profile.userName,profile,locallySaved = false,
isSearchedUser = true)
list.add(chatUser)
}
loadState.value=LoadState.OnSuccess(list)
dataStore.storeList(Constants.KEY_LAST_QUERIED_LIST,list)
}
.addOnFailureListener { exception ->
loadState.value=LoadState.OnFailure(exception)
Timber.wtf("Error getting documents: ${exception.message}")
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/single_chat/AdChat.kt
================================================
package com.gowtham.letschat.fragments.single_chat
import android.content.Context
import android.media.MediaPlayer
import android.net.Uri
import android.os.CountDownTimer
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.*
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.utils.Events.EventAudioMsg
import com.gowtham.letschat.utils.Events.EventUpdateRecycleItem
import org.greenrobot.eventbus.EventBus
import timber.log.Timber
import java.io.IOException
import java.util.ArrayList
import kotlin.properties.Delegates
class AdChat(private val context: Context, private val msgClickListener: ItemClickListener) :
ListAdapter(DiffCallbackMessages()) {
private val preference = MPreference(context)
companion object {
private const val TYPE_TXT_SENT = 0
private const val TYPE_TXT_RECEIVED = 1
private const val TYPE_IMG_SENT = 2
private const val TYPE_IMG_RECEIVE = 3
private const val TYPE_STICKER_SENT = 4
private const val TYPE_STICKER_RECEIVE = 5
private const val TYPE_AUDIO_SENT = 6
private const val TYPE_AUDIO_RECEIVE = 7
private var lastPlayedHolder: RowAudioSentBinding?=null
private var lastReceivedPlayedHolder: RowAudioReceiveBinding?=null
private var lastPlayedAudioId : Long=-1
private var player = MediaPlayer()
lateinit var messageList: MutableList
fun stopPlaying() {
if(player.isPlaying) {
lastReceivedPlayedHolder?.progressBar?.abandon()
lastPlayedHolder?.progressBar?.abandon()
lastReceivedPlayedHolder?.imgPlay?.setImageResource(R.drawable.ic_action_play)
lastPlayedHolder?.imgPlay?.setImageResource(R.drawable.ic_action_play)
player.apply {
stop()
reset()
EventBus.getDefault().post(EventAudioMsg(false))
}
}
}
fun isPlaying() = player.isPlaying
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (viewType) {
TYPE_TXT_SENT -> {
val binding = RowSentMessageBinding.inflate(layoutInflater, parent, false)
TxtSentVHolder(binding)
}
TYPE_TXT_RECEIVED-> {
val binding = RowReceiveMessageBinding.inflate(layoutInflater, parent, false)
TxtReceiveVHolder(binding)
}
TYPE_IMG_SENT-> {
val binding = RowImageSentBinding.inflate(layoutInflater, parent, false)
ImageSentVHolder(binding)
}
TYPE_IMG_RECEIVE-> {
val binding = RowImageReceiveBinding.inflate(layoutInflater, parent, false)
ImageReceiveVHolder(binding)
}
TYPE_STICKER_SENT-> {
val binding = RowStickerSentBinding.inflate(layoutInflater, parent, false)
StickerSentVHolder(binding)
}
TYPE_STICKER_RECEIVE-> {
val binding = RowStickerReceiveBinding.inflate(layoutInflater, parent, false)
StickerReceiveVHolder(binding)
}
TYPE_AUDIO_SENT-> {
val binding = RowAudioSentBinding.inflate(layoutInflater, parent, false)
AudioSentVHolder(binding)
}
else-> {
val binding = RowAudioReceiveBinding.inflate(layoutInflater, parent, false)
AudioReceiveVHolder(binding)
}
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when(holder){
is TxtSentVHolder ->
holder.bind(context,getItem(position))
is TxtReceiveVHolder ->
holder.bind(context,getItem(position))
is ImageSentVHolder ->
holder.bind(getItem(position),msgClickListener)
is ImageReceiveVHolder ->
holder.bind(getItem(position),msgClickListener)
is StickerSentVHolder ->
holder.bind(getItem(position))
is StickerReceiveVHolder ->
holder.bind(getItem(position))
is AudioSentVHolder ->
holder.bind(context,getItem(position))
is AudioReceiveVHolder ->
holder.bind(context,getItem(position))
}
}
override fun getItemViewType(position: Int): Int {
val message = getItem(position)
val fromMe=message.from == preference.getUid()
if (fromMe && message.type == "text")
return TYPE_TXT_SENT
else if (!fromMe && message.type == "text")
return TYPE_TXT_RECEIVED
else if (fromMe && message.type == "image" && message.imageMessage?.imageType=="image")
return TYPE_IMG_SENT
else if (!fromMe && message.type == "image" && message.imageMessage?.imageType=="image")
return TYPE_IMG_RECEIVE
else if (fromMe && message.type == "image" && (message.imageMessage?.imageType=="sticker"
|| message.imageMessage?.imageType=="gif"))
return TYPE_STICKER_SENT
else if (!fromMe && message.type == "image" && (message.imageMessage?.imageType=="sticker"
|| message.imageMessage?.imageType=="gif"))
return TYPE_STICKER_RECEIVE
else if (fromMe && message.type == "audio")
return TYPE_AUDIO_SENT
else if (!fromMe && message.type == "audio")
return TYPE_AUDIO_RECEIVE
return super.getItemViewType(position)
}
class TxtSentVHolder(val binding: RowSentMessageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(context: Context,item: Message) {
binding.message = item
binding.messageList= messageList as ArrayList
if (bindingAdapterPosition>0) {
val message = messageList[bindingAdapterPosition - 1]
if (message.from == item.from)
binding.txtMsg.setBackgroundResource(R.drawable.shape_send_msg_corned)
}
binding.executePendingBindings()
}
}
class TxtReceiveVHolder(val binding: RowReceiveMessageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(context:Context,item: Message) {
binding.message = item
if (bindingAdapterPosition>0) {
val message = messageList[bindingAdapterPosition - 1]
if (message.from == item.from)
binding.txtMsg.setBackgroundResource(R.drawable.shape_receive_msg_corned)
}
binding.executePendingBindings()
}
}
class ImageSentVHolder(val binding: RowImageSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Message, msgClickListener: ItemClickListener) {
binding.message = item
binding.imageMsg.setOnClickListener {
msgClickListener.onItemClicked(it,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
class ImageReceiveVHolder(val binding: RowImageReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Message,msgClickListener: ItemClickListener) {
binding.message = item
binding.imageMsg.setOnClickListener {
msgClickListener.onItemClicked(it,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
class StickerSentVHolder(val binding: RowStickerSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Message) {
binding.message = item
binding.executePendingBindings()
}
}
class StickerReceiveVHolder(val binding: RowStickerReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Message) {
binding.message = item
binding.executePendingBindings()
}
}
class AudioReceiveVHolder(val binding: RowAudioReceiveBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(context: Context,item: Message) {
binding.message = item
binding.progressBar.setStoriesCountDebug(1,0)
binding.progressBar.setAllStoryDuration(item.audioMessage?.duration!!.toLong()*1000)
binding.imgPlay.setOnClickListener {
startPlaying(
context,
item,
binding)
}
binding.executePendingBindings()
}
private fun startPlaying(
context: Context,
item: Message,
currentHolder: RowAudioReceiveBinding) {
if (player.isPlaying){
stopPlaying()
lastReceivedPlayedHolder?.progressBar?.abandon()
lastReceivedPlayedHolder?.imgPlay?.setImageResource(R.drawable.ic_action_play)
lastPlayedHolder?.imgPlay?.setImageResource(R.drawable.ic_action_play)
lastPlayedHolder?.progressBar?.abandon()
if (lastPlayedAudioId==item.createdAt)
return
}
player= MediaPlayer()
lastReceivedPlayedHolder =currentHolder
lastPlayedAudioId=item.createdAt
currentHolder.progressBuffer.show()
currentHolder.imgPlay.gone()
player.apply {
try {
setDataSource(context, Uri.parse(item.audioMessage?.uri))
prepareAsync()
setOnPreparedListener {
Timber.v("Started..")
start()
currentHolder.progressBuffer.gone()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_stop)
currentHolder.imgPlay.show()
currentHolder.progressBar.startStories()
EventBus.getDefault().post(EventAudioMsg(true))
}
setOnCompletionListener {
currentHolder.progressBar.abandon()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_play)
EventBus.getDefault().post(EventAudioMsg(false))
}
} catch (e: IOException) {
println("ChatFragment.startPlaying:prepare failed")
}
}
}
}
class AudioSentVHolder(val binding: RowAudioSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
context: Context,
item: Message,) {
binding.message = item
binding.progressBar.setStoriesCountDebug(1,0)
binding.progressBar.setAllStoryDuration(item.audioMessage?.duration!!.toLong()*1000)
binding.imgPlay.setOnClickListener {
startPlaying(
context,
item,
binding)
}
binding.executePendingBindings()
}
private fun startPlaying(
context: Context,
item: Message,
currentHolder: RowAudioSentBinding) {
if (player.isPlaying){
stopPlaying()
if (lastPlayedAudioId==item.createdAt)
return
}
player= MediaPlayer()
lastPlayedHolder =currentHolder
lastPlayedAudioId=item.createdAt
currentHolder.progressBuffer.show()
currentHolder.imgPlay.gone()
player.apply {
try {
setDataSource(context, Uri.parse(item.audioMessage?.uri))
prepareAsync()
setOnPreparedListener {
Timber.v("Started..")
start()
currentHolder.progressBuffer.gone()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_stop)
currentHolder.imgPlay.show()
currentHolder.progressBar.startStories()
EventBus.getDefault().post(EventAudioMsg(true))
}
setOnCompletionListener {
currentHolder.progressBar.abandon()
currentHolder.imgPlay.setImageResource(R.drawable.ic_action_play)
EventBus.getDefault().post(EventAudioMsg(false))
}
} catch (e: IOException) {
println("ChatFragment.startPlaying:prepare failed")
}
}
}
}
}
class DiffCallbackMessages : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
return oldItem.createdAt == newItem.createdAt
}
override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
return oldItem == newItem
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/single_chat/FSingleChat.kt
================================================
package com.gowtham.letschat.fragments.single_chat
import android.Manifest
import android.animation.Animator
import android.app.Activity
import android.content.Intent
import android.content.pm.ActivityInfo
import android.media.MediaRecorder
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.ContactsContract
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.inputmethod.InputContentInfoCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import com.canhub.cropper.CropImage
import com.gowtham.letschat.databinding.FSingleChatBinding
import com.gowtham.letschat.db.data.*
import com.gowtham.letschat.fragments.FAttachment
import com.gowtham.letschat.models.MyImage
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.*
import com.gowtham.letschat.utils.Events.EventAudioMsg
import com.gowtham.letschat.utils.Utils.edtValue
import com.gowtham.letschat.views.CustomEditText
import com.stfalcon.imageviewer.StfalconImageViewer
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
import java.io.IOException
import java.util.*
import javax.inject.Inject
import kotlin.collections.ArrayList
@AndroidEntryPoint
class FSingleChat : Fragment(), ItemClickListener,CustomEditText.KeyBoardInputCallbackListener {
private lateinit var binding: FSingleChatBinding
@Inject
lateinit var preference: MPreference
private lateinit var chatUser: ChatUser
private lateinit var fromUser: UserProfile
private lateinit var toUser: UserProfile
private var messageList = mutableListOf()
private val viewModel: SingleChatViewModel by viewModels()
private lateinit var localUserId: String
private var recorder: MediaRecorder? = null
val args by navArgs()
private lateinit var manager: LinearLayoutManager
private lateinit var chatUserId: String
var isRecording = false //whether is recoding now or not
private var recordStart = 0L
private var recordDuration = 0L
private val REQ_AUDIO_PERMISSION=29
private var lastAudioFile=""
private var msgPostponed=false
private val adChat: AdChat by lazy {
AdChat(requireContext(), this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = FSingleChatBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewmodel=viewModel
chatUser= args.chatUserProfile!!
viewModel.setUnReadCountZero(chatUser)
setListeners()
if(!chatUser.locallySaved && !chatUser.isSearchedUser)
binding.viewChatHeader.imageAddContact.show()
viewModel.canScroll(false)
binding.viewChatBtm.edtMsg.setKeyBoardInputCallbackListener(this)
setDataInView()
subscribeObservers()
lifecycleScope.launch {
viewModel.getMessagesByChatUserId(chatUserId).collect { mMessagesList ->
if(mMessagesList.isEmpty())
return@collect
messageList = mMessagesList as MutableList
if(AdChat.isPlaying()){
msgPostponed=true
return@collect
}
AdChat.messageList = messageList
adChat.submitList(mMessagesList)
//scroll to last items in recycler (recent messages)
if (messageList.isNotEmpty()) {
if (viewModel.getCanScroll()) //scroll only if new message arrived
binding.listMessage.smoothScrollToPos(messageList.lastIndex)
else
viewModel.canScroll(true)
}
}
}
}
private fun setListeners() {
binding.viewChatBtm.lottieSend.setOnClickListener {
sendMessage()
}
binding.viewChatHeader.viewBack.setOnClickListener {
findNavController().popBackStack()
}
binding.viewChatBtm.imgRecord.setOnClickListener {
AdChat.stopPlaying()
if(Utils.checkPermission(this, Manifest.permission.RECORD_AUDIO,reqCode = REQ_AUDIO_PERMISSION))
startRecording()
}
binding.lottieVoice.setOnClickListener {
if (isRecording){
stopRecording()
val duration=(recordDuration/1000).toInt()
if (duration<=1) {
requireContext().toast("Nothing is recorded!")
return@setOnClickListener
}
val msg=createMessage().apply {
type="audio"
audioMessage= AudioMessage(lastAudioFile,duration)
chatUsers= ArrayList()
}
viewModel.uploadToCloud(msg,lastAudioFile)
}
}
binding.viewChatHeader.imageAddContact.setOnClickListener {
if (Utils.askContactPermission(this))
openSaveIntent()
}
binding.viewChatBtm.imageAdd.setOnClickListener {
val fragment=FAttachment.newInstance(Bundle())
fragment.show(childFragmentManager,"")
}
}
private fun setDataInView() {
try {
fromUser = preference.getUserProfile()!!
localUserId=fromUser.uId!!
manager= LinearLayoutManager(context)
binding.listMessage.apply {
manager.stackFromEnd=true
layoutManager=manager
setHasFixedSize(true)
isNestedScrollingEnabled=false
itemAnimator = null
}
binding.listMessage.adapter = adChat
adChat.addRestorePolicy()
viewModel.setChatUser(chatUser)
toUser=chatUser.user
chatUserId=toUser.uId!!
binding.chatUser = chatUser
binding.viewChatBtm.edtMsg.addTextChangedListener(msgTxtChangeListener)
binding.viewChatBtm.lottieSend.addAnimatorListener(object : Animator.AnimatorListener {
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
super.onAnimationEnd(animation, isReverse)
}
override fun onAnimationEnd(p0: Animator?) {
if (edtValue(binding.viewChatBtm.edtMsg).isEmpty()) {
binding.viewChatBtm.imgRecord.show()
binding.viewChatBtm.lottieSend.gone()
}
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationRepeat(p0: Animator?) {
}
})
binding.lottieVoice.addAnimatorListener(object : Animator.AnimatorListener{
override fun onAnimationStart(p0: Animator?) {
}
override fun onAnimationEnd(animation: Animator?, isReverse: Boolean) {
super.onAnimationEnd(animation, isReverse)
}
override fun onAnimationEnd(p0: Animator?) {
binding.viewChatBtm.imgRecord.show()
binding.lottieVoice.gone()
}
override fun onAnimationCancel(p0: Animator?) {
}
override fun onAnimationRepeat(p0: Animator?) {
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun subscribeObservers() {
//pass messages list for recycler to show
viewModel.chatUserOnlineStatus.observe(viewLifecycleOwner, {
Utils.setOnlineStatus(binding.viewChatHeader.txtLastSeen, it, localUserId)
})
}
private fun openSaveIntent() {
val contactIntent = Intent(ContactsContract.Intents.Insert.ACTION)
contactIntent.type = ContactsContract.RawContacts.CONTENT_TYPE
contactIntent
.putExtra(ContactsContract.Intents.Insert.NAME, chatUser.user.userName)
.putExtra(ContactsContract.Intents.Insert.PHONE, chatUser.user.mobile?.number.toString())
startActivityForResult(contactIntent, REQ_ADD_CONTACT)
}
private fun sendMessage() {
val msg = edtValue(binding.viewChatBtm.edtMsg)
if (msg.isEmpty())
return
binding.viewChatBtm.lottieSend.playAnimation()
val message = createMessage().apply {
textMessage=TextMessage(msg)
chatUsers= ArrayList()
}
viewModel.sendMessage(message)
binding.viewChatBtm.edtMsg.setText("")
}
private fun createMessage(): Message {
return Message(
System.currentTimeMillis(),
from = preference.getUid().toString(),
chatUserId=chatUserId,
to = toUser.uId!!, senderName = fromUser.userName,
senderImage = fromUser.image
)
}
private val msgTxtChangeListener=object : TextWatcher{
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
viewModel.sendTyping(binding.viewChatBtm.edtMsg.trim())
if(binding.viewChatBtm.lottieSend.isAnimating)
return
if(s.isNullOrBlank()) {
binding.viewChatBtm.imgRecord.show()
binding.viewChatBtm.lottieSend.hide()
}
else{
binding.viewChatBtm.lottieSend.show()
binding.viewChatBtm.imgRecord.hide()
}
}
override fun afterTextChanged(s: Editable?) {
}
}
override fun onItemClicked(v: View, position: Int) {
val message=messageList.get(position)
if (message.type=="image" && message.imageMessage!!.imageType=="image") {
binding.fullSizeImageView.show()
StfalconImageViewer.Builder(
context,
listOf(MyImage(messageList.get(position).imageMessage?.uri!!))
) { imageView, myImage ->
ImageUtils.loadGalleryImage(myImage.url, imageView)
}
.withDismissListener { binding.fullSizeImageView.visibility = View.GONE }
.show()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_ADD_CONTACT){
if (resultCode == Activity.RESULT_OK) {
binding.viewChatHeader.imageAddContact.gone()
val contacts=UserUtils.fetchContacts(requireContext())
val savedName=contacts.firstOrNull { it.mobile==chatUser.user.mobile?.number }
savedName?.let {
binding.viewChatHeader.txtLocalName.text=it.name
chatUser.localName=it.name
chatUser.locallySaved=true
viewModel.insertUser(chatUser)
}
}else if (resultCode == Activity.RESULT_CANCELED) {
Timber.v("Cancelled Added Contact")
}
}else if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE)
onCropResult(data)
else
ImageUtils.cropImage(requireActivity(), data, true)
}
private fun onCropResult(data: Intent?) {
try {
val imagePath: Uri? = ImageUtils.getCroppedImage(data)
if (imagePath!=null){
val message=createMessage().apply {
type="image"
imageMessage=ImageMessage(imagePath.toString())
chatUsers= ArrayList()
}
viewModel.uploadToCloud(message,imagePath.toString())
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode==REQ_AUDIO_PERMISSION){
if (Utils.isPermissionOk(*grantResults))
startRecording()
else
requireActivity().toast("Audio permission is needed!")
}else if (Utils.isPermissionOk(*grantResults))
openSaveIntent()
}
override fun onCommitContent(inputContentInfo: InputContentInfoCompat?,
flags: Int,
opts: Bundle?) {
val imageMsg=createMessage()
val image=ImageMessage("${inputContentInfo?.contentUri}")
image.imageType=if(image.uri.toString().endsWith(".png")) "sticker" else "gif"
imageMsg.apply {
type="image"
imageMessage=image
chatUsers= ArrayList()
}
viewModel.uploadToCloud(imageMsg,image.toString())
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onAttachmentItemClicked(event: BottomSheetEvent){
when(event.position){
0->{
ImageUtils.takePhoto(requireActivity())
}
1->{
ImageUtils.chooseGallery(requireActivity())
}
2->{
//create intent for gallery video
}
3->{
//create intent for camera video
}
}
}
private fun startRecording() {
binding.lottieVoice.show()
binding.lottieVoice.playAnimation()
binding.viewChatBtm.edtMsg.apply {
isEnabled=false
hint="Recording..."
}
onAudioEvent(EventAudioMsg(true))
//name of the file where record will be stored
lastAudioFile=
"${requireActivity().externalCacheDir?.absolutePath}/audiorecord${System.currentTimeMillis()}.mp3"
recorder = MediaRecorder().apply {
setAudioSource(MediaRecorder.AudioSource.MIC)
setOutputFormat(MediaRecorder.OutputFormat.DEFAULT)
setOutputFile(lastAudioFile)
setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB)
try {
prepare()
} catch (e: IOException) {
println("ChatFragment.startRecording${e.message}")
}
start()
isRecording=true
recordStart = Date().time
}
Handler(Looper.getMainLooper()).postDelayed({
binding.lottieVoice.pauseAnimation()
},800)
}
private fun stopRecording() {
onAudioEvent(EventAudioMsg(false))
binding.viewChatBtm.edtMsg.apply {
isEnabled=true
hint="Type Something..."
}
Handler(Looper.getMainLooper()).postDelayed({
binding.lottieVoice.resumeAnimation()
},200)
recorder?.apply {
stop()
release()
recorder = null
}
isRecording=false
recordDuration = Date().time - recordStart
}
override fun onResume() {
viewModel.setSeenAllMessage()
preference.setCurrentUser(chatUserId)
viewModel.sendCachedTxtMesssages()
Utils.removeNotification(requireContext())
super.onResume()
}
override fun onDestroy() {
Utils.closeKeyBoard(requireActivity())
super.onDestroy()
}
override fun onStop() {
super.onStop()
preference.clearCurrentUser()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
}
override fun onDestroyView() {
super.onDestroyView()
stopRecording()
AdChat.stopPlaying()
EventBus.getDefault().unregister(this)
}
@Subscribe
fun onAudioEvent(audioEvent: EventAudioMsg){
if (audioEvent.isPlaying){
//lock current orientation
val currentOrientation=requireActivity().resources.configuration.orientation
requireActivity().requestedOrientation = currentOrientation
}else {
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
if (msgPostponed){
//refresh list
AdChat.messageList = messageList
adChat.submitList(messageList)
msgPostponed=false
}
}
}
companion object{
private const val REQ_ADD_CONTACT=22
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/single_chat/SingleChatViewModel.kt
================================================
package com.gowtham.letschat.fragments.single_chat
import android.content.Context
import android.os.Handler
import android.os.Looper
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkRequest
import com.google.firebase.database.*
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.firestore.FirebaseFirestore
import com.google.gson.reflect.TypeToken
import com.gowtham.letschat.TYPE_NEW_MESSAGE
import com.gowtham.letschat.core.MessageSender
import com.gowtham.letschat.core.MessageStatusUpdater
import com.gowtham.letschat.core.OnMessageResponse
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.di.MessageCollection
import com.gowtham.letschat.models.UserStatus
import com.gowtham.letschat.services.UploadWorker
import com.gowtham.letschat.utils.Constants.CHAT_USER_DATA
import com.gowtham.letschat.utils.Constants.MESSAGE_DATA
import com.gowtham.letschat.utils.Constants.MESSAGE_FILE_URI
import com.gowtham.letschat.utils.LogMessage
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import com.gowtham.letschat.utils.Utils
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import javax.inject.Inject
import kotlin.reflect.full.memberProperties
@HiltViewModel
class SingleChatViewModel @Inject
constructor(
@ApplicationContext private val context: Context,
private val dbRepository: DbRepository,
@MessageCollection
private val messageCollection: CollectionReference,
private val preference: MPreference,
private val firebaseFireStore: FirebaseFirestore,
) : ViewModel() {
private val database = FirebaseDatabase.getInstance()
private val toUser = preference.getOnlineUser()
private val fromUser = preference.getUid()
val message = MutableLiveData()
private val statusRef: DatabaseReference = database.getReference("Users/$toUser")
private var statusListener: ValueEventListener? = null
val chatUserOnlineStatus = MutableLiveData(UserStatus())
private val messageStatusUpdater=MessageStatusUpdater(messageCollection,firebaseFireStore)
private lateinit var chatUser: ChatUser
private val typingHandler = Handler(Looper.getMainLooper())
private var isTyping = false
private var canScroll = false
private var chatUserOnline = false
init {
statusListener = statusRef.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val userStatus = snapshot.getValue(UserStatus::class.java)
chatUserOnlineStatus.value = userStatus
chatUserOnline = userStatus?.status == "online"
}
override fun onCancelled(error: DatabaseError) {
}
})
}
fun setChatUser(chatUser: ChatUser) {
if (!this::chatUser.isInitialized) {
this.chatUser = chatUser
setSeenAllMessage()
}
}
fun canScroll(can: Boolean) {
canScroll = can
}
fun getCanScroll() = canScroll
fun getMessagesByChatUserId(chatUserId: String) =
dbRepository.getMessagesByChatUserId(chatUserId)
fun sendMessage(message: Message) {
Handler(Looper.getMainLooper()).postDelayed({
val messageSender = MessageSender(
messageCollection,
dbRepository,
chatUser,
messageListener
)
messageSender.checkAndSend(fromUser!!, toUser, message)
}, 400)
dbRepository.insertMessage(message)
removeTypingCallbacks()
}
fun sendCachedTxtMesssages() {
//Send msg that is not sent succesfully in last time
CoroutineScope(Dispatchers.IO).launch {
updateCacheMessges(dbRepository.getChatsOfFriend(toUser))
}
}
private suspend fun updateCacheMessges(listOfMessage: List) {
withContext(Dispatchers.Main) {
val nonSendMsgs =
listOfMessage.filter { it.from == fromUser && it.status == 0 && it.type == "text" }
LogMessage.v("nonSendMsgs Size ${nonSendMsgs.size}")
if (nonSendMsgs.isNotEmpty()) {
for (cachedMsg in nonSendMsgs) {
val messageSender = MessageSender(
messageCollection,
dbRepository,
chatUser,
messageListener
)
messageSender.checkAndSend(fromUser!!, toUser, cachedMsg)
}
}
}
}
private val messageListener = object : OnMessageResponse {
override fun onSuccess(message: Message) {
LogMessage.v("messageListener OnSuccess ${message.textMessage?.text}")
dbRepository.insertMessage(message)
if (chatUser.user.token.isNotEmpty())
UserUtils.sendPush(
context,
TYPE_NEW_MESSAGE,
Json.encodeToString(message),
chatUser.user.token,
message.to
)
}
override fun onFailed(message: Message) {
LogMessage.v("messageListener onFailed ${message.createdAt}")
dbRepository.insertMessage(message)
}
}
override fun onCleared() {
LogMessage.v("SingleChat cleared")
statusListener?.let {
statusRef.removeEventListener(it)
}
super.onCleared()
}
fun setSeenAllMessage() {
LogMessage.v("SetSeenAllMessage called")
if (this::chatUser.isInitialized) {
chatUser.unRead = 0
dbRepository.insertUser(chatUser)
viewModelScope.launch(Dispatchers.IO) {
val messageList = dbRepository.getChatsOfFriend(chatUser.id)
withContext(Dispatchers.Main){
if(messageList.isNotEmpty())
updateToSeen(messageList)
}
}
}
}
private fun updateToSeen(messageList: List) {
chatUser.documentId?.let {
messageStatusUpdater.updateToSeen(toUser, it, messageList)
}
}
fun sendTyping(edtValue: String) {
if (edtValue.isEmpty()) {
if (isTyping)
UserUtils.sendTypingStatus(database, false, fromUser!!, toUser)
isTyping = false
} else if (!isTyping) {
UserUtils.sendTypingStatus(database, true, fromUser!!, toUser)
isTyping = true
removeTypingCallbacks()
typingHandler.postDelayed(typingThread, 4000)
}
}
private val typingThread = Runnable {
isTyping = false
UserUtils.sendTypingStatus(database, false, fromUser!!, toUser)
removeTypingCallbacks()
}
private fun removeTypingCallbacks() {
typingHandler.removeCallbacks(typingThread)
}
fun setUnReadCountZero(chatUser: ChatUser) {
UserUtils.setUnReadCountZero(dbRepository, chatUser)
}
fun insertUser(chatUser: ChatUser) {
dbRepository.insertUser(chatUser)
}
fun uploadToCloud(message: Message, fileUri: String) {
try {
dbRepository.insertMessage(message)
removeTypingCallbacks()
val messageData = Json.encodeToString(message)
val chatUserData = Json.encodeToString(chatUser)
val data = Data.Builder()
.putString(MESSAGE_FILE_URI, fileUri)
.putString(MESSAGE_DATA, messageData)
.putString(CHAT_USER_DATA, chatUserData)
.build()
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder()
.setInputData(data)
.build()
WorkManager.getInstance(context).enqueue(uploadWorkRequest)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
//convert a data class to a map
fun T.serializeToMap(): Map {
return convert()
}
//convert a map to a data class
inline fun Map.toDataClass(): T {
return convert()
}
//convert an object of type I to type O
inline fun I.convert(): O {
val json = Utils.getGSONObj().toJson(this)
return Utils.getGSONObj().fromJson(json, object : TypeToken() {}.type)
}
inline fun T.asMap(): Map {
val props = T::class.memberProperties.associateBy { it.name }
return props.keys.associateWith { props[it]?.get(this) }
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/single_chat_home/AdSingleChatHome.kt
================================================
package com.gowtham.letschat.fragments.single_chat_home
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.gowtham.letschat.databinding.RowChatBinding
import com.gowtham.letschat.databinding.RowReceiveMessageBinding
import com.gowtham.letschat.databinding.RowSentMessageBinding
import com.gowtham.letschat.db.data.ChatUserWithMessages
import com.gowtham.letschat.utils.ItemClickListener
import com.gowtham.letschat.utils.MPreference
import java.util.*
class AdSingleChatHome(private val context: Context) :
ListAdapter(DiffCallbackChats()) {
private val preference = MPreference(context)
companion object {
lateinit var allChatList: MutableList
lateinit var itemClickListener: ItemClickListener
}
fun filter(query: String) {
try {
val list= mutableListOf()
if (query.isEmpty())
list.addAll(allChatList)
else {
for (contact in allChatList) {
if (contact.user.localName.toLowerCase(Locale.getDefault())
.contains(query.toLowerCase(Locale.getDefault()))) {
list.add(contact)
}
}
}
submitList(null)
submitList(list)
} catch (e: Exception) {
e.stackTrace
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RowChatBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val viewHolder=holder as ViewHolder
viewHolder.bind(getItem(position))
}
class ViewHolder(val binding: RowChatBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: ChatUserWithMessages) {
binding.chatUser = item
binding.viewRoot.setOnClickListener { v ->
itemClickListener.onItemClicked(v,bindingAdapterPosition)
}
binding.executePendingBindings()
}
}
}
class DiffCallbackChats : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: ChatUserWithMessages, newItem: ChatUserWithMessages): Boolean {
return oldItem.user.id == oldItem.user.id
}
override fun areContentsTheSame(oldItem: ChatUserWithMessages, newItem: ChatUserWithMessages): Boolean {
return oldItem.messages == newItem.messages && oldItem.user==newItem.user
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/single_chat_home/FSingleChatHome.kt
================================================
package com.gowtham.letschat.fragments.single_chat_home
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.gowtham.letschat.R
import com.gowtham.letschat.core.ChatHandler
import com.gowtham.letschat.core.ChatUserProfileListener
import com.gowtham.letschat.core.GroupChatHandler
import com.gowtham.letschat.databinding.FSingleChatHomeBinding
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.data.ChatUserWithMessages
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.ui.activities.SharedViewModel
import com.gowtham.letschat.utils.*
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class FSingleChatHome : Fragment(),ItemClickListener {
@Inject
lateinit var preference: MPreference
@Inject
lateinit var chatUserDao: ChatUserDao
@Inject
lateinit var messageDao: MessageDao
private lateinit var activity: Activity
private lateinit var profile: UserProfile
private var chatList = mutableListOf()
private val sharedViewModel by activityViewModels()
private lateinit var binding: FSingleChatHomeBinding
private val viewModel: SingleChatHomeViewModel by viewModels()
private val adChat: AdSingleChatHome by lazy {
AdSingleChatHome(requireContext())
}
@Inject
lateinit var chatHandler: ChatHandler
@Inject
lateinit var groupChatHandler: GroupChatHandler
@Inject
lateinit var chatUsersListener: ChatUserProfileListener
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View {
binding = FSingleChatHomeBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity()
binding.lifecycleOwner = viewLifecycleOwner
chatHandler.initHandler()
groupChatHandler.initHandler()
chatUsersListener.initListener()
profile = preference.getUserProfile()!!
setDataInView()
subScribeObservers()
}
private fun subScribeObservers() {
lifecycleScope.launch {
viewModel.getChatUsers().collect { list ->
updateList(list)
}
}
sharedViewModel.getState().observe(viewLifecycleOwner,{state->
if (state is ScreenState.IdleState){
CoroutineScope(Dispatchers.IO).launch {
updateList(viewModel.getChatUsersAsList())
}
}
})
sharedViewModel.lastQuery.observe(viewLifecycleOwner,{
if (sharedViewModel.getState().value is ScreenState.SearchState)
adChat.filter(it)
})
}
private suspend fun updateList(list: List) {
withContext(Dispatchers.Main){
val filteredList = list.filter { it.messages.isNotEmpty() }
if (filteredList.isNotEmpty()) {
binding.imageEmpty.gone()
chatList = filteredList as MutableList
//sort by recent message
chatList = filteredList.sortedByDescending { it.messages.last().createdAt }
.toMutableList()
AdSingleChatHome.allChatList=chatList
adChat.submitList(chatList)
if(sharedViewModel.getState().value is ScreenState.SearchState)
adChat.filter(sharedViewModel.lastQuery.value.toString())
}else
binding.imageEmpty.show()
}
}
private fun setDataInView() {
binding.listChat.itemAnimator = null
binding.listChat.adapter = adChat
AdSingleChatHome.itemClickListener = this
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (Utils.isPermissionOk(*grantResults)){
if (findNavController().isValidDestination(R.id.FSingleChatHome))
findNavController().navigate(R.id.action_FSingleChatHome_to_FContacts)
}
else
activity.toast("Permission is needed!")
}
override fun onItemClicked(v: View, position: Int) {
sharedViewModel.setState(ScreenState.IdleState)
val chatUser=adChat.currentList[position]
preference.setCurrentUser(chatUser.user.id)
val action= FSingleChatHomeDirections.actionFSingleChatToFChat(chatUser.user)
findNavController().navigate(action)
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/fragments/single_chat_home/SingleChatHomeViewModel.kt
================================================
package com.gowtham.letschat.fragments.single_chat_home
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.gowtham.letschat.db.DefaultDbRepo
import com.gowtham.letschat.db.data.ChatUser
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class SingleChatHomeViewModel @Inject
constructor(private val dbRepo: DefaultDbRepo): ViewModel() {
val message= MutableLiveData()
fun getChatUsers() = dbRepo.getChatUserWithMessages()
fun getChatUsersAsList() = dbRepo.getChatUserWithMessagesList()
fun insertChatUser(chatUser: ChatUser) = dbRepo.insertUser(chatUser)
fun insertMultipleChatUser(users : List) = dbRepo.insertMultipleUser(users)
fun getAllChatUser() = dbRepo.getAllChatUser()
fun deleteUser(userId: String) = dbRepo.deleteUserById(userId)
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/Contact.kt
================================================
package com.gowtham.letschat.models
data class Contact(var name: String,var mobile: String)
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/Country.kt
================================================
package com.gowtham.letschat.models
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class Country(
val code: String, val name: String, val noCode: String,
val money: String
) : Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/ModelDeviceDetails.kt
================================================
package com.gowtham.letschat.models
import android.os.Parcel
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class ModelDeviceDetails(var device_id: String?=null,var device_model: String?=null,
var device_brand: String?=null,var device_country: String?=null,
var device_os_v: String?=null,var app_version: String?=null,
var package_name: String?=null,var device_type: String?=null): Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/ModelMobile.kt
================================================
package com.gowtham.letschat.models
import android.os.Parcelable
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class ModelMobile(
var country: String="", var number: String=""): Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/MyImage.kt
================================================
package com.gowtham.letschat.models
data class MyImage(val url: String)
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/PushMsg.kt
================================================
package com.gowtham.letschat.models
import com.google.firebase.firestore.IgnoreExtraProperties
import kotlinx.serialization.Serializable
@Serializable
@IgnoreExtraProperties
data class PushMsg(var type: String?=null,var to: String?=null,var title: String?=null,
var message: String?=null,var message_body: String?=null) {
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/UserProfile.kt
================================================
package com.gowtham.letschat.models
import android.os.Parcelable
import com.google.firebase.firestore.IgnoreExtraProperties
import com.google.firebase.firestore.PropertyName
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
@Serializable
@IgnoreExtraProperties
@Parcelize
data class UserProfile(var uId: String?=null,var createdAt: Long?=null,
var updatedAt: Long?=null,
var image: String="", var userName: String="",
var about: String="",
var token :String="",
var mobile: ModelMobile?=null,
@get:PropertyName("device_details")
@set:PropertyName("device_details")
var deviceDetails: ModelDeviceDetails?=null) : Parcelable
================================================
FILE: app/src/main/java/com/gowtham/letschat/models/UserStatus.kt
================================================
package com.gowtham.letschat.models
data class UserStatus (val status: String="offline",val last_seen: Long=0,
val typing_status: String="non_typing",val chatuser: String?=null) {
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/services/GroupUploadWorker.kt
================================================
package com.gowtham.letschat.services
import android.content.Context
import android.net.Uri
import androidx.hilt.work.HiltWorker
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.TYPE_NEW_GROUP_MESSAGE
import com.gowtham.letschat.core.GroupMsgSender
import com.gowtham.letschat.core.OnGrpMessageResponse
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.db.data.GroupMessage
import com.gowtham.letschat.di.GroupCollection
import com.gowtham.letschat.utils.Constants
import com.gowtham.letschat.utils.UserUtils
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.io.FileInputStream
import java.util.concurrent.CountDownLatch
@HiltWorker
class GroupUploadWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
@GroupCollection
val groupCollection: CollectionReference,
private val dbRepository: DbRepository):
Worker(appContext, workerParams) {
private val params=workerParams
override fun doWork(): Result {
val stringData=params.inputData.getString(Constants.MESSAGE_DATA) ?: ""
val message= Json.decodeFromString(stringData)
val url=params.inputData.getString(Constants.MESSAGE_FILE_URI)!!
val sourceName=getSourceName(message,url)
val storageRef=UserUtils.getStorageRef(applicationContext)
val child = storageRef.child(
"group/${message.to}/$sourceName")
val task = if(url.contains(".mp3")) {
val stream = FileInputStream(url) //audio message
child.putStream(stream)
}else
child.putFile(Uri.parse(message.imageMessage?.uri))
val countDownLatch = CountDownLatch(1)
val result= arrayOf(Result.failure())
task.addOnSuccessListener {
child.downloadUrl.addOnCompleteListener { taskResult ->
Timber.v("TaskResult ${taskResult.result.toString()}")
val imgUrl=taskResult.result.toString()
sendMessage(message,imgUrl,result,countDownLatch)
}.addOnFailureListener { e ->
Timber.v("TaskResult Failed ${e.message}")
result[0]= Result.failure()
message.status[0]=4
dbRepository.insertMessage(message)
countDownLatch.countDown()
}
}
countDownLatch.await()
return result[0]
}
private fun sendMessage(
message: GroupMessage,imgUrl: String,
result: Array,
countDownLatch: CountDownLatch) {
val group=Json.decodeFromString(params.inputData.getString(Constants.GROUP_DATA)!!)
setUrl(message,imgUrl)
val messageSender = GroupMsgSender(groupCollection)
messageSender.sendMessage(message, group, object : OnGrpMessageResponse{
override fun onSuccess(message: GroupMessage) {
sendPushToMembers(group,message)
result[0]= Result.success()
countDownLatch.countDown()
}
override fun onFailed(message: GroupMessage) {
result[0]= Result.failure()
dbRepository.insertMessage(message)
countDownLatch.countDown()
}
})
}
private fun setUrl(message: GroupMessage, imgUrl: String) {
if (message.type=="audio")
message.audioMessage?.uri=imgUrl
else
message.imageMessage?.uri=imgUrl
}
private fun sendPushToMembers(group: Group, message: GroupMessage) {
val users = group.members?.filter { it.user.token.isNotEmpty() }?.map {
it.user.token
it
}
users?.forEach {
UserUtils.sendPush(
applicationContext, TYPE_NEW_GROUP_MESSAGE,
Json.encodeToString(message), it.user.token, it.id
)
}
}
private fun getSourceName(message: GroupMessage, url: String): String {
val createdAt=message.createdAt.toString()
val num=createdAt.substring(createdAt.length - 5)
val extension=url.substring(url.lastIndexOf('.'))
return "${message.type}_$num$extension"
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/services/UploadWorker.kt
================================================
package com.gowtham.letschat.services
import android.content.Context
import android.net.Uri
import androidx.hilt.work.HiltWorker
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.google.firebase.firestore.CollectionReference
import com.google.firebase.storage.UploadTask
import com.gowtham.letschat.TYPE_NEW_MESSAGE
import com.gowtham.letschat.core.MessageSender
import com.gowtham.letschat.core.OnMessageResponse
import com.gowtham.letschat.db.DbRepository
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Message
import com.gowtham.letschat.di.MessageCollection
import com.gowtham.letschat.utils.Constants
import com.gowtham.letschat.utils.UserUtils
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.io.FileInputStream
import java.util.concurrent.CountDownLatch
@HiltWorker
class UploadWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
@MessageCollection
val msgCollection: CollectionReference,
val dbRepository: DbRepository):
Worker(appContext, workerParams) {
private val params=workerParams
override fun doWork(): Result {
val stringData=params.inputData.getString(Constants.MESSAGE_DATA) ?: ""
val message= Json.decodeFromString(stringData)
val url=params.inputData.getString(Constants.MESSAGE_FILE_URI)!!
val sourceName=getSourceName(message,url)
val storageRef=UserUtils.getStorageRef(applicationContext)
val child = storageRef.child(
"chats/${message.to}/$sourceName")
val task: UploadTask
task = if(url.contains(".mp3")) {
val stream = FileInputStream(url) //audio message
child.putStream(stream)
}else
child.putFile(Uri.parse(message.imageMessage?.uri))
val countDownLatch = CountDownLatch(1)
val result= arrayOf(Result.failure())
task.addOnSuccessListener {
child.downloadUrl.addOnCompleteListener { taskResult ->
Timber.v("TaskResult ${taskResult.result.toString()}")
val downloadUrl=taskResult.result.toString()
sendMessage(message,downloadUrl,result,countDownLatch)
}.addOnFailureListener { e ->
Timber.v("TaskResult Failed ${e.message}")
result[0]= Result.failure()
message.status=4
dbRepository.insertMessage(message)
countDownLatch.countDown()
}
}
countDownLatch.await()
return result[0]
}
private fun getSourceName(message: Message, url: String): String {
val createdAt=message.createdAt.toString()
val num=createdAt.substring(createdAt.length - 5)
val extension=url.substring(url.lastIndexOf('.'))
return "${message.type}_$num$extension"
}
private fun sendMessage(message: Message,downloadUrl: String,result: Array,
countDownLatch: CountDownLatch) {
val chatUser=Json.decodeFromString(params.inputData.getString(Constants.CHAT_USER_DATA)!!)
setUrl(message,downloadUrl)
val messageSender = MessageSender(
msgCollection,
dbRepository,
chatUser,object : OnMessageResponse{
override fun onSuccess(message: Message) {
UserUtils.sendPush(applicationContext, TYPE_NEW_MESSAGE, Json.encodeToString(message)
, chatUser.user.token,message.to)
result[0]= Result.success()
countDownLatch.countDown()
}
override fun onFailed(message: Message) {
result[0]= Result.failure()
dbRepository.insertMessage(message)
countDownLatch.countDown()
}
}
)
messageSender.checkAndSend(message.from, message.to, message)
}
private fun setUrl(message: Message, imgUrl: String) {
if (message.type=="audio")
message.audioMessage?.uri=imgUrl
else
message.imageMessage?.uri=imgUrl
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/ui/activities/ActBase.kt
================================================
package com.gowtham.letschat.ui.activities
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.google.firebase.database.*
import com.google.firebase.ktx.Firebase
import com.gowtham.letschat.MApplication
import com.gowtham.letschat.db.ChatUserDatabase
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.db.daos.GroupDao
import com.gowtham.letschat.db.daos.GroupMessageDao
import com.gowtham.letschat.db.daos.MessageDao
import com.gowtham.letschat.models.UserStatus
import com.gowtham.letschat.utils.*
import dagger.hilt.android.AndroidEntryPoint
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
open class ActBase : AppCompatActivity() {
private val database = FirebaseDatabase.getInstance()
@Inject
lateinit var preference: MPreference
@Inject
lateinit var chatUserDao: ChatUserDao
@Inject
lateinit var msgDao: MessageDao
@Inject
lateinit var groupDao: GroupDao
@Inject
lateinit var messageDao: GroupMessageDao
@Inject
lateinit var db: ChatUserDatabase
private var connectedRef: DatabaseReference?=null
private val newLogInReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (Constants.ACTION_LOGGED_IN_ANOTHER_DEVICE == intent.action)
Utils.showLoggedInAlert(this@ActBase, preference,db )
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
MApplication.isAppRunning=true
registerReceiver(newLogInReceiver, IntentFilter(Constants.ACTION_LOGGED_IN_ANOTHER_DEVICE))
if (!preference.getUid().isNullOrEmpty())
updateStatus()
}
override fun onResume() {
MApplication.isAppRunning=true
super.onResume()
}
private fun updateStatus(){
try {
val lastOnlineRef = database.getReference("/Users/${preference.getUid()}/last_seen")
val status = database.getReference("/Users/${preference.getUid()}/status")
connectedRef = database.getReference(".info/connected")
connectedRef?.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val connected: Boolean = (snapshot.value ?: false) as Boolean
if (connected) {
LogMessage.v("Online status updated##")
status.setValue("online")
lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)
status.onDisconnect().setValue("offline")
}
}
override fun onCancelled(error: DatabaseError) {
LogMessage.v("Listener was cancelled at .info/connected ${error.message}")
}
})
} catch (e: Exception) {
e.printStackTrace()
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onProfileUpdated(event: UserStatus) { //will be triggered only when initial profile update completed
if (event.status=="online")
updateStatus()
else{
val status = database.getReference("/Users/${preference.getUid()}/status")
status.setValue("offline")
}
}
override fun onDestroy() {
MApplication.isAppRunning=false
EventBus.getDefault().unregister(this)
Timber.v("onDestroy")
unregisterReceiver(newLogInReceiver)
super.onDestroy()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/ui/activities/ActSplash.kt
================================================
package com.gowtham.letschat.ui.activities
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import com.google.firebase.firestore.CollectionReference
import com.gowtham.letschat.R
import com.gowtham.letschat.models.UserProfile
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.UserUtils
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
class ActSplash : AppCompatActivity() {
@Inject
lateinit var preference: MPreference
@Inject
lateinit var userCollection: CollectionReference
private lateinit var sharedViewModel: SharedViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.act_splash)
sharedViewModel = ViewModelProvider(this).get(SharedViewModel::class.java)
UserUtils.updatePushToken(this, userCollection,false)
sharedViewModel.onFromSplash()
sharedViewModel.openMainAct.observe(this, {
startActivity(Intent(this, MainActivity::class.java))
finish()
})
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/ui/activities/MainActivity.kt
================================================
package com.gowtham.letschat.ui.activities
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.Navigation
import androidx.navigation.fragment.FragmentNavigator
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.gowtham.letschat.BuildConfig
import com.gowtham.letschat.R
import com.gowtham.letschat.databinding.ActivityMainBinding
import com.gowtham.letschat.db.data.ChatUser
import com.gowtham.letschat.db.data.Group
import com.gowtham.letschat.fragments.single_chat_home.FSingleChatHomeDirections
import com.gowtham.letschat.utils.*
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.launch
import timber.log.Timber
class MainActivity : ActBase() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private val sharedViewModel: SharedViewModel by viewModels()
private lateinit var searchView: SearchView
private lateinit var searchItem: MenuItem
private var stopped=false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
setSupportActionBar(binding.toolbar)
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
binding.fab.setOnClickListener {
if (searchItem.isActionViewExpanded)
searchItem.collapseActionView()
if (Utils.askContactPermission(returnFragment()!!)) {
if (navController.isValidDestination(R.id.FSingleChatHome))
navController.navigate(R.id.action_FSingleChatHome_to_FContacts)
else if (navController.isValidDestination(R.id.FGroupChatHome))
navController.navigate(R.id.action_FGroupChatHome_to_FAddGroupMembers)
}
}
setDataInView()
subscribeObservers()
}
private fun subscribeObservers() {
val badge = binding.bottomNav.getOrCreateBadge(R.id.nav_chat)
badge.isVisible = false
val groupChatBadge = binding.bottomNav.getOrCreateBadge(R.id.nav_group)
groupChatBadge.isVisible = false
lifecycleScope.launch {
groupDao.getGroupWithMessages().conflate().collect { list ->
val count = list.filter { it.group.unRead != 0 }
groupChatBadge.isVisible = count.isNotEmpty() //hide if 0
groupChatBadge.number = count.size
}
}
lifecycleScope.launch {
chatUserDao.getChatUserWithMessages().conflate().collect { list ->
val count = list.filter { it.user.unRead != 0 && it.messages.isNotEmpty() }
badge.isVisible = count.isNotEmpty() //hide if 0
badge.number = count.size
}
}
}
private fun setDataInView() {
try {
navController = Navigation.findNavController(this, R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, destination, _ ->
onDestinationChanged(destination.id)
}
val appBarConfiguration = AppBarConfiguration(setOf(R.id.FSingleChatHome))
binding.toolbar.setupWithNavController(navController, appBarConfiguration)
binding.bottomNav.setOnNavigationItemSelectedListener(onBottomNavigationListener)
val isNewMessage = intent.action == Constants.ACTION_NEW_MESSAGE
val isNewGroupMessage = intent.action == Constants.ACTION_GROUP_NEW_MESSAGE
val userData = intent.getParcelableExtra(Constants.CHAT_USER_DATA)
val groupData = intent.getParcelableExtra(Constants.GROUP_DATA)
if (preference.isLoggedIn() && navController.isValidDestination(R.id.FLogIn)) {
if (preference.getUserProfile() == null)
navController.navigate(R.id.action_FLogIn_to_FProfile)
else
navController.navigate(R.id.action_FLogIn_to_FSingleChatHome)
}
//single chat message notification clicked
if (isNewMessage && navController.isValidDestination(R.id.FSingleChatHome)) {
preference.setCurrentUser(userData!!.id)
val action = FSingleChatHomeDirections.actionFSingleChatToFChat(userData)
navController.navigate(action)
} else if (isNewGroupMessage && navController.isValidDestination(R.id.FSingleChatHome)) {
preference.setCurrentGroup(groupData!!.id)
val action = FSingleChatHomeDirections.actionFSingleChatHomeToFGroupChat(groupData)
navController.navigate(action)
}
if (!preference.isSameDevice())
Utils.showLoggedInAlert(this, preference, db)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun onDestinationChanged(currentDestination: Int) {
try {
when(currentDestination) {
R.id.FSingleChatHome -> {
binding.bottomNav.selectedItemId = R.id.nav_chat
showView()
}
R.id.FGroupChatHome -> {
binding.bottomNav.selectedItemId = R.id.nav_group
showView()
}
R.id.FSearch -> {
binding.bottomNav.selectedItemId = R.id.nav_search
showView()
binding.fab.hide()
}
R.id.FMyProfile -> {
binding.bottomNav.selectedItemId = R.id.nav_profile
showView()
binding.fab.hide()
}
else -> {
binding.bottomNav.gone()
binding.fab.gone()
binding.toolbar.gone()
}
}
Handler(Looper.getMainLooper()).postDelayed({ //delay time for searchview
if (this::searchItem.isInitialized) {
if (currentDestination == R.id.FMyProfile) {
searchItem.collapseActionView()
searchItem.isVisible = false
}else
searchItem.isVisible = true
}
}, 500)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater: MenuInflater = menuInflater
inflater.inflate(R.menu.menu_search, menu)
initToolbarItem()
return true
}
private fun initToolbarItem() {
searchItem = binding.toolbar.menu.findItem(R.id.action_search)
searchView = searchItem.actionView as SearchView
searchView.apply {
maxWidth = Integer.MAX_VALUE
queryHint = getString(R.string.txt_search)
}
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
sharedViewModel.setState(ScreenState.SearchState)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
if (!stopped)
sharedViewModel.setState(ScreenState.IdleState)
return true
}
})
sharedViewModel.getState().observe(this, { state ->
if (state is ScreenState.SearchState && searchView.isIconified) {
searchItem.expandActionView()
searchView.setQuery(sharedViewModel.getLastQuery().value, false)
}
})
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
sharedViewModel.setLastQuery(newText.toString())
return true
}
})
}
private fun showView() {
binding.bottomNav.show()
binding.fab.show()
binding.toolbar.show()
}
private fun isNotSameDestination(destination: Int): Boolean {
return destination != Navigation.findNavController(this, R.id.nav_host_fragment)
.currentDestination!!.id
}
private val onBottomNavigationListener =
BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.nav_chat -> {
val navOptions =
NavOptions.Builder().setPopUpTo(R.id.nav_host_fragment, true).build()
if (isNotSameDestination(R.id.FSingleChatHome)) {
searchItem.collapseActionView()
Navigation.findNavController(this, R.id.nav_host_fragment)
.navigate(R.id.FSingleChatHome, null, navOptions)
}
true
}
R.id.nav_group -> {
if (isNotSameDestination(R.id.FGroupChatHome)) {
searchItem.collapseActionView()
Navigation.findNavController(this, R.id.nav_host_fragment)
.navigate(R.id.FGroupChatHome)
}
true
}
R.id.nav_search -> {
if (isNotSameDestination(R.id.FSearch)) {
searchItem.collapseActionView()
Navigation.findNavController(this, R.id.nav_host_fragment)
.navigate(R.id.FSearch)
}
true
}
else -> {
if (isNotSameDestination(R.id.FMyProfile)) {
searchItem.collapseActionView()
Navigation.findNavController(this, R.id.nav_host_fragment)
.navigate(R.id.FMyProfile)
}
true
}
}
}
fun returnFragment(): Fragment? {
val navHostFragment: Fragment? =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
return navHostFragment?.childFragmentManager?.fragments?.get(0)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val navHostFragment = supportFragmentManager.fragments.first() as? NavHostFragment
if (navHostFragment != null) {
val childFragments = navHostFragment.childFragmentManager.fragments
childFragments.forEach { fragment ->
fragment.onActivityResult(requestCode, resultCode, data)
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array,
grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
/* val navHostFragment = supportFragmentManager.fragments.first() as? NavHostFragment
if (navHostFragment != null) {
val childFragments = navHostFragment.childFragmentManager.fragments
childFragments.forEach { fragment ->
fragment.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}*/
}
override fun onBackPressed() {
if (navController.isValidDestination(R.id.FSingleChatHome))
finish()
else if (navController.isValidDestination(R.id.FMyProfile) ||
navController.isValidDestination(R.id.FGroupChatHome) ||
navController.isValidDestination(R.id.FSearch)) {
val navOptions =
NavOptions.Builder().setPopUpTo(R.id.nav_host_fragment, true).build()
Navigation.findNavController(this, R.id.nav_host_fragment)
.navigate(R.id.FSingleChatHome, null, navOptions)
} else
super.onBackPressed()
}
override fun onStop() {
super.onStop()
Timber.v("onSdd")
stopped=true
}
override fun onResume() {
super.onResume()
Timber.v("onResime")
stopped=false
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/ui/activities/SharedViewModel.kt
================================================
package com.gowtham.letschat.ui.activities
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.gowtham.letschat.db.daos.ChatUserDao
import com.gowtham.letschat.models.Country
import com.gowtham.letschat.utils.MPreference
import com.gowtham.letschat.utils.ScreenState
import com.gowtham.letschat.utils.printMeD
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.scopes.ActivityScoped
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.concurrent.schedule
@HiltViewModel
class SharedViewModel @Inject constructor() : ViewModel() {
val country = MutableLiveData()
val openMainAct = MutableLiveData()
private val _state = MutableLiveData(ScreenState.IdleState)
val lastQuery = MutableLiveData()
val listOfQuery = arrayListOf("")
private var timer: TimerTask? = null
init {
"Init SharedViewModel".printMeD()
}
fun getLastQuery(): LiveData {
return lastQuery
}
fun setLastQuery(query: String) {
Timber.v("Last Query $query")
listOfQuery.add(query)
lastQuery.value = query
}
fun setState(state: ScreenState) {
Timber.v("State $state")
_state.value = state
}
fun getState(): LiveData {
return _state
}
fun setCountry(country: Country) {
this.country.value = country
}
fun onFromSplash() {
if (timer == null) {
timer = Timer().schedule(2000) {
openMainAct.postValue(true)
}
}
}
override fun onCleared() {
super.onCleared()
"onCleared SharedViewModel".printMeD()
}
}
================================================
FILE: app/src/main/java/com/gowtham/letschat/utils/BindingAdapters.kt
================================================
package com.gowtham.letschat.utils
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextUtils
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.view.setPadding
import androidx.core.widget.ImageViewCompat
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseMethod
import coil.ImageLoader
import coil.decode.GifDecoder
import coil.load
import coil.request.CachePolicy
import coil.request.ImageRequest
import coil.request.SuccessResult
import coil.transform.CircleCropTransformation
import com.airbnb.lottie.LottieAnimationView
import com.google.android.material.chip.Chip
import com.gowtham.letschat.MApplication
import com.gowtham.letschat.R
import com.gowtham.letschat.db.data.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
object BindingAdapters {
@BindingAdapter("main", "secondText")
@JvmStatic
fun setBoldString(view: TextView, maintext: String, sequence: String) {
view.text = getBoldText(maintext, sequence)
}
@JvmStatic
fun getBoldText(text: String, name: String): SpannableStringBuilder {
val str = SpannableStringBuilder(text)
val textPosition = text.indexOf(name)
str.setSpan(
android.text.style.StyleSpan(Typeface.BOLD),
textPosition, textPosition + name.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
return str
}
@BindingAdapter("imageUrl")
@JvmStatic
fun loadImage(view: ImageView, url: String?) {
if(url.isNullOrEmpty())
return
else {
ImageViewCompat.setImageTintList(view, null) //removing image tint
view.setPadding(0)
}
ImageUtils.loadUserImage(view, url)
}
@BindingAdapter("lastMessage")
@JvmStatic
fun setLastMessage(txtView: TextView, msgList: List) {
val lastMsg=msgList.last()
txtView.text= getLastMsgTxt(lastMsg)
}
@InverseMethod("messageBsg")
@JvmStatic
fun messageBsg(msg: Message): String {
return "sd"
}
@BindingAdapter("message" , "messageList" , "adapterPos")
@JvmStatic
fun messageBackground(txtView: TextView,msg: Message,list: ArrayList,adapterPos: Int) {
txtView.setBackgroundResource(R.drawable.shape_send_msg)
if (list.size<=2){
val message=list[adapterPos-1]
if (message.from=="me"){
}
}
}
@BindingAdapter("loadImage")
@JvmStatic
fun loadImage(imgView: ImageView, message: Message) {
val url=message.imageMessage?.uri
val imageType=message.imageMessage?.imageType
loadMsgImage(imgView,url!!,imageType!!)
}
@BindingAdapter("loadGroupMsgImage")
@JvmStatic
fun loadGrpMsgImage(imgView: ImageView, message: GroupMessage) {
val url=message.imageMessage?.uri
val imageType=message.imageMessage?.imageType
loadMsgImage(imgView,url.toString(),imageType.toString())
}
private fun loadMsgImage(imgView: ImageView, url: String, imageType: String) {
try {
if (imageType=="gif") {
val imageLoader = ImageLoader.Builder(imgView.context)
.componentRegistry {
add(GifDecoder())
}
.build()
imgView.load(url,imageLoader){
diskCachePolicy(CachePolicy.ENABLED)
placeholder(R.drawable.gif)
error(R.drawable.gif)
}
}else {
val isSticker=imageType=="sticker"
val placeHolder=if(isSticker) R.drawable.ic_sticker else R.drawable.ic_gal_pholder
imgView.load(url) {
diskCachePolicy(CachePolicy.ENABLED)
placeholder(placeHolder)
error(placeHolder)
}}
} catch (e: Exception) {
e.printStackTrace()
}
}
fun getLastMsgTxt(msg: Message) : String{
return when(msg.type){
"text" -> {
msg.textMessage?.text.toString()
}
"audio" -> {
"Audio"
}
"image" -> {
msg.imageMessage?.imageType.toString().capitalize(Locale.getDefault())
}
"video" -> {
"Video"
}
"file" -> {
"File"
}
else->{ "Steotho Image" }
}
}
fun getLastMsgTxt(msg: GroupMessage) : String{
return when(msg.type){
"text" -> {
msg.textMessage?.text.toString()
}
"audio" -> {
"Audio"
}
"image" -> {
msg.imageMessage?.imageType.toString().capitalize(Locale.getDefault())
}
"video" -> {
"Video"
}
"file" -> {
"File"
}
else->{ "Steotho image" }
}
}
@BindingAdapter("messageSendTime")
@JvmStatic
fun setMessageTime(txtView: TextView, msgList: List) {
val lastMsg=msgList.last()
val sentTime = lastMsg.createdAt
txtView.text = Utils.getTime(sentTime)
}
@BindingAdapter("showMsgTime")
@JvmStatic
fun showMsgTime(txtView: TextView, lastMsg: Message) {
val sentTime = lastMsg.createdAt
txtView.text = Utils.getTime(sentTime)
}
@BindingAdapter("showGrpMsgTime")
@JvmStatic
fun showGrpMsgTime(txtView: TextView, lastMsg: GroupMessage) {
val sentTime = lastMsg.createdAt
txtView.text = Utils.getTime(sentTime)
}
@BindingAdapter("loadAsDrawable")
@JvmStatic
fun loadAsDrawable(chip: Chip, user: ChatUser) {
val url=user.user.image
if(url.isNotEmpty()){
CoroutineScope(Dispatchers.IO).launch {
val drawable= getBitmap(url)
withContext(Dispatchers.Main){
chip.chipIcon=drawable
}
}
}
}
private suspend fun getBitmap(url: String): Drawable {
val context=MApplication.appContext
val loader= ImageLoader(context)
val request= ImageRequest.Builder(context)
.data(url)
.transformations(CircleCropTransformation())
.build()
return (loader.execute(request) as SuccessResult).drawable
}
@BindingAdapter("messageStatus")
@JvmStatic
fun setState(txtStatus: TextView, status: Int) {
txtStatus.text=when(status){
0 -> "Sending.."
1 -> "Sent"
2 -> "Delivered"
3 -> "Seen"
else-> "Failed"
}
}
@BindingAdapter("groupMessageStatus")
@JvmStatic
fun groupMsgStatus(txtStatus: TextView, message: GroupMessage) {
val preference=MPreference(MApplication.appContext)
val statusList=message.status
val myStatus=statusList.first()
if (message.from==preference.getUid())
statusList.removeAt(0)
val deliveried=message.status.any{ it==2 || it==3 } //if anyone has seen the message
val seen=message.status.all{ it==3 } //all members seen the messge
txtStatus.text= when {
myStatus==0 -> "Sending"
seen -> "Seen"
deliveried -> "Delivered"
myStatus==1 -> "Sent"
else -> "Failed"
}
}
@BindingAdapter("progressState")
@JvmStatic
fun setProgressState(view: ProgressBar, state: LoadState?) {
state?.let {
view.visibility=when(it){
LoadState.OnLoading -> View.VISIBLE
else ->
View.GONE
}
}
}
@BindingAdapter("setUnReadCount")
@JvmStatic
fun setUnReadCount(txtView: TextView, msgList: List) {
val fromUser=MPreference(txtView.context).getUid()
val unReadCount=msgList.filter { it.to==fromUser && it.status<3 }.size
txtView.text = unReadCount.toString()
txtView.visibility= if (unReadCount==0) View.GONE else View.VISIBLE
}
@BindingAdapter("setUnReadCount2")
@JvmStatic
fun setUnReadCount(txtView: TextView, count: Int) {
Timber.v("setUnReadCount2 $count")
txtView.text = count.toString()
txtView.visibility= if (count==0) View.GONE else View.VISIBLE
}
@BindingAdapter("showSelected")
@JvmStatic
fun showSelected(view: LottieAnimationView, isSelected: Boolean) {
if (isSelected) {
view.playAnimation()
view.show()
}else
view.gone()
}
@BindingAdapter("showTxtView")
@JvmStatic
fun setChipIcon(txtView: TextView, user: ChatUser) {
if(user.user.image.isEmpty())
txtView.text=user.localName.first().toString()
else
txtView.gone()
}
@BindingAdapter("setMemberNames")
@JvmStatic
fun setMemberNames(txtView: TextView, group: Group) {
val members =group.members?.map { chatUser->
val savedName=chatUser.localName
if (savedName.isNotEmpty())
savedName
else
"${chatUser.user.mobile?.country} ${chatUser.user.mobile?.number}"
}
members?.let {
txtView.text=TextUtils.join(", ", it)
}
}
@BindingAdapter("setGroupName")
@JvmStatic
fun setGroupName(txtView: TextView, group: Group) {
txtView.text=Utils.getGroupName(group.id)
}
@BindingAdapter("groupLastMessage")
@JvmStatic
fun groupLastMessage(txtView: TextView, group: GroupWithMessages) {
val messages=group.messages
if (messages.isEmpty()){
val createdBy=group.group.createdBy
val msg="Created by ${group.group.members?.first { it.id==createdBy }?.localName}"
txtView.text=msg
}
else{
val message=messages.last()
val localName=group.group.members?.first { it.id==message.from }?.localName
val txtMsg="$localName : ${getLastMsgTxt(message)}"
txtView.text=txtMsg
}
}
@BindingAdapter("setGroupMessageSendTime")
@JvmStatic
fun setGroupMessageSendTime(txtView: TextView, msgList: List) {
if (msgList.isNotEmpty()) {
val lastMsg = msgList.last()
val sentTime = lastMsg.createdAt
txtView.text = Utils.getTime(sentTime)
}
}
@BindingAdapter("setGroupUnReadCount")
@JvmStatic
fun setGroupUnReadCount(txtView: TextView, unReadCount: Int) {
if(unReadCount==0)
txtView.gone()
else {
txtView.text = unReadCount.toString()
txtView.show()
}
}
@BindingAdapter("chatUsers", "message")
@JvmStatic
fun setChatUserName(txtView: TextView, users: Array