Repository: a914-gowtham/LetsChat Branch: master Commit: 8d8c2ffad6c7 Files: 254 Total size: 16.7 MB Directory structure: gitextract_cohzfmis/ ├── .firebaserc ├── .gitignore ├── .idea/ │ ├── assetWizardSettings.xml │ ├── caches/ │ │ └── build_file_checksums.ser │ ├── codeStyles/ │ │ ├── Project.xml │ │ └── codeStyleConfig.xml │ ├── compiler.xml │ ├── inspectionProfiles/ │ │ └── Project_Default.xml │ ├── jarRepositories.xml │ ├── misc.xml │ ├── modules.xml │ ├── navEditor.xml │ ├── runConfigurations.xml │ └── vcs.xml ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── app-debug.apk │ ├── build.gradle │ ├── google-services.json │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── gowtham/ │ │ └── letschat/ │ │ ├── FlowUtilAndroidTest.kt │ │ ├── HiltTestRunner.kt │ │ ├── LiveDataUtilAndroidTest.kt │ │ ├── db/ │ │ │ └── daos/ │ │ │ ├── ChatUserDaoTest.kt │ │ │ ├── GroupDaoTest.kt │ │ │ ├── GroupMessageDaoTest.kt │ │ │ └── MessageDaoTest.kt │ │ └── di/ │ │ └── TestAppModule.kt │ ├── main/ │ │ ├── AndroidManifest.xml │ │ ├── java/ │ │ │ └── com/ │ │ │ └── gowtham/ │ │ │ └── letschat/ │ │ │ ├── FirebasePush.kt │ │ │ ├── MApplication.kt │ │ │ ├── core/ │ │ │ │ ├── ChatHandler.kt │ │ │ │ ├── ChatUserProfileListener.kt │ │ │ │ ├── ChatUserUtil.kt │ │ │ │ ├── ContactsQuery.kt │ │ │ │ ├── GroupChatHandler.kt │ │ │ │ ├── GroupMsgSender.kt │ │ │ │ ├── GroupMsgStatusUpdater.kt │ │ │ │ ├── GroupQuery.kt │ │ │ │ ├── MessageSender.kt │ │ │ │ └── MessageStatusUpdater.kt │ │ │ ├── db/ │ │ │ │ ├── ChatUserDatabase.kt │ │ │ │ ├── DbRepository.kt │ │ │ │ ├── DefaultDbRepo.kt │ │ │ │ ├── TypeConverter.kt │ │ │ │ ├── daos/ │ │ │ │ │ ├── ChatUserDao.kt │ │ │ │ │ ├── GroupDao.kt │ │ │ │ │ ├── GroupMessageDao.kt │ │ │ │ │ └── MessageDao.kt │ │ │ │ └── data/ │ │ │ │ ├── ChatUser.kt │ │ │ │ ├── ChatUserWithMessages.kt │ │ │ │ ├── Group.kt │ │ │ │ ├── GroupMessage.kt │ │ │ │ ├── GroupWithMessages.kt │ │ │ │ └── Message.kt │ │ │ ├── di/ │ │ │ │ ├── AppModule.kt │ │ │ │ └── DbModule.kt │ │ │ ├── fragments/ │ │ │ │ ├── FAttachment.kt │ │ │ │ ├── FImageSrcSheet.kt │ │ │ │ ├── MainFragmentFactory.kt │ │ │ │ ├── add_group_members/ │ │ │ │ │ ├── AdAddMembers.kt │ │ │ │ │ ├── AdChip.kt │ │ │ │ │ ├── AddGroupViewModel.kt │ │ │ │ │ └── FAddGroupMembers.kt │ │ │ │ ├── contacts/ │ │ │ │ │ ├── AdContact.kt │ │ │ │ │ ├── ContactsViewModel.kt │ │ │ │ │ └── FContacts.kt │ │ │ │ ├── countries/ │ │ │ │ │ ├── AdCountries.kt │ │ │ │ │ └── FCountries.kt │ │ │ │ ├── create_group/ │ │ │ │ │ ├── CreateGroupViewModel.kt │ │ │ │ │ └── FCreateGroup.kt │ │ │ │ ├── group_chat/ │ │ │ │ │ ├── AdGroupChat.kt │ │ │ │ │ ├── FGroupChat.kt │ │ │ │ │ └── GroupChatViewModel.kt │ │ │ │ ├── group_chat_home/ │ │ │ │ │ ├── AdGroupChatHome.kt │ │ │ │ │ ├── FGroupChatHome.kt │ │ │ │ │ └── GroupChatHomeViewModel.kt │ │ │ │ ├── login/ │ │ │ │ │ ├── FLogin.kt │ │ │ │ │ ├── FVerify.kt │ │ │ │ │ ├── LogInViewModel.kt │ │ │ │ │ └── LoginRepo.kt │ │ │ │ ├── myprofile/ │ │ │ │ │ ├── FMyProfile.kt │ │ │ │ │ └── FMyProfileViewModel.kt │ │ │ │ ├── profile/ │ │ │ │ │ ├── FProfile.kt │ │ │ │ │ └── ProfileViewModel.kt │ │ │ │ ├── search/ │ │ │ │ │ ├── FSearch.kt │ │ │ │ │ ├── FSearchViewModel.kt │ │ │ │ │ └── SearchRepo.kt │ │ │ │ ├── single_chat/ │ │ │ │ │ ├── AdChat.kt │ │ │ │ │ ├── FSingleChat.kt │ │ │ │ │ └── SingleChatViewModel.kt │ │ │ │ └── single_chat_home/ │ │ │ │ ├── AdSingleChatHome.kt │ │ │ │ ├── FSingleChatHome.kt │ │ │ │ └── SingleChatHomeViewModel.kt │ │ │ ├── models/ │ │ │ │ ├── Contact.kt │ │ │ │ ├── Country.kt │ │ │ │ ├── ModelDeviceDetails.kt │ │ │ │ ├── ModelMobile.kt │ │ │ │ ├── MyImage.kt │ │ │ │ ├── PushMsg.kt │ │ │ │ ├── UserProfile.kt │ │ │ │ └── UserStatus.kt │ │ │ ├── services/ │ │ │ │ ├── GroupUploadWorker.kt │ │ │ │ └── UploadWorker.kt │ │ │ ├── ui/ │ │ │ │ └── activities/ │ │ │ │ ├── ActBase.kt │ │ │ │ ├── ActSplash.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ └── SharedViewModel.kt │ │ │ ├── utils/ │ │ │ │ ├── BindingAdapters.kt │ │ │ │ ├── BottomSheetEvent.kt │ │ │ │ ├── ConnectionChangeEvent.kt │ │ │ │ ├── Constants.kt │ │ │ │ ├── Countries.kt │ │ │ │ ├── DataStorePreference.kt │ │ │ │ ├── DiffCallbackChatUser.kt │ │ │ │ ├── Event.kt │ │ │ │ ├── Events/ │ │ │ │ │ ├── EventAudioMsg.kt │ │ │ │ │ └── EventUpdateRecycleItem.kt │ │ │ │ ├── GroupMsgActionReceiver.kt │ │ │ │ ├── ImageUtils.kt │ │ │ │ ├── ItemClickListener.kt │ │ │ │ ├── LoadState.kt │ │ │ │ ├── LogInFailedState.kt │ │ │ │ ├── LogMessage.kt │ │ │ │ ├── MPreference.kt │ │ │ │ ├── NActionReceiver.kt │ │ │ │ ├── NotificationUtils.kt │ │ │ │ ├── OnSuccessListener.kt │ │ │ │ ├── ScreenState.kt │ │ │ │ ├── UserUtils.kt │ │ │ │ ├── Utils.kt │ │ │ │ ├── Validator.kt │ │ │ │ └── ViewUtils.kt │ │ │ └── views/ │ │ │ ├── CustomEditText.kt │ │ │ ├── CustomProgress.kt │ │ │ ├── CustomProgressView.kt │ │ │ ├── MainNavHostFragment.kt │ │ │ ├── PausableProgressBar.kt │ │ │ ├── PausableScaleAnimation.kt │ │ │ └── StoriesProgressView.kt │ │ └── res/ │ │ ├── anim/ │ │ │ ├── slide_in_right.xml │ │ │ └── slide_out_left.xml │ │ ├── drawable/ │ │ │ ├── ic_arrow_back.xml │ │ │ ├── ic_arrow_down.xml │ │ │ ├── ic_arrow_r8.xml │ │ │ ├── ic_clear.xml │ │ │ ├── ic_close_24px.xml │ │ │ ├── ic_launcher_background.xml │ │ │ ├── ic_menu_24px.xml │ │ │ ├── ic_search.xml │ │ │ ├── selector_menu.xml │ │ │ ├── shape_audio_bg.xml │ │ │ ├── shape_border_line.xml │ │ │ ├── shape_btn_bg.xml │ │ │ ├── shape_circle.xml │ │ │ ├── shape_circle_blue.xml │ │ │ ├── shape_contact_selected.xml │ │ │ ├── shape_divider.xml │ │ │ ├── shape_edit_bg.xml │ │ │ ├── shape_gradient.xml │ │ │ ├── shape_home_bg.xml │ │ │ ├── shape_menu_active.xml │ │ │ ├── shape_menu_non_active.xml │ │ │ ├── shape_msg_bg.xml │ │ │ ├── shape_radius.xml │ │ │ ├── shape_receive_msg.xml │ │ │ ├── shape_receive_msg_corned.xml │ │ │ ├── shape_send_msg.xml │ │ │ ├── shape_send_msg_corned.xml │ │ │ └── shape_unread_count.xml │ │ ├── drawable-v24/ │ │ │ └── ic_launcher_foreground.xml │ │ ├── layout/ │ │ │ ├── act_chat.xml │ │ │ ├── act_splash.xml │ │ │ ├── activity_main.xml │ │ │ ├── activity_main2.xml │ │ │ ├── alert_dialog.xml │ │ │ ├── alert_logout.xml │ │ │ ├── f_add_group_members.xml │ │ │ ├── f_attachment.xml │ │ │ ├── f_contacts.xml │ │ │ ├── f_countries.xml │ │ │ ├── f_create_group.xml │ │ │ ├── f_group_chat.xml │ │ │ ├── f_group_chat_home.xml │ │ │ ├── f_image_src_sheet.xml │ │ │ ├── f_login.xml │ │ │ ├── f_my_profile.xml │ │ │ ├── f_profile.xml │ │ │ ├── f_search.xml │ │ │ ├── f_single_chat.xml │ │ │ ├── f_single_chat_home.xml │ │ │ ├── f_verify.xml │ │ │ ├── load_state_footer.xml │ │ │ ├── pausable_progress.xml │ │ │ ├── progress_dialog.xml │ │ │ ├── row_add_member.xml │ │ │ ├── row_audio_receive.xml │ │ │ ├── row_audio_sent.xml │ │ │ ├── row_chat.xml │ │ │ ├── row_chip.xml │ │ │ ├── row_contact.xml │ │ │ ├── row_country.xml │ │ │ ├── row_group_audio_receive.xml │ │ │ ├── row_group_audio_sent.xml │ │ │ ├── row_group_chat.xml │ │ │ ├── row_group_image_receive.xml │ │ │ ├── row_group_image_sent.xml │ │ │ ├── row_group_sticker_receive.xml │ │ │ ├── row_group_sticker_sent.xml │ │ │ ├── row_group_txt_sent.xml │ │ │ ├── row_grp_txt_receive.xml │ │ │ ├── row_image_receive.xml │ │ │ ├── row_image_sent.xml │ │ │ ├── row_receive_message.xml │ │ │ ├── row_search_contact.xml │ │ │ ├── row_sent_message.xml │ │ │ ├── row_sticker_receive.xml │ │ │ ├── row_sticker_sent.xml │ │ │ ├── view_chat_btm.xml │ │ │ ├── view_chat_toolbar.xml │ │ │ ├── view_group_chat_btm.xml │ │ │ └── view_group_chat_toolbar.xml │ │ ├── menu/ │ │ │ ├── menu_btm_nav.xml │ │ │ ├── menu_contacts.xml │ │ │ ├── menu_option.xml │ │ │ └── menu_search.xml │ │ ├── navigation/ │ │ │ └── nav_graph.xml │ │ ├── raw/ │ │ │ ├── empty_state.json │ │ │ ├── lottie_send.json │ │ │ ├── lottie_tick.json │ │ │ └── lottie_voice.json │ │ ├── values/ │ │ │ ├── attrs.xml │ │ │ ├── colors.xml │ │ │ ├── dimen.xml │ │ │ ├── ids.xml │ │ │ ├── strings.xml │ │ │ └── styles.xml │ │ └── xml/ │ │ ├── network_security_config.xml │ │ └── provider_path.xml │ ├── release/ │ │ └── res/ │ │ └── values/ │ │ └── google_maps_api.xml │ └── test/ │ └── java/ │ └── com/ │ └── gowtham/ │ └── letschat/ │ ├── LiveDataUtilAndroid.kt │ ├── db/ │ │ └── DbRepositoryTest.kt │ ├── fragments/ │ │ └── single_chat_home/ │ │ └── SingleChatHomeViewModelTest.kt │ └── utils/ │ ├── MainCoroutineRule.kt │ └── ValidatorTest.kt ├── build.gradle ├── firebase.json ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .firebaserc ================================================ { "projects": { "default": "letschat-31c80" } } ================================================ FILE: .gitignore ================================================ # Files for the ART/Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # Intellij *.iml .idea/workspace.xml .idea/tasks.xml .idea/gradle.xml .idea/dictionaries .idea/libraries # Keystore files *.jks # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild # Freeline freeline.py freeline/ freeline_project_description.json ================================================ FILE: .idea/assetWizardSettings.xml ================================================ ================================================ FILE: .idea/codeStyles/Project.xml ================================================
xmlns:android ^$
xmlns:.* ^$ BY_NAME
.*:id http://schemas.android.com/apk/res/android
.*:name http://schemas.android.com/apk/res/android
name ^$
style ^$
.* ^$ BY_NAME
.* http://schemas.android.com/apk/res/android ANDROID_ATTRIBUTE_ORDER
.* .* BY_NAME
================================================ FILE: .idea/codeStyles/codeStyleConfig.xml ================================================ ================================================ FILE: .idea/compiler.xml ================================================ ================================================ FILE: .idea/inspectionProfiles/Project_Default.xml ================================================ ================================================ FILE: .idea/jarRepositories.xml ================================================ ================================================ FILE: .idea/misc.xml ================================================ ================================================ FILE: .idea/modules.xml ================================================ ================================================ FILE: .idea/navEditor.xml ================================================ ================================================ FILE: .idea/runConfigurations.xml ================================================ ================================================ 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 👇*** [![LetsChat App](https://img.shields.io/badge/LetsChat-APK-blue.svg?style=for-the-badge&logo=android)](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, message: GroupMessage) { val messageOwner= users.first { message.from==it.id } txtView.text=messageOwner.localName } @BindingAdapter("showUserIdIfNotLocalSaved", "currentMessage") @JvmStatic fun showUserIdIfNotLocalSaved( txtView: TextView, users: Array, message: GroupMessage ) { val messageOwner= users.first { message.from==it.id } txtView.text=messageOwner.user.userName if (messageOwner.locallySaved) txtView.gone() else txtView.show() } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/BottomSheetEvent.kt ================================================ package com.gowtham.letschat.utils data class BottomSheetEvent(var position: Int) ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/ConnectionChangeEvent.kt ================================================ package com.gowtham.letschat.utils class ConnectionChangeEvent(val message: String) ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Constants.kt ================================================ package com.gowtham.letschat.utils object Constants { const val VIEW_TYPE="view_type" const val VIEW_CT=43 const val KEY_LAST_QUERIED_LIST="last_queried_list" const val CHAT_USER_DB_NAME="chat_user_db" const val ACTION_LOGGED_IN_ANOTHER_DEVICE="action_logged_in_another_device" const val ACTION_NEW_MESSAGE="action_new_message" const val ACTION_GROUP_NEW_MESSAGE="action_group_new_message" const val ACTION_MARK_AS_READ="action_mark_as_read" const val ACTION_REPLY="action_reply" const val CHAT_USER_DATA="chat_user_data" const val GROUP_DATA="group_data" const val CHAT_DATA="chat_data" const val MESSAGE_DATA="message_data" const val MESSAGE_FILE_URI="message_file_uri" const val FCM_SERVER_KEY="AAAApjZeY_U:APA91bG5Ifs2wIABgd1edHXKvXaRm1yG7bUj3A0VY8TBvF00i-l7yQw82FF10dkhUj56uAC_fZHVqN5_h4IIGQ21Jpiyj_uszjG8Lez1aOSX0YF-Nhp1qDt_CcAIBv64uHubsLkmudVF" } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Countries.kt ================================================ package com.gowtham.letschat.utils import com.gowtham.letschat.models.Country object Countries { private val COUNTRIES= arrayOf( Country("AF", "Afghanistan", "+93", "AFN"), Country("AX", "Aland Islands", "+358", "EUR"), Country("AL", "Albania", "+355", "ALL"), Country("DZ", "Algeria", "+213", "DZD"), Country("AS", "American Samoa", "+1", "USD"), Country("AD", "Andorra", "+376", "EUR"), Country("AO", "Angola", "+244", "AOA"), Country("AI", "Anguilla", "+1", "XCD"), Country("AQ", "Antarctica", "+672", "USD"), Country("AG", "Antigua and Barbuda", "+1", "XCD"), Country("AR", "Argentina", "+54", "ARS"), Country("AM", "Armenia", "+374", "AMD"), Country("AW", "Aruba", "+297", "AWG"), Country("AU", "Australia", "+61", "AUD"), Country("AT", "Austria", "+43", "EUR"), Country("AZ", "Azerbaijan", "+994", "AZN"), Country("BS", "Bahamas", "+1", "BSD"), Country("BH", "Bahrain", "+973", "BHD"), Country("BD", "Bangladesh", "+880", "BDT"), Country("BB", "Barbados", "+1", "BBD"), Country("BY", "Belarus", "+375", "BYR"), Country("BE", "Belgium", "+32", "EUR"), Country("BZ", "Belize", "+501", "BZD"), Country("BJ", "Benin", "+229", "XOF"), Country("BM", "Bermuda", "+1", "BMD"), Country("BT", "Bhutan", "+975", "BTN"), Country("BO", "Bolivia, Plurinational State of", "+591", "BOB"), Country("BA", "Bosnia and Herzegovina", "+387", "BAM"), Country("BQ", "Bonaire", "+599", "USD"), Country("BW", "Botswana", "+267", "BWP"), Country("BV", "Bouvet Island", "+47", "NOK"), Country("BR", "Brazil", "+55", "BRL"), Country("IO", "British Indian Ocean Territory", "+246", "USD"), Country("BN", "Brunei Darussalam", "+673", "BND"), Country("BG", "Bulgaria", "+359", "BGN"), Country("BF", "Burkina Faso", "+226", "XOF"), Country("BI", "Burundi", "+257", "BIF"), Country("KH", "Cambodia", "+855", "KHR"), Country("CM", "Cameroon", "+237", "XAF"), Country("CA", "Canada", "+1", "CAD"), Country("CV", "Cape Verde", "+238", "CVE"), Country("KY", "Cayman Islands", "+345", "KYD"), Country("CF", "Central African Republic", "+236", "XAF"), Country("TD", "Chad", "+235", "XAF"), Country("CL", "Chile", "+56", "CLP"), Country("CN", "China", "+86", "CNY"), Country("CX", "Christmas Island", "+61", "AUD"), Country("CC", "Cocos (Keeling) Islands", "+61", "AUD"), Country("CO", "Colombia", "+57", "COP"), Country("KM", "Comoros", "+269", "KMF"), Country("CD", "Congo, The Democratic Republic of the", "+243", "CDF"), Country("CG", "Congo", "+242", "XAF"), Country("CK", "Cook Islands", "+682", "NZD"), Country("CR", "Costa Rica", "+506", "CRC"), Country("HR", "Croatia", "+385", "HRK"), Country("CU", "Cuba", "+53", "CUP"), Country("CW", "Curacao", "+599", "ANG"), Country("CY", "Cyprus", "+357", "EUR"), Country("CZ", "Czech Republic", "+420", "CZK"), Country("DK", "Denmark", "+45", "DKK"), Country("DJ", "Djibouti", "+253", "DJF"), Country("DM", "Dominica", "+1", "XCD"), Country("DO", "Dominican Republic", "+1", "DOP"), Country("TL", "East Timor", "+670", "USD"), Country("EC", "Ecuador", "+593", "USD"), Country("EG", "Egypt", "+20", "EGP"), Country("SV", "El Salvador", "+503", "SVC"), Country("GQ", "Equatorial Guinea", "+240", "XAF"), Country("ER", "Eritrea", "+291", "ERN"), Country("EE", "Estonia", "+372", "EUR"), Country("ET", "Ethiopia", "+251", "ETB"), Country("FK", "Falkland Islands (Malvinas)", "+500", "FKP"), Country("FO", "Faroe Islands", "+298", "DKK"), Country("FJ", "Fiji", "+679", "FJD"), Country("FI", "Finland", "+358", "EUR"), Country("FR", "France", "+33", "EUR"), Country("GF", "French Guiana", "+594", "EUR"), Country("TF", "French Southern Territories", "+262", "EUR"), Country("PF", "French Polynesia", "+689", "XPF"), Country("GA", "Gabon", "+241", "XAF"), Country("GM", "Gambia", "+220", "GMD"), Country("GE", "Georgia", "+995", "GEL"), Country("DE", "Germany", "+49", "EUR"), Country("GH", "Ghana", "+233", "GHS"), Country("GI", "Gibraltar", "+350", "GIP"), Country("GR", "Greece", "+30", "EUR"), Country("GL", "Greenland", "+299", "DKK"), Country("GD", "Grenada", "+1", "XCD"), Country("GP", "Guadeloupe", "+590", "EUR"), Country("GU", "Guam", "+1", "USD"), Country("GT", "Guatemala", "+502", "GTQ"), Country("GG", "Guernsey", "+44", "GGP"), Country("GN", "Guinea", "+224", "GNF"), Country("GW", "Guinea-Bissau", "+245", "XOF"), Country("GY", "Guyana", "+595", "GYD"), Country("HT", "Haiti", "+509", "HTG"), Country("HM", "Heard Island and McDonald Islands", "+000", "AUD"), Country("VA", "Holy See (Vatican City State)", "+379", "EUR"), Country("HN", "Honduras", "+504", "HNL"), Country("HK", "Hong Kong", "+852", "HKD"), Country("HU", "Hungary", "+36", "HUF"), Country("IS", "Iceland", "+354", "ISK"), Utils.getDefaultCountry(), Country("ID", "Indonesia", "+62", "IDR"), Country("IR", "Iran, Islamic Republic of", "+98", "IRR"), Country("IQ", "Iraq", "+964", "IQD"), Country("IE", "Ireland", "+353", "EUR"), Country("IM", "Isle of Man", "+44", "GBP"), Country("IL", "Israel", "+972", "ILS"), Country("IT", "Italy", "+39", "EUR"), Country("CI", "Ivory Coast", "+225", "XOF"), Country("JM", "Jamaica", "+1", "JMD"), Country("JP", "Japan", "+81", "JPY"), Country("JE", "Jersey", "+44", "JEP"), Country("JO", "Jordan", "+962", "JOD"), Country("KZ", "Kazakhstan", "+7", "KZT"), Country("KE", "Kenya", "+254", "KES"), Country("KI", "Kiribati", "+686", "AUD"), Country("XK", "Kosovo", "+383", "EUR"), Country("KW", "Kuwait", "+965", "KWD"), Country("KG", "Kyrgyzstan", "+996", "KGS"), Country("LA", "Lao People's Democratic Republic", "+856", "LAK"), Country("LV", "Latvia", "+371", "LVL"), Country("LB", "Lebanon", "+961", "LBP"), Country("LS", "Lesotho", "+266", "LSL"), Country("LR", "Liberia", "+231", "LRD"), Country("LY", "Libyan Arab Jamahiriya", "+218", "LYD"), Country("LI", "Liechtenstein", "+423", "CHF"), Country("LT", "Lithuania", "+370", "LTL"), Country("LU", "Luxembourg", "+352", "EUR"), Country("MO", "Macao", "+853", "MOP"), Country( "MK", "Macedonia, The Former Yugoslav Republic of", "+389", "MKD" ), Country("MG", "Madagascar", "+261", "MGA"), Country("MW", "Malawi", "+265", "MWK"), Country("MY", "Malaysia", "+60", "MYR"), Country("MV", "Maldives", "+960", "MVR"), Country("ML", "Mali", "+223", "XOF"), Country("MT", "Malta", "+356", "EUR"), Country("MH", "Marshall Islands", "+692", "USD"), Country("MQ", "Martinique", "+596", "EUR"), Country("MR", "Mauritania", "+222", "MRO"), Country("MU", "Mauritius", "+230", "MUR"), Country("YT", "Mayotte", "+262", "EUR"), Country("MX", "Mexico", "+52", "MXN"), Country("FM", "Micronesia, Federated States of", "+691", "USD"), Country("MD", "Moldova, Republic of", "+373", "MDL"), Country("MC", "Monaco", "+377", "EUR"), Country("MN", "Mongolia", "+976", "MNT"), Country("ME", "Montenegro", "+382", "EUR"), Country("MS", "Montserrat", "+1", "XCD"), Country("MA", "Morocco", "+212", "MAD"), Country("MZ", "Mozambique", "+258", "MZN"), Country("MM", "Myanmar", "+95", "MMK"), Country("NA", "Namibia", "+264", "NAD"), Country("NR", "Nauru", "+674", "AUD"), Country("NP", "Nepal", "+977", "NPR"), Country("NL", "Netherlands", "+31", "EUR"), Country("NC", "New Caledonia", "+687", "XPF"), Country("NZ", "New Zealand", "+64", "NZD"), Country("NI", "Nicaragua", "+505", "NIO"), Country("NE", "Niger", "+227", "XOF"), Country("NG", "Nigeria", "+234", "NGN"), Country("NU", "Niue", "+683", "NZD"), Country("NF", "Norfolk Island", "+672", "AUD"), Country("MP", "Northern Mariana Islands", "+1", "USD"), Country("KP", "North Korea", "+850", "KPW"), Country("NO", "Norway", "+47", "NOK"), Country("OM", "Oman", "+968", "OMR"), Country("PK", "Pakistan", "+92", "PKR"), Country("PW", "Palau", "+680", "USD"), Country("PS", "Palestinian Territory, Occupied", "+970", "ILS"), Country("PA", "Panama", "+507", "PAB"), Country("PG", "Papua New Guinea", "+675", "PGK"), Country("PY", "Paraguay", "+595", "PYG"), Country("PE", "Peru", "+51", "PEN"), Country("PH", "Philippines", "+63", "PHP"), Country("PN", "Pitcairn", "+872", "NZD"), Country("PL", "Poland", "+48", "PLN"), Country("PT", "Portugal", "+351", "EUR"), Country("PR", "Puerto Rico", "+1", "USD"), Country("QA", "Qatar", "+974", "QAR"), Country("RO", "Romania", "+40", "RON"), Country("RU", "Russia", "+7", "RUB"), Country("RW", "Rwanda", "+250", "RWF"), Country("RE", "Reunion", "+262", "EUR"), Country("BL", "Saint Barthelemy", "+590", "EUR"), Country( "SH", "Saint Helena, Ascension and Tristan Da Cunha", "+290", "SHP" ), Country("KN", "Saint Kitts and Nevis", "+1", "XCD"), Country("LC", "Saint Lucia", "+1", "XCD"), Country("MF", "Saint Martin", "+590", "EUR"), Country("PM", "Saint Pierre and Miquelon", "+508", "EUR"), Country("VC", "Saint Vincent and the Grenadines", "+1", "XCD"), Country("WS", "Samoa", "+685", "WST"), Country("SM", "San Marino", "+378", "EUR"), Country("ST", "Sao Tome and Principe", "+239", "STD"), Country("SA", "Saudi Arabia", "+966", "SAR"), Country("SN", "Senegal", "+221", "XOF"), Country("RS", "Serbia", "+381", "RSD"), Country("SC", "Seychelles", "+248", "SCR"), Country("SL", "Sierra Leone", "+232", "SLL"), Country("SG", "Singapore", "+65", "SGD"), Country("SX", "Sint Maarten", "+1", "ANG"), Country("SK", "Slovakia", "+421", "EUR"), Country("SI", "Slovenia", "+386", "EUR"), Country("SB", "Solomon Islands", "+677", "SBD"), Country("SO", "Somalia", "+252", "SOS"), Country("ZA", "South Africa", "+27", "ZAR"), Country("SS", "South Sudan", "+211", "SSP"), Country( "GS", "South Georgia and the South Sandwich Islands", "+500", "GBP" ), Country("KR", "South Korea", "+82", "KRW"), Country("ES", "Spain", "+34", "EUR"), Country("LK", "Sri Lanka", "+94", "LKR"), Country("SD", "Sudan", "+249", "SDG"), Country("SR", "Suriname", "+597", "SRD"), Country("SJ", "Svalbard and Jan Mayen", "+47", "NOK"), Country("SZ", "Swaziland", "+268", "SZL"), Country("SE", "Sweden", "+46", "SEK"), Country("CH", "Switzerland", "+41", "CHF"), Country("SY", "Syrian Arab Republic", "+963", "SYP"), Country("TW", "Taiwan", "+886", "TWD"), Country("TJ", "Tajikistan", "+992", "TJS"), Country("TZ", "Tanzania, United Republic of", "+255", "TZS"), Country("TH", "Thailand", "+66", "THB"), Country("TG", "Togo", "+228", "XOF"), Country("TK", "Tokelau", "+690", "NZD"), Country("TO", "Tonga", "+676", "TOP"), Country("TT", "Trinidad and Tobago", "+1", "TTD"), Country("TN", "Tunisia", "+216", "TND"), Country("TR", "Turkey", "+90", "TRY"), Country("TM", "Turkmenistan", "+993", "TMT"), Country("TC", "Turks and Caicos Islands", "+1", "USD"), Country("TV", "Tuvalu", "+688", "AUD"), Country("UM", "U.S. Minor Outlying Islands", "+1", "USD"), Country("UG", "Uganda", "+256", "UGX"), Country("UA", "Ukraine", "+380", "UAH"), Country("AE", "United Arab Emirates", "+971", "AED"), Country("GB", "United Kingdom", "+44", "GBP"), Country("US", "United States", "+1", "USD"), Country("UY", "Uruguay", "+598", "UYU"), Country("UZ", "Uzbekistan", "+998", "UZS"), Country("VU", "Vanuatu", "+678", "VUV"), Country("VE", "Venezuela, Bolivarian Republic of", "+58", "VEF"), Country("VN", "Vietnam", "+84", "VND"), Country("VG", "Virgin Islands, British", "+1", "USD"), Country("VI", "Virgin Islands, U.S.", "+1", "USD"), Country("WF", "Wallis and Futuna", "+681", "XPF"), Country("EH", "Western Sahara", "+212", "MAD"), Country("YE", "Yemen", "+967", "YER"), Country("ZM", "Zambia", "+260", "ZMW"), Country("ZW", "Zimbabwe", "+263", "USD") ) fun getCountries(): List{ return COUNTRIES.toList() } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/DataStorePreference.kt ================================================ package com.gowtham.letschat.utils import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.gowtham.letschat.db.data.ChatUser import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton @Singleton class DataStorePreference @Inject constructor(@ApplicationContext private val context: Context){ val Context.dataStore: DataStore by preferencesDataStore(name = "search_results") fun storeList(key: String,value: List){ CoroutineScope(Dispatchers.IO).launch { val dataStoreKey= stringPreferencesKey(key) context.dataStore.edit { it[dataStoreKey]=Json.encodeToString(value) } } } fun getList(): Flow{ val listKey = stringPreferencesKey(Constants.KEY_LAST_QUERIED_LIST) return context.dataStore.data .map { preferences -> // No type safety. preferences[listKey] ?: "" } } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/DiffCallbackChatUser.kt ================================================ package com.gowtham.letschat.utils import androidx.recyclerview.widget.DiffUtil import com.gowtham.letschat.db.data.ChatUser class DiffCallbackChatUser : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: ChatUser, newItem: ChatUser): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: ChatUser, newItem: ChatUser): Boolean { return oldItem.isSelected == newItem.isSelected && oldItem.locallySaved==newItem.locallySaved && oldItem.unRead==newItem.unRead && oldItem.localName ==newItem.localName } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Event.kt ================================================ package com.gowtham.letschat.utils open class Event(private val content: T) { var hasBeenHandled = false private set // Allow external read but not write /** * Returns the content and prevents its use again. */ fun getContentIfNotHandled(): T? { return if (hasBeenHandled) { null } else { hasBeenHandled = true content } } /** * Returns the content, even if it's already been handled. */ fun peekContent(): T = content } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Events/EventAudioMsg.kt ================================================ package com.gowtham.letschat.utils.Events class EventAudioMsg(val isPlaying: Boolean) ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Events/EventUpdateRecycleItem.kt ================================================ package com.gowtham.letschat.utils.Events class EventUpdateRecycleItem(val adapterPosition: Int) ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/GroupMsgActionReceiver.kt ================================================ package com.gowtham.letschat.utils import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.annotation.CallSuper import androidx.core.app.RemoteInput import com.google.firebase.firestore.CollectionReference import com.gowtham.letschat.KEY_TEXT_REPLY import com.gowtham.letschat.MApplication import com.gowtham.letschat.TYPE_NEW_GROUP import com.gowtham.letschat.TYPE_NEW_GROUP_MESSAGE import com.gowtham.letschat.core.* import com.gowtham.letschat.db.daos.GroupDao import com.gowtham.letschat.db.daos.GroupMessageDao import com.gowtham.letschat.db.data.* import com.gowtham.letschat.di.GroupCollection import com.gowtham.letschat.di.MessageCollection import com.gowtham.letschat.models.UserProfile import dagger.hilt.android.AndroidEntryPoint import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject abstract class HiltGroupBroadcastReceiver : BroadcastReceiver(){ @CallSuper override fun onReceive(p0: Context?, p1: Intent?) { } } @AndroidEntryPoint class GroupMsgActionReceiver : HiltGroupBroadcastReceiver(), OnGrpMessageResponse { @Inject lateinit var preference: MPreference @GroupCollection @Inject lateinit var grpCollection: CollectionReference @Inject lateinit var groupDao: GroupDao @Inject lateinit var groupMsgStatusUpdater: GroupMsgStatusUpdater @Inject lateinit var groupMsgDao: GroupMessageDao lateinit var groupWithMsg: GroupWithMessages private var notificationId: Int=0 lateinit var context: Context private lateinit var myUserId: String private lateinit var group: Group private lateinit var profile: UserProfile override fun onReceive(context: Context?, intent: Intent?) { super.onReceive(context, intent) this.context=context!! myUserId = preference.getUid()!! profile=preference.getUserProfile()!! groupWithMsg = intent!!.getParcelableExtra(Constants.GROUP_DATA)!! group=groupWithMsg.group val notiIdString = groupWithMsg.group.createdAt.toString() //last 4 digits as notificationId notificationId= notiIdString.substring(notiIdString.length - 4) .toInt() if (intent.action == Constants.ACTION_MARK_AS_READ) { groupMsgStatusUpdater.updateToSeen(myUserId, groupWithMsg.messages, group.id) Utils.removeNotificationById(context, notificationId) updateOnDb() }else{ val reply = getMessageText(intent) if (!reply.isNullOrBlank()) { val txtMessage=TextMessage(reply) val message = createMessage() message.textMessage=txtMessage val messageSender = GroupMsgSender(grpCollection) messageSender.sendMessage(message, group,this) } } } private fun createMessage(): GroupMessage { val toUsers=groupWithMsg.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 = myUserId, to = toUsers, profile.userName, profile.image, statusList, deliveryTimeList, deliveryTimeList) } private fun updateOnDb() { val list= groupWithMsg.messages.filter { Utils.myMsgStatus(myUserId,it)<3 && it.from!=myUserId }.map { it.status[Utils.myIndexOfStatus(myUserId,it)]=3 it } //change status to seen for other messagea of the user UserUtils.setUnReadCountGroup(groupDao,groupWithMsg.group) UserUtils.insertMutlipleGroupMsg(groupMsgDao,list) } private fun getMessageText(intent: Intent): String? { return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(KEY_TEXT_REPLY).toString() } override fun onSuccess(message: GroupMessage) { Utils.removeNotificationById(context, notificationId) //update to seen status groupMsgStatusUpdater.updateToSeen(myUserId, groupWithMsg.messages,group.id) updateOnDb() UserUtils.insertGroupMsg(groupMsgDao,message) for (user in group.members!!) { val token = user.user.token if (token.isNotEmpty()) UserUtils.sendPush(context, TYPE_NEW_GROUP_MESSAGE, Json.encodeToString(message), token, user.id) } } override fun onFailed(message: GroupMessage) { UserUtils.insertGroupMsg(groupMsgDao,message) } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/ImageUtils.kt ================================================ package com.gowtham.letschat.utils import android.Manifest import android.app.Activity import android.content.ContentResolver import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.text.TextUtils import android.webkit.MimeTypeMap import android.widget.ImageView import androidx.core.content.FileProvider import androidx.fragment.app.Fragment import coil.load import coil.request.CachePolicy import coil.transform.CircleCropTransformation import com.canhub.cropper.CropImage import com.canhub.cropper.CropImageView import com.gowtham.letschat.R import com.gowtham.letschat.fragments.FImageSrcSheet import com.gowtham.letschat.fragments.SheetListener import java.io.* import kotlin.random.Random object ImageUtils { private const val FROM_GALLERY = 116 private const val TAKE_PHOTO = 111 private var photoUri: Uri? = null fun askPermission(context: Fragment) { if (checkStoragePermission(context)) showCameraOptions(context) } fun loadUserImage(imageView: ImageView, imgUrl: String){ imageView.load(imgUrl) { crossfade(true) crossfade(300) diskCachePolicy(CachePolicy.ENABLED) placeholder(R.drawable.ic_other_user) error(R.drawable.ic_other_user) transformations(CircleCropTransformation()) } } private fun checkStoragePermission(context: Fragment): Boolean { return Utils.checkPermission( context, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA ) } private fun showCameraOptions(context: Fragment) { photoUri = null val builder = FImageSrcSheet.newInstance(Bundle()) builder.addListener(object : SheetListener { override fun selectedItem(index: Int) { if (index == 0) takePhoto(context.requireActivity()) else chooseGallery(context.requireActivity()) } },) builder.show(context.childFragmentManager, "") } public fun chooseGallery(context: Activity) { try { val intent = Intent(Intent.ACTION_PICK) intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*") context.startActivityForResult(intent, FROM_GALLERY) } catch (e: Exception) { e.printStackTrace() } } public fun takePhoto(context: Activity) { val fileName = "Snap_" + System.currentTimeMillis() / 1000 + ".jpg" openCameraIntent(context, MediaStore.ACTION_IMAGE_CAPTURE, fileName, TAKE_PHOTO) } private fun openCameraIntent( context: Activity, action: String, fileName: String, reqCode: Int) { try { val intent = Intent(action) if (intent.resolveActivity(context.packageManager) != null) { val file = File(createImageFolder(context, ""), fileName) photoUri = if (isNougat()) FileProvider.getUriForFile(context, providerPath(context), file) else Uri.fromFile(file) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri) context.startActivityForResult(intent, reqCode) context.overridePendingTransition( android.R.anim.fade_in, android.R.anim.fade_out ) } else context.toast("Camera not available") } catch (e: java.lang.Exception) { e.printStackTrace() } } fun cropImage(context: Activity, data: Intent?, squareCrop: Boolean = true) { val imgUri: Uri? = getPhotoUri(data) imgUri?.let { val cropImage = CropImage.activity(imgUri) .setOutputCompressFormat(Bitmap.CompressFormat.JPEG) .setGuidelines(CropImageView.Guidelines.ON) if (squareCrop) cropImage.setAspectRatio(1, 1) cropImage.start(context) } } fun getCroppedImage(data: Intent?): Uri? { try { val result = CropImage.getActivityResult(data) return result?.originalUri } catch (e: java.lang.Exception) { e.printStackTrace() } return null } private fun getPhotoUri(data: Intent?): Uri? { return if (data == null || data.data == null) photoUri else data.data } private fun createImageFolder(context: Context, path: String): String? { val folderPath = context.getExternalFilesDir("") ?.absolutePath + "/" + context.getString(R.string.app_name) try { val file = File("$folderPath/$path") if (!file.exists()) file.mkdirs() return file.absolutePath } catch (e: java.lang.Exception) { e.printStackTrace() } return folderPath } private fun isNougat(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N } private fun providerPath(context: Context): String { return context.packageName + ".fileprovider" } fun onImagePerResult(context: Fragment, vararg result: Int) { if (Utils.isPermissionOk(*result)) showCameraOptions(context) else context.requireContext().toast(R.string.txt_file_p_error) } fun getUriPath(context: Context, uri: Uri, vararg data: String): String? { return if (uri.toString() .contains(providerPath(context)) ) uri.path else if (isGoogleOldPhotosUri(uri)) uri.lastPathSegment else if (isGoogleNewPhotosUri( uri ) || isPicasaPhotoUri(uri) ) copyFile(context, uri, *data) else { val result: String? val cursor = context.contentResolver.query(uri, null, null, null, null) if (cursor == null) result = uri.path else { cursor.moveToFirst() result = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA)) cursor.close() } result ?: "" } } private fun isGoogleOldPhotosUri(uri: Uri): Boolean { return "com.google.android.apps.photos.content" == uri.authority } private fun isGoogleNewPhotosUri(uri: Uri): Boolean { return "com.google.android.apps.photos.contentprovider" == uri.authority } private fun isPicasaPhotoUri(uri: Uri?): Boolean { return (uri != null && !TextUtils.isEmpty(uri.authority) && (uri.authority!!.startsWith("com.android.gallery3d") || uri.authority!!.startsWith("com.google.android.gallery3d"))) } private fun copyFile(context: Context, uri: Uri, vararg data: String): String? { var filePath: String var inputStream: InputStream? = null var outStream: BufferedOutputStream? = null try { val extension = getExtension(context, uri, data[1]) inputStream = context.contentResolver.openInputStream(uri) val extDir = context.externalCacheDir if (extDir == null || inputStream == null) return "" filePath = (extDir.absolutePath + "/" + data[0] + "_" + Random.nextInt(100) + extension) outStream = BufferedOutputStream(FileOutputStream(filePath)) val buf = ByteArray(2048) var len: Int while (inputStream.read(buf).also { len = it } > 0) { outStream.write(buf, 0, len) } } catch (e: java.lang.Exception) { e.printStackTrace() filePath = "" } finally { try { inputStream?.close() outStream?.close() } catch (e: IOException) { e.printStackTrace() } } return filePath } private fun getExtension(context: Context, uri: Uri, actual: String): String { try { val extension: String? extension = if (uri.scheme != null && uri.scheme == ContentResolver.SCHEME_CONTENT) { val mime = MimeTypeMap.getSingleton() mime.getExtensionFromMimeType(context.contentResolver.getType(uri)) } else MimeTypeMap.getFileExtensionFromUrl( Uri.fromFile(File(uri.path)).toString() ) return if (extension == null || extension.isEmpty()) actual else extension } catch (e: java.lang.Exception) { e.printStackTrace() } return actual } fun loadGalleryImage(url: String, imageView: ImageView) { imageView.load(url){ crossfade(true) crossfade(300) diskCachePolicy(CachePolicy.ENABLED) placeholder(R.drawable.ic_gal_pholder) error(R.drawable.ic_broken_image) } } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/ItemClickListener.kt ================================================ package com.gowtham.letschat.utils import android.view.View interface ItemClickListener { fun onItemClicked(v: View, position: Int) } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/LoadState.kt ================================================ package com.gowtham.letschat.utils sealed class LoadState { class OnSuccess(val data: Any?=null): LoadState(){ override fun toString(): String { return "OnSuccess State" } } class OnFailure(val e: Exception): LoadState(){ override fun toString(): String { return "OnFailure State" } } object OnLoading : LoadState() { override fun toString(): String { return "OnLoading State" } } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/LogInFailedState.kt ================================================ package com.gowtham.letschat.utils enum class LogInFailedState { Verification, SignIn } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/LogMessage.kt ================================================ package com.gowtham.letschat.utils import android.util.Log import com.gowtham.letschat.BuildConfig.DEBUG object LogMessage { private val logVisible = DEBUG internal fun v(msg: String) { if (logVisible) Log.v("LetsChat",msg) } internal fun e(msg: String) { if (logVisible) Log.e("LetsChat",msg) } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/MPreference.kt ================================================ package com.gowtham.letschat.utils import android.content.Context import android.content.SharedPreferences import com.google.gson.Gson import com.gowtham.letschat.models.ModelMobile import com.gowtham.letschat.models.UserProfile import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton class MPreference @Inject constructor(@ApplicationContext private val context: Context) { private val UID = "userId" private val LOGIN="login" private val USER="user" private val MOBILE="mobile" private val TOKEN="token" private val ONLINE_USER="online_user" private val ONLINE_GROUP="online_group" private val LOGIN_TIME="login_time" private val LAST_LOGGED_DEVICE_SAME="last_logged_device_same" private val PREFS_FILENAME = "com.gowtham.letschat.prefs" private val sharedPreferences: SharedPreferences = context.getSharedPreferences(PREFS_FILENAME, 0) private val editor = sharedPreferences.edit() private fun storeString(key: String, value: String) { editor.run { putString(key, value) apply() } } private fun storeLong(key: String, value: Long) { editor.run { putLong(key, value) apply() } } private fun storeBoolean(key: String, value: Boolean) = editor.run { putBoolean(key, value) apply() } private fun getString(key: String) = sharedPreferences.getString(key, "") fun clearAll() = editor?.run { clear() apply() } fun setLogin(){ storeBoolean(LOGIN, true)} fun setLastDevice(same: Boolean){ storeBoolean(LAST_LOGGED_DEVICE_SAME, same)} fun setLogInTime(){ storeLong(LOGIN_TIME,System.currentTimeMillis()) } fun getLogInTime()= sharedPreferences.getLong(LOGIN_TIME, 0) fun setCurrentUser(id: String){ storeString(ONLINE_USER, id) } fun clearCurrentUser() { setCurrentUser("") } fun getOnlineUser(): String { return getString(ONLINE_USER) ?: "" } fun setCurrentGroup(id: String){ storeString(ONLINE_GROUP, id) } fun clearCurrentGroup() { setCurrentGroup("") } fun getOnlineGroup(): String { return getString(ONLINE_GROUP) ?: "" } fun isSameDevice()= sharedPreferences.getBoolean(LAST_LOGGED_DEVICE_SAME, true) fun isLoggedIn()= sharedPreferences.getBoolean(LOGIN, false) fun isNotLoggedIn()= !isLoggedIn() fun setUid(uid: String) = storeString(UID, uid) fun getUid()= getString(UID) fun saveProfile(profile: UserProfile){ storeString(USER, Gson().toJson(profile)) } fun saveMobile(mobile: ModelMobile){ storeString(MOBILE, Gson().toJson(mobile)) } fun updatePushToken(token: String){ storeString(TOKEN, token) } fun getPushToken() = getString(TOKEN) fun getUserProfile(): UserProfile? { val str=getString(USER) if (str.isNullOrBlank()) return null return Gson().fromJson(str, UserProfile::class.java) } fun getMobile(): ModelMobile? { val str=getString(MOBILE) if (str.isNullOrBlank()) return null return Gson().fromJson(str, ModelMobile::class.java) } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/NActionReceiver.kt ================================================ package com.gowtham.letschat.utils import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.annotation.CallSuper import androidx.core.app.RemoteInput import com.google.firebase.firestore.CollectionReference import com.gowtham.letschat.KEY_TEXT_REPLY 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.daos.MessageDao import com.gowtham.letschat.db.data.ChatUserWithMessages import com.gowtham.letschat.db.data.Message import com.gowtham.letschat.db.data.TextMessage import com.gowtham.letschat.di.MessageCollection import com.gowtham.letschat.utils.Constants.ACTION_MARK_AS_READ import com.gowtham.letschat.utils.Constants.ACTION_REPLY import com.gowtham.letschat.utils.Constants.CHAT_DATA import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import javax.inject.Inject abstract class HiltBroadcastReceiver : BroadcastReceiver(){ @CallSuper override fun onReceive(context: Context?, intent: Intent?) { } } @AndroidEntryPoint class NActionReceiver : HiltBroadcastReceiver(), OnMessageResponse { @Inject lateinit var preference: MPreference @Inject lateinit var messageDao: MessageDao @Inject lateinit var messageStatusUpdater: MessageStatusUpdater @Inject lateinit var dbRepo: DbRepository @MessageCollection @Inject lateinit var messageCollection: CollectionReference private var notificationId: Int=0 lateinit var context: Context lateinit var chatUser: ChatUserWithMessages private lateinit var myUserId: String private lateinit var chatUserId: String override fun onReceive(context: Context?, intent: Intent?) { super.onReceive(context, intent) try { this.context=context!! myUserId = preference.getUid()!! chatUser = intent!!.getParcelableExtra(CHAT_DATA)!! val notiIdString = chatUser.user.user.createdAt.toString() //last 4 digits as notificationId notificationId= notiIdString.substring(notiIdString.length - 4) .toInt() chatUserId = UserUtils.getChatUserId(myUserId, chatUser.messages.last()) if (intent.action == ACTION_MARK_AS_READ) { chatUser.messages.let { messageStatusUpdater.updateToSeen( chatUserId,chatUser.user.documentId!!,it) } Utils.removeNotificationById(context, notificationId) updateOnDb() } else if (intent.action == ACTION_REPLY) { val reply = getMessageText(intent) if (reply.isNotBlank()) { val message = createMessage(reply, myUserId, chatUserId) message.chatUserId=chatUserId val messageSender = MessageSender(messageCollection, dbRepo, chatUser.user, this) messageSender.checkAndSend(myUserId, chatUserId, message) } } } catch (e: Exception) { e.printStackTrace() } } private fun updateOnDb() { val list= chatUser.messages.filter { it.status<3 && it.to==myUserId }.map { it.status=3 it } //seen message other message of this user chatUser.user.unRead=0 dbRepo.insertUser(chatUser.user) CoroutineScope(Dispatchers.IO).launch { dbRepo.insertMultipleMessage(list.toMutableList()) } } private fun createMessage(reply: String, myUserId: String, chatUserId: String): Message { val profile = preference.getUserProfile()!! val txtMessage = TextMessage(reply) return Message( System.currentTimeMillis(), from = myUserId, to = chatUserId, senderName = profile.userName, senderImage = profile.image, textMessage = txtMessage, status = 0 ) } private fun getMessageText(intent: Intent): String { return RemoteInput.getResultsFromIntent(intent)?.getCharSequence(KEY_TEXT_REPLY).toString() } override fun onSuccess(message: Message) { Utils.removeNotificationById(context, notificationId) //update to seen status chatUser.messages.let { messageStatusUpdater.updateToSeen( chatUserId,chatUser.user.documentId!!,it) } updateOnDb() val token=chatUser.user.user.token if (token.isNotBlank()) UserUtils.sendPush(context, TYPE_NEW_MESSAGE,Json.encodeToString(message),token,message.to) dbRepo.insertMessage(message) } override fun onFailed(message: Message) { dbRepo.insertMessage(message) } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/NotificationUtils.kt ================================================ package com.gowtham.letschat.utils import android.app.Notification import android.app.PendingIntent import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person import androidx.core.app.RemoteInput import com.gowtham.letschat.FirebasePush import com.gowtham.letschat.GROUP_KEY import com.gowtham.letschat.KEY_TEXT_REPLY import com.gowtham.letschat.R import com.gowtham.letschat.db.data.* import com.gowtham.letschat.ui.activities.MainActivity object NotificationUtils { fun getSummaryNotification(context: Context, manager: NotificationManagerCompat): Notification { return Utils.createBuilder(context, manager,true) .setContentTitle("emailObject.getSummary()") .setContentText("${FirebasePush.messageCount} new messages") .setSmallIcon(R.drawable.ic_stat_name) .setStyle( NotificationCompat.InboxStyle() .addLine("Alex Faarborg Check this out") .addLine("Jeff Chang Launch Party") .addLine("Jeff Chang Launch Party") .setBigContentTitle("${FirebasePush.messageCount} new messages") .setSummaryText("${FirebasePush.messageCount} new messages from ${FirebasePush.personCount} friends")) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setGroup(GROUP_KEY) .setContentIntent(getHomeIntent(context)) .setGroupSummary(true) .build() } fun getReplyAction(context: Context, user: ChatUserWithMessages): NotificationCompat.Action { val replyLabel = "Reply" val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run { setLabel(replyLabel) build() } return NotificationCompat.Action.Builder( R.drawable.ic_send, "Reply", getReplyPIntent(context,user)) .addRemoteInput(remoteInput) .build() } fun getGroupReplyAction(context: Context, user: GroupWithMessages): NotificationCompat.Action { val replyLabel = "Reply" val remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run { setLabel(replyLabel) build() } return NotificationCompat.Action.Builder( R.drawable.ic_send, "Reply", getGroupReplyPIntent(context,user)) .addRemoteInput(remoteInput) .build() } private fun getHomeIntent(context: Context): PendingIntent { val intent= Intent(context, MainActivity::class.java) intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) return PendingIntent.getActivity( context, 2, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) } fun getMarkAsPIntent(context: Context, user: ChatUserWithMessages): PendingIntent { val snoozeIntent = Intent(context, NActionReceiver::class.java).apply { putExtra(Constants.CHAT_DATA,user) action = Constants.ACTION_MARK_AS_READ } return PendingIntent.getBroadcast(context, 1, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT) } private fun getReplyPIntent(context: Context, user: ChatUserWithMessages): PendingIntent { val intent = Intent(context, NActionReceiver::class.java).apply { putExtra(Constants.CHAT_DATA,user) action = Constants.ACTION_REPLY } return PendingIntent.getBroadcast(context, 5, intent, PendingIntent.FLAG_UPDATE_CURRENT) } fun getGroupReplyPIntent(context: Context, user: GroupWithMessages): PendingIntent { val intent = Intent(context, GroupMsgActionReceiver::class.java).apply { putExtra(Constants.GROUP_DATA,user) action = Constants.ACTION_REPLY } return PendingIntent.getBroadcast(context, 6, intent, PendingIntent.FLAG_UPDATE_CURRENT) } fun getGroupMarkAsPIntent(context: Context, user: GroupWithMessages): PendingIntent { val snoozeIntent = Intent(context, GroupMsgActionReceiver::class.java).apply { putExtra(Constants.GROUP_DATA,user) action = Constants.ACTION_MARK_AS_READ } return PendingIntent.getBroadcast(context, 1, snoozeIntent, PendingIntent.FLAG_UPDATE_CURRENT) } fun getPIntent(context: Context, user: ChatUser): PendingIntent { val intent= Intent(context, MainActivity::class.java) intent.action= Constants.ACTION_NEW_MESSAGE intent.putExtra(Constants.CHAT_USER_DATA, user) intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) return PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) } fun getGroupMsgIntent(context: Context, group: Group): PendingIntent { val intent= Intent(context, MainActivity::class.java) intent.action= Constants.ACTION_GROUP_NEW_MESSAGE intent.putExtra(Constants.GROUP_DATA, group) intent.flags = (Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) return PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT) } fun getStyle(context: Context, person: Person, user: ChatUserWithMessages) : NotificationCompat.Style{ val style= NotificationCompat.MessagingStyle(person) val chatPerson: Person = Person.Builder().setIcon(null) .setKey(user.user.id).setName(user.user.localName).build() val messages=user.messages.filter { it.status<3 && it.from!=MPreference(context).getUid()} for (message in messages) { FirebasePush.messageCount +=1 style.addMessage(BindingAdapters.getLastMsgTxt(message), message.createdAt, chatPerson) } return style } fun getGroupStyle( context: Context, myUserId: String, person: Person, group: GroupWithMessages) : NotificationCompat.Style{ val style= NotificationCompat.MessagingStyle(person) val members=group.group.members ?: ArrayList() val messages=group.messages val filterMessages=messages.filter { it.from != myUserId && Utils.myMsgStatus(myUserId, it) < 3 } if (filterMessages.isNotEmpty()) { for (msg in filterMessages) style.addMessage(getGroupMsg(members,msg), msg.createdAt,person) FirebasePush.messageCount += 1 } return style } private fun getGroupMsg(members: ArrayList, msg: GroupMessage): String { val user=members.first { it.id==msg.from }.localName return "$user : ${BindingAdapters.getLastMsgTxt(msg)}" } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/OnSuccessListener.kt ================================================ package com.gowtham.letschat.utils interface OnSuccessListener { fun onResult(success: Boolean,data: Any?=null) } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/ScreenState.kt ================================================ package com.gowtham.letschat.utils sealed class ScreenState { object IdleState : ScreenState(){ override fun toString(): String { return "IdleState State" } } object SearchState : ScreenState(){ override fun toString(): String { return "SearchState State" } } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/UserUtils.kt ================================================ package com.gowtham.letschat.utils import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.pm.PackageManager import android.os.Build import android.provider.ContactsContract import android.provider.Settings import com.fcmsender.FCMSender import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.FirebaseDatabase import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query import com.google.firebase.installations.FirebaseInstallations import com.google.firebase.ktx.Firebase import com.google.firebase.storage.StorageReference import com.google.firebase.storage.ktx.storage import com.gowtham.letschat.MApplication import com.gowtham.letschat.core.* import com.gowtham.letschat.db.ChatUserDatabase import com.gowtham.letschat.db.DbRepository import com.gowtham.letschat.db.daos.GroupDao import com.gowtham.letschat.db.daos.GroupMessageDao import com.gowtham.letschat.db.data.* import com.gowtham.letschat.fragments.group_chat_home.AdGroupChatHome import com.gowtham.letschat.fragments.single_chat_home.AdSingleChatHome import com.gowtham.letschat.models.Contact import com.gowtham.letschat.models.ModelDeviceDetails import com.gowtham.letschat.models.UserProfile import com.gowtham.letschat.models.UserStatus import com.gowtham.letschat.ui.activities.ActSplash 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 org.greenrobot.eventbus.EventBus import org.json.JSONObject import timber.log.Timber import java.util.* import kotlin.collections.ArrayList import kotlin.collections.HashMap import kotlin.system.measureNanoTime object UserUtils { var queriedList=ArrayList() var resultCount=0 const val NOTIFICATION_ID=22 var totalRecursionCount=0 fun updatePushToken(context: Context,userCollection: CollectionReference, isSave: Boolean) { try { if (Utils.isNetConnected(context)) { FirebaseInstallations.getInstance().getToken(false).addOnSuccessListener { result-> MPreference(context).updatePushToken(result.token) if (isSave) updateDeviceDetails(context,userCollection) } } } catch (e: Exception) { e.printStackTrace() } } private fun updateDeviceDetails(context: Context,userCollection: CollectionReference) { val preference = MPreference(context) val token = preference.getPushToken() Timber.v("AAA ${preference.getUid()}") Timber.v("BB ${preference.getUserProfile()?.uId}") if (token.isNullOrEmpty()) updatePushToken(context,userCollection, true) else if (Utils.isNetConnected(context)) { val profile = preference.getUserProfile()?.apply { this.token=token this.deviceDetails= Json.decodeFromString(getDeviceInfo(context).toString()) } val updateData = hashMapOf( "token" to token, "updatedAt" to System.currentTimeMillis(), "device_details" to Json.decodeFromString(getDeviceInfo(context).toString()), ) userCollection.document(preference.getUid()!!).update(updateData).addOnSuccessListener { preference.saveProfile(profile!!) LogMessage.v("Token Updated $token##") } } } fun getStorageRef(context: Context): StorageReference { val preference = MPreference(context) val ref = Firebase.storage.getReference("Users") return ref.child(preference.getUid().toString()) } fun getDocumentRef(context: Context): DocumentReference { val preference = MPreference(context) val db = FirebaseFirestore.getInstance() return db.collection("Users").document(preference.getUid()!!) } fun getMessageSubCollectionRef(): Query { val db = FirebaseFirestore.getInstance() return db.collectionGroup("messages") } fun getGroupMsgSubCollectionRef(): Query { val db = FirebaseFirestore.getInstance() return db.collectionGroup("group_messages") } fun fetchContacts(context: Context): List { val preference=MPreference(context) val names = ArrayList() val numbers = ArrayList() val contacts=ArrayList() val uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI val projection = arrayOf( ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER ) val selection: String? = null //it's like a where concept in mysql val selectionArgs: Array? = null val sortOrder: String? = null val resolver = context.contentResolver val cursor = resolver.query(uri, projection, selection, selectionArgs, sortOrder) while (cursor!!.moveToNext()) { val name = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)) val number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)) if(number.contains(preference.getMobile()!!.number)) continue names.add(name) numbers.add(number) contacts.add(Contact(name, number)) } cursor.close() val hashMap=getCountryCodeRemovedList(context, contacts) contacts.clear() for (number in hashMap.keys){ contacts.add(Contact(hashMap[number].toString(), number)) } return contacts.sortedWith(compareBy { it.name }) } private fun getCountryCodeRemovedList(context: Context, contacts: ArrayList): HashMap { val hashMap:HashMap = HashMap() //hashmap to get rid of duplication val preference=MPreference(context) contacts.forEach { contact -> if (contact.mobile.length <= 5 || contact.mobile.contains(preference.getMobile()?.number!!)) return@forEach var mobile=contact.mobile for (country in Countries.getCountries()) { if (mobile.contains(country.noCode)) { mobile = contact.mobile.replace(country.noCode, "") break } } hashMap[mobile.replace(" ", "")] = contact.name } return hashMap } fun getDeviceInfo(context: Context): JSONObject { try { val deviceInfo = JSONObject() deviceInfo.put("device_id", getDeviceId(context)) deviceInfo.put("device_model", Build.MODEL) deviceInfo.put("device_brand", Build.BOARD) deviceInfo.put("device_country", Locale.getDefault()) deviceInfo.put("device_os_v", Build.VERSION.RELEASE) deviceInfo.put("app_version", getVersionName(context)) deviceInfo.put("package_name", context.packageName) deviceInfo.put("device_type", "android") return deviceInfo } catch (e: java.lang.Exception) { e.printStackTrace() } return JSONObject() } @SuppressLint("HardwareIds") fun getDeviceId(context: Context): String? { return Settings.Secure.getString( context.contentResolver, Settings.Secure.ANDROID_ID ) } private fun getVersionName(context: Context): String? { try { val packageName = context.packageName val pInfo = context.packageManager.getPackageInfo(packageName, 0) return pInfo.versionName } catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() } return "1.0" } fun logOut(context: Activity, preference: MPreference,db: ChatUserDatabase) { try { CoroutineScope(Dispatchers.IO).launch { db.clearAllTables() } EventBus.getDefault().post(UserStatus("offline")) ChatHandler.removeListeners() GroupChatHandler.removeListener() ChatUserProfileListener.removeListener() AdSingleChatHome.allChatList= emptyList().toMutableList() AdGroupChatHome.allList= emptyList().toMutableList() FirebaseAuth.getInstance().signOut() preference.clearAll() Utils.startNewActivity(context, ActSplash::class.java) } catch (e: Exception) { e.printStackTrace() } } fun sendPush(context: Context, type: String,body: String, token: String,to: String) { try { val data=JSONObject() val pushData=JSONObject() data.put("type", type) data.put("message_body",body) data.put("to",to) pushData.put("data",data) val push = FCMSender.Builder() .serverKey(Constants.FCM_SERVER_KEY) .setData(pushData) .toTokenOrTopic(token) .responseListener(object : FCMSender.ResponseListener { override fun onFailure(errorCode: Int,message: String) { LogMessage.v("notification sent Failed to $token") } override fun onSuccess(response: String) { LogMessage.v("notification sent Successfully to $token") } }).build() push.sendPush(context) } catch (e: Exception) { e.printStackTrace() } } fun setUnReadCountZero(repo: DbRepository, chatUser: ChatUser) { try { val time= measureNanoTime { chatUser.unRead=0 repo.insertUser(chatUser) } Timber.v("Taken time $time") } catch (e: Exception) { e.printStackTrace() } } fun getChatUserId(fromUser: String, message: Message)= if (message.from != fromUser) message.from else message.to fun sendTypingStatus(database: FirebaseDatabase, isTyping: Boolean, vararg users: String) { try { val typingRef = database.getReference("/Users/${users[0]}/typing_status") val chatUserRef = database.getReference("/Users/${users[0]}/chatuser") typingRef.setValue(if (isTyping) "typing" else "not_typing") chatUserRef.setValue(users[1]) typingRef.onDisconnect().setValue("not_typing") chatUserRef.onDisconnect().setValue("") } catch (e: Exception) { e.printStackTrace() } } fun updateContactsProfiles(listener :QueryCompleteListener?): Boolean { Timber.v("Query Called") val allContacts = fetchContacts(MApplication.appContext).toMutableList() val listOfMobiles = ArrayList() allContacts.forEach { listOfMobiles.add(it.mobile) } if(listOfMobiles.isEmpty()) return false return makeQueryRecursively(listOfMobiles, 1,listener ?: onQueryCompleted) } private tailrec fun makeQueryRecursively(listOfMobNos: ArrayList, position: Int ,listener :QueryCompleteListener): Boolean { val firstTen = ArrayList() val size = if (listOfMobNos.size < 10) listOfMobNos.size else 10 for (index in 0 until size) firstTen.add(listOfMobNos[index]) listOfMobNos.subList(0, size).clear() //remove queried elements val contactsQuery = ContactsQuery(firstTen, position,listener) contactsQuery.makeQuery() return if (listOfMobNos.isEmpty()) { totalRecursionCount=position LogMessage.v("Queried times $position") true }else makeQueryRecursively(listOfMobNos, position + 1,listener) } fun getChatUser( doc: UserProfile, chatUsers: List, savedName: String): ChatUser { var existData: ChatUser? = null if (chatUsers.isNotEmpty()) { val contact = chatUsers.firstOrNull { it.id == doc.uId } contact?.let { existData=it } } return existData?.apply { user = doc localName = savedName locallySaved=true } ?: ChatUser(doc.uId.toString(), savedName, doc,locallySaved = true) } private val onQueryCompleted=object : QueryCompleteListener { override fun onQueryCompleted(queriedList: ArrayList) { try { Timber.v("onQueryCompleted ${queriedList.size}") val localContacts= fetchContacts(MApplication.appContext) val finalList = ArrayList() //set localsaved name to queried users CoroutineScope(Dispatchers.IO).launch { val chatUsers=MApplication.userDaoo.getChatUserList() withContext(Dispatchers.Main){ for(doc in queriedList){ val savedNumber=localContacts.firstOrNull { it.mobile == doc.mobile?.number } if(savedNumber!=null){ val chatUser=getChatUser(doc,chatUsers,savedNumber.name) finalList.add(chatUser) } } setDefaultValues() withContext(Dispatchers.IO){ MApplication.userDaoo.insertMultipleUser(finalList) } } } } catch (e: Exception) { e.printStackTrace() } } } private fun setDefaultValues() { totalRecursionCount =0 resultCount =0 queriedList.clear() } fun setUnReadCountGroup(groupDao: GroupDao, group: Group) { CoroutineScope(Dispatchers.IO).launch { group.unRead=0 groupDao.insertGroup(group) } } fun insertGroupMsg(groupMsgDao: GroupMessageDao, message: GroupMessage) { CoroutineScope(Dispatchers.IO).launch { groupMsgDao.insertMessage(message) } } fun insertMutlipleGroupMsg(groupMsgDao: GroupMessageDao, messages: List) { CoroutineScope(Dispatchers.IO).launch { groupMsgDao.insertMultipleMessage(messages) } } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Utils.kt ================================================ package com.gowtham.letschat.utils import android.annotation.SuppressLint import android.app.* import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Color import android.media.AudioAttributes import android.media.RingtoneManager import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.TextView import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.google.gson.Gson import com.google.gson.GsonBuilder import com.gowtham.letschat.R import com.gowtham.letschat.db.ChatUserDatabase import com.gowtham.letschat.db.data.GroupMessage import com.gowtham.letschat.models.Country import com.gowtham.letschat.models.UserStatus import java.text.SimpleDateFormat object Utils { private const val PERMISSION_REQ_CODE = 114 /* private const val MIN: Long=1000 * 60 private const val HOUR= MIN * 60 private const val DAY= HOUR* 24 private const val WEEK= DAY * 7 private const val MONTH= WEEK * 4 private const val YEAR= MONTH * 12*/ fun getDefaultCountry() = Country("IN", "India", "+91", "INR") fun clearNull(str: String?) = str?.trim() ?: "" @Suppress("DEPRECATION") fun isNetConnected(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) (capabilities != null && ((capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))) } else { val activeNetworkInfo = connectivityManager.activeNetworkInfo (activeNetworkInfo != null && activeNetworkInfo.isConnected) } } fun isNoInternet(context: Context) = !isNetConnected(context) fun getColor(context: Context, color: Int): Int { return ContextCompat.getColor(context, color) } fun checkPermission(context: Fragment, vararg permissions: String,reqCode: Int= PERMISSION_REQ_CODE): Boolean { var allPermitted = false for (permission in permissions) { allPermitted = (ContextCompat.checkSelfPermission(context.requireContext(), permission) == PackageManager.PERMISSION_GRANTED) if (!allPermitted) break } if (allPermitted) return true context.requestPermissions( permissions, reqCode ) return false } fun getGSONObj(): Gson { return GsonBuilder().create() } /* fun fromGson(json: String?, className: Class?): T { return getGSONObj().fromJson(json, className) }*/ fun isPermissionOk(vararg results: Int): Boolean { var isAllGranted = true for (result in results) { if (PackageManager.PERMISSION_GRANTED != result) { isAllGranted = false break } } return isAllGranted } fun startNewActivity(activity: Activity, className: Class<*>?) { val intent = Intent(activity, className) intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK activity.startActivity(intent) activity.finish() } fun askContactPermission(context: Fragment): Boolean { return checkPermission( context, android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.WRITE_CONTACTS ) } fun isContactPermissionOk(context: Context): Boolean{ return (ContextCompat.checkSelfPermission(context, android.Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission( context, android.Manifest.permission.WRITE_CONTACTS ) == PackageManager.PERMISSION_GRANTED) } fun showLoggedInAlert( context: Activity, preference: MPreference, db: ChatUserDatabase) { try { val dialog = Dialog(context) dialog.setCancelable(false) dialog.setContentView(R.layout.alert_dialog) dialog.window!!.setLayout( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) dialog.findViewById(R.id.txt_log_out).setOnClickListener { dialog.dismiss() UserUtils.logOut(context, preference, db) } dialog.show() } catch (e: Exception) { } } /* fun dpToPx(dp: Int): Int { return (dp * Resources.getSystem().displayMetrics.density).toInt() } */ fun setOnlineStatus(txtView: TextView, status: UserStatus, uId: String) { txtView.visibility= View.VISIBLE txtView.text= when { status.typing_status=="typing" && uId==status.chatuser -> { "typing..." } status.status=="online" -> { "online" } status.last_seen>0L -> { "last seen ${getLastSeen(status.last_seen)}" } else -> { "..." } } } fun createBuilder( context: Context, manager: NotificationManagerCompat, isSummary: Boolean = false ): NotificationCompat.Builder { val channelId = context.packageName val notBuilder = NotificationCompat.Builder(context, channelId) notBuilder.setSmallIcon(R.drawable.ic_stat_name) notBuilder.setAutoCancel(true) notBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) notBuilder.setDefaults(Notification.DEFAULT_ALL) notBuilder.priority = NotificationCompat.PRIORITY_HIGH val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) notBuilder.setSound(soundUri) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val importance=if (isSummary) NotificationManager.IMPORTANCE_HIGH else NotificationManager.IMPORTANCE_DEFAULT val channel = NotificationChannel( channelId, context.getString(R.string.txt_notifications), importance ) channel.importance =importance channel.shouldShowLights() channel.lightColor = Color.BLUE channel.canBypassDnd() val audioAttributes = AudioAttributes.Builder() .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_NOTIFICATION) .build() channel.setSound(soundUri, audioAttributes) channel.description = context.getString(R.string.txt_not_description) notBuilder.setChannelId(channelId) manager.createNotificationChannel(channel) } return notBuilder } fun returnNManager(context: Context): NotificationManagerCompat { return NotificationManagerCompat.from(context) } fun removeNotification(context: Context){ val manager= returnNManager(context) manager.cancelAll() } fun removeNotificationById(context: Context, id: Int){ val manager= returnNManager(context) manager.cancel(id) } fun getGroupName(groupId: String) = groupId.substring(0, groupId.lastIndexOf("_")) fun closeKeyBoard(activity: Activity){ val view=activity.currentFocus val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager view?.let { imm.hideSoftInputFromWindow(it.windowToken, 0) } } fun showSoftKeyboard(activity: Activity, view: View) { if (view.requestFocus()) { val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) } } fun myMsgStatus(myUserId: String, msg: GroupMessage): Int{ val indexOfMine=myIndexOfStatus(myUserId, msg) return msg.status[indexOfMine] } fun myIndexOfStatus(myUserId: String, msg: GroupMessage): Int{ return msg.to.indexOf(myUserId) } @SuppressLint("SimpleDateFormat") fun getTime(sentTime: Long): String{ val currentTime=System.currentTimeMillis() val dayCount = (currentTime - sentTime)/(24 * 60 * 60 * 1000) val calender= java.util.Calendar.getInstance() calender.timeInMillis=sentTime val date=calender.time return when{ dayCount> 1L -> { //DD/MM/YYYY format SimpleDateFormat("dd/MMM/yyyy").format(date) } dayCount==1L -> { "Yesterday" } else->{ //hh:mm aa SimpleDateFormat("hh:mm aa").format(date) } } } @SuppressLint("SimpleDateFormat") fun getLastSeen(lastSeen: Long): String{ val currentTime=System.currentTimeMillis() val dayCount = (currentTime - lastSeen)/(24 * 60 * 60 * 1000) val calender= java.util.Calendar.getInstance() calender.timeInMillis=lastSeen val date=calender.time return when{ dayCount> 1L -> { //DD/MM/YYYY format SimpleDateFormat("dd/MMM/yyyy").format(date) } dayCount==1L -> { "yesterday ${SimpleDateFormat("hh:mm aa").format(date)}" } else->{ //hh:mm aa "today ${SimpleDateFormat("hh:mm aa").format(date)}" } } } fun edtValue(edtMsg: EditText): String { return edtMsg.text!!.trim().toString() } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/Validator.kt ================================================ package com.gowtham.letschat.utils import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber object Validator { fun isValidNo(code: String, mobileNo: String): Boolean { try { val phoneUtil = PhoneNumberUtil.getInstance() val phNumberProto: PhoneNumber = phoneUtil.parse( mobileNo, code ) return phoneUtil.isValidNumber(phNumberProto) } catch (e: Exception) { e.printStackTrace() } return false } fun isMobileNumberEmpty(mobileNo: String?): Boolean{ return mobileNo.isNullOrEmpty() } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/utils/ViewUtils.kt ================================================ package com.gowtham.letschat.utils import android.app.Activity import android.content.Context import android.os.Handler import android.os.Looper import android.text.Editable import android.util.Log import android.view.View import android.widget.EditText import android.widget.ProgressBar import android.widget.Toast import androidx.navigation.NavController import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import com.gowtham.letschat.R import com.gowtham.letschat.db.data.Message import com.gowtham.letschat.views.CustomProgressView import timber.log.Timber fun String.printMeD(){ Log.d("LetsChat:: ",this) } fun Context.toast(msg: String){ Toast.makeText(this,msg,Toast.LENGTH_SHORT).show() } fun Context.toastLong(msg: String){ Toast.makeText(this,msg,Toast.LENGTH_LONG).show() } fun Context.toast(msg: Int){ Toast.makeText(this,getString(msg),Toast.LENGTH_SHORT).show() } fun Context.toastNet(){ Toast.makeText(this,R.string.err_no_net,Toast.LENGTH_SHORT).show() } fun snack(context: Activity,msg: String){ Snackbar.make(context.findViewById(android.R.id.content),msg,2000).show() } fun snackNet(context: Activity){ Snackbar.make(context.findViewById(android.R.id.content),R.string.err_no_net,2000).show() } fun View.gone(){ this.visibility=View.GONE } fun View.show(){ this.visibility=View.VISIBLE } fun List.getUnreadCount(userId: String): Int { return this.filter { it.from==userId && it.status<3 }.size } fun View.hide(){ this.visibility=View.INVISIBLE } fun CustomProgressView.toggle(show: Boolean){ if (show) this.show() else this.dismiss(); } fun CustomProgressView.dismissIfShowing(){ if (this.isShowing) this.dismiss() } fun ProgressBar.toggle(show: Boolean){ if (show) this.show() else this.gone(); } fun EditText.trim() = this.text.toString().trim() fun Char.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this.toString()) fun NavController.isValidDestination(destination: Int): Boolean { return destination == this.currentDestination!!.id } fun RecyclerView.smoothScrollToPos(position: Int) { Handler(Looper.getMainLooper()).postDelayed({ this.smoothScrollToPosition(position) }, 300) } fun ListAdapter.updateList(list: List?) { this.submitList( if (list == this.currentList) { Timber.v("Same list") list.toList() } else { Timber.v("Not Same list") list } ) } fun ListAdapter.addRestorePolicy(){ stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/CustomEditText.kt ================================================ package com.gowtham.letschat.views import android.content.Context import android.os.Build import android.os.Bundle import android.util.AttributeSet import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.appcompat.widget.AppCompatEditText import androidx.core.os.BuildCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputConnectionCompat.OnCommitContentListener import androidx.core.view.inputmethod.InputContentInfoCompat class CustomEditText : AppCompatEditText { lateinit var imgTypeString: Array private var keyBoardInputCallbackListener: KeyBoardInputCallbackListener? = null constructor(context: Context) : super(context) { initView() } constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { initView() } private fun initView() { imgTypeString = arrayOf( "image/png", "image/gif", "image/jpeg", "image/webp" ) } override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection { val ic: InputConnection = super.onCreateInputConnection(outAttrs)!! EditorInfoCompat.setContentMimeTypes( outAttrs!!, imgTypeString ) return InputConnectionCompat.createWrapper(ic, outAttrs, callback) } val callback = OnCommitContentListener { inputContentInfo, flags, opts -> // read and display inputContentInfo asynchronously if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION != 0) { try { inputContentInfo.requestPermission() } catch (e: Exception) { return@OnCommitContentListener false // return false if failed } } var supported = false for (mimeType in imgTypeString) { if (inputContentInfo.description.hasMimeType(mimeType)) { supported = true break } } if (!supported) return@OnCommitContentListener false keyBoardInputCallbackListener?.onCommitContent(inputContentInfo, flags, opts) true // return true if succeeded } interface KeyBoardInputCallbackListener { fun onCommitContent( inputContentInfo: InputContentInfoCompat?, flags: Int, opts: Bundle? ) } fun setKeyBoardInputCallbackListener(keyBoardInputCallbackListener: KeyBoardInputCallbackListener?) { this.keyBoardInputCallbackListener = keyBoardInputCallbackListener } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/CustomProgress.kt ================================================ package com.gowtham.letschat.views import android.content.Context import android.graphics.BlendMode import android.graphics.BlendModeColorFilter import android.graphics.PorterDuff import android.os.Build import android.util.AttributeSet import android.widget.ProgressBar import androidx.annotation.RequiresApi import com.gowtham.letschat.R import com.gowtham.letschat.utils.Utils @Suppress("DEPRECATION") class CustomProgress : ProgressBar { constructor(context: Context) : super(context) { setTintColor(context) } constructor( context: Context, attrs: AttributeSet? ) : super(context, attrs) { setTintColor(context) } constructor( context: Context, attrs: AttributeSet?, defStyleAttr: Int ) : super(context, attrs, defStyleAttr) { setTintColor(context) } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) constructor( context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int ) : super(context, attrs, defStyleAttr, defStyleRes) { setTintColor(context) } private fun setTintColor(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { this.indeterminateDrawable.colorFilter = BlendModeColorFilter( Utils.getColor( context, R.color.colorTheme ), BlendMode.SRC_ATOP ) } else { this.indeterminateDrawable.setColorFilter( Utils.getColor( context, R.color.colorTheme ), PorterDuff.Mode.MULTIPLY ) } } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/CustomProgressView.kt ================================================ package com.gowtham.letschat.views import android.app.Dialog import android.content.Context import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.Window import com.gowtham.letschat.R class CustomProgressView constructor(context: Context) : Dialog(context) { init { val view: View = LayoutInflater.from(context).inflate(R.layout.progress_dialog, null) requestWindowFeature(Window.FEATURE_NO_TITLE) this.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) this.window?.setBackgroundDrawable( ColorDrawable(Color.TRANSPARENT) ) setCancelable(false) this.setContentView(view) } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/MainNavHostFragment.kt ================================================ package com.gowtham.letschat.views import android.content.Context import androidx.navigation.fragment.NavHostFragment import com.gowtham.letschat.fragments.MainFragmentFactory import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @AndroidEntryPoint class MainNavHostFragment : NavHostFragment(){ @Inject lateinit var fragmentFactory: MainFragmentFactory override fun onAttach(context: Context) { super.onAttach(context) childFragmentManager.fragmentFactory = fragmentFactory } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/PausableProgressBar.kt ================================================ package com.gowtham.letschat.views import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import android.view.animation.Animation import android.view.animation.LinearInterpolator import android.widget.FrameLayout import com.gowtham.letschat.R class PausableProgressBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { private var frontProgressView: View? = null private var maxProgressView: View? = null private var animation: PausableScaleAnimation? = null private var duration = DEFAULT_PROGRESS_DURATION.toLong() private var callback: Callback? = null private var isStarted = false init { LayoutInflater.from(context).inflate(R.layout.pausable_progress, this) frontProgressView = findViewById(R.id.front_progress) maxProgressView = findViewById(R.id.max_progress) } fun setDuration(duration: Long) { this.duration = duration if (animation != null){ animation = null startProgress() } } fun setCallback(callback: Callback) { this.callback = callback } fun setMax() { finishProgress(true) } fun setMin() { finishProgress(false) } fun setMinWithoutCallback() { maxProgressView!!.setBackgroundResource(R.color.progress_secondary) maxProgressView!!.visibility = View.VISIBLE if (animation != null) { animation!!.setAnimationListener(null) animation!!.cancel() } } fun setMaxWithoutCallback() { maxProgressView!!.setBackgroundResource(R.color.progress_max_active) maxProgressView!!.visibility = View.VISIBLE if (animation != null) { animation!!.setAnimationListener(null) animation!!.cancel() } } private fun finishProgress(isMax: Boolean) { if (isMax) maxProgressView!!.setBackgroundResource(R.color.progress_max_active) maxProgressView!!.visibility = if (isMax) View.VISIBLE else View.GONE if (animation != null) { animation!!.setAnimationListener(null) animation!!.cancel() if (callback != null) { callback!!.onFinishProgress() } } } fun startProgress() { maxProgressView!!.visibility = View.GONE if (duration <= 0) duration = DEFAULT_PROGRESS_DURATION animation = PausableScaleAnimation( 0f, 1f, 1f, 1f, Animation.ABSOLUTE, 0f, Animation.RELATIVE_TO_SELF, 0f ) animation!!.duration = duration animation!!.interpolator = LinearInterpolator() animation!!.setAnimationListener(object : Animation.AnimationListener { override fun onAnimationStart(animation: Animation) { if (isStarted) { return } isStarted = true frontProgressView!!.visibility = View.VISIBLE if (callback != null) callback!!.onStartProgress() } override fun onAnimationEnd(animation: Animation) { isStarted = false if (callback != null) callback!!.onFinishProgress() } override fun onAnimationRepeat(animation: Animation) { //NO-OP } }) animation!!.fillAfter = true frontProgressView!!.startAnimation(animation) } fun pauseProgress() { if (animation != null) { animation!!.pause() } } fun resumeProgress() { if (animation != null) { animation!!.resume() } } fun clear() { if (animation != null) { animation!!.setAnimationListener(null) animation!!.cancel() animation = null } } interface Callback { fun onStartProgress() fun onFinishProgress() } companion object { private const val DEFAULT_PROGRESS_DURATION = 4000L } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/PausableScaleAnimation.kt ================================================ package com.gowtham.letschat.views import android.view.animation.ScaleAnimation import android.view.animation.Transformation class PausableScaleAnimation internal constructor( fromX: Float, toX: Float, fromY: Float, toY: Float, pivotXType: Int, pivotXValue: Float, pivotYType: Int, pivotYValue: Float ) : ScaleAnimation( fromX, toX, fromY, toY, pivotXType, pivotXValue, pivotYType, pivotYValue ) { private var elapsedAtPause: Long = 0 private var isPaused = false override fun getTransformation( currentTime: Long, outTransformation: Transformation, scale: Float ): Boolean { if (isPaused && elapsedAtPause == 0L) { elapsedAtPause = currentTime - startTime } if (isPaused) { startTime = currentTime - elapsedAtPause } return super.getTransformation(currentTime, outTransformation, scale) } fun pause() { if (isPaused) return elapsedAtPause = 0 isPaused = true } fun resume() { isPaused = false } } ================================================ FILE: app/src/main/java/com/gowtham/letschat/views/StoriesProgressView.kt ================================================ package com.gowtham.letschat.views import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.LinearLayout import com.gowtham.letschat.R class StoriesProgressView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { private val progressBars: MutableList = ArrayList() private var storiesListener: StoriesListener? = null private var storiesCount = -1 private var current = -1 private var isSkipStart = false private var isReverseStart = false private var position = -1 private var isComplete = false init { orientation = HORIZONTAL val typedArray = context.obtainStyledAttributes(attrs, R.styleable.StoriesProgressView ) storiesCount = typedArray.getInt(R.styleable.StoriesProgressView_progressCount, 0) typedArray.recycle() bindViews() } private fun bindViews() { progressBars.clear() removeAllViews() for (i in 0 until storiesCount) { val p = createProgressBar() p.tag = "p($position) c($i)" // debug progressBars.add(p) addView(p) if (i + 1 < storiesCount) { addView(createSpace()) } } } private fun createProgressBar(): PausableProgressBar { return PausableProgressBar(context).apply { layoutParams = PROGRESS_BAR_LAYOUT_PARAM } } private fun createSpace(): View { return View(context).apply { layoutParams = SPACE_LAYOUT_PARAM } } private fun callback(index: Int): PausableProgressBar.Callback { return object : PausableProgressBar.Callback { override fun onStartProgress() { current = index } override fun onFinishProgress() { if (isReverseStart) { if (storiesListener != null) storiesListener!!.onPrev() if (0 <= current - 1) { val p = progressBars[current - 1] p.setMinWithoutCallback() progressBars[--current].startProgress() } else { progressBars[current].startProgress() } isReverseStart = false return } val next = current + 1 if (next <= progressBars.size - 1) { if (storiesListener != null) storiesListener!!.onNext() progressBars[next].startProgress() ++current } else { isComplete = true if (storiesListener != null) storiesListener!!.onComplete() } isSkipStart = false } } } fun setStoriesCountDebug(storiesCount: Int, position: Int) { this.storiesCount = storiesCount this.position = position bindViews() } fun setStoriesListener(storiesListener: StoriesListener?) { this.storiesListener = storiesListener } fun skip() { if (isSkipStart || isReverseStart) return if (isComplete) return if (current < 0) return val p = progressBars[current] isSkipStart = true p.setMax() } fun reverse() { if (isSkipStart || isReverseStart) return if (isComplete) return if (current < 0) return val p = progressBars[current] isReverseStart = true p.setMin() } fun setAllStoryDuration(duration: Long) { for (i in progressBars.indices) { progressBars[i].setDuration(duration) progressBars[i].setCallback(callback(i)) } } fun startStories() { if (progressBars.size > 0) { progressBars[0].startProgress() } } fun startStories(from: Int) { for (i in progressBars.indices) { progressBars[i].clear() } for (i in 0 until from) { if (progressBars.size > i) { progressBars[i].setMaxWithoutCallback() } } if (progressBars.size > from) { progressBars[from].startProgress() } } fun destroy() { for (p in progressBars) { p.clear() } } fun abandon() { if (progressBars.size > current && current >= 0) { progressBars[current].setMinWithoutCallback() } } fun pause() { if (current < 0) return progressBars[current].pauseProgress() } fun resume() { if (current < 0 && progressBars.size > 0) { progressBars[0].startProgress() return } progressBars[current].resumeProgress() } fun getProgressWithIndex(index: Int): PausableProgressBar { return progressBars[index] } companion object { private val PROGRESS_BAR_LAYOUT_PARAM = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1F) private val SPACE_LAYOUT_PARAM = LayoutParams(5, LayoutParams.WRAP_CONTENT) } interface StoriesListener { fun onNext() fun onPrev() fun onComplete() } } ================================================ FILE: app/src/main/res/anim/slide_in_right.xml ================================================ ================================================ FILE: app/src/main/res/anim/slide_out_left.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_back.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_down.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_arrow_r8.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_clear.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_close_24px.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_menu_24px.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_search.xml ================================================ ================================================ FILE: app/src/main/res/drawable/selector_menu.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_audio_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_border_line.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_btn_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_circle.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_circle_blue.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_contact_selected.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_divider.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_edit_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_gradient.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_home_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_menu_active.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_menu_non_active.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_msg_bg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_radius.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_receive_msg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_receive_msg_corned.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_send_msg.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_send_msg_corned.xml ================================================ ================================================ FILE: app/src/main/res/drawable/shape_unread_count.xml ================================================ ================================================ FILE: app/src/main/res/drawable-v24/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/layout/act_chat.xml ================================================ ================================================ FILE: app/src/main/res/layout/act_splash.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main.xml ================================================ ================================================ FILE: app/src/main/res/layout/activity_main2.xml ================================================ ================================================ FILE: app/src/main/res/layout/alert_dialog.xml ================================================ ================================================ FILE: app/src/main/res/layout/alert_logout.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_add_group_members.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_attachment.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_contacts.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_countries.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_create_group.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_group_chat.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_group_chat_home.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_image_src_sheet.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_login.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_my_profile.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_profile.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_search.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_single_chat.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_single_chat_home.xml ================================================ ================================================ FILE: app/src/main/res/layout/f_verify.xml ================================================ ================================================ FILE: app/src/main/res/layout/load_state_footer.xml ================================================