Repository: dgewe/Chat-App-Android
Branch: master
Commit: cff2f947a449
Files: 118
Total size: 198.1 KB
Directory structure:
gitextract_1onv52u2/
├── .gitattributes
├── .gitignore
├── .idea/
│ ├── .name
│ ├── codeStyles/
│ │ ├── Project.xml
│ │ └── codeStyleConfig.xml
│ ├── jarRepositories.xml
│ ├── misc.xml
│ ├── render.experimental.xml
│ └── runConfigurations.xml
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle
│ ├── google-services.json
│ ├── proguard-rules.pro
│ └── src/
│ ├── androidTest/
│ │ └── java/
│ │ └── com/
│ │ └── fredrikbogg/
│ │ └── android_chat_app/
│ │ └── ExampleInstrumentedTest.kt
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── fredrikbogg/
│ │ │ └── android_chat_app/
│ │ │ ├── App.kt
│ │ │ ├── data/
│ │ │ │ ├── Event.kt
│ │ │ │ ├── Result.kt
│ │ │ │ ├── db/
│ │ │ │ │ ├── entity/
│ │ │ │ │ │ ├── Chat.kt
│ │ │ │ │ │ ├── Message.kt
│ │ │ │ │ │ └── User.kt
│ │ │ │ │ ├── remote/
│ │ │ │ │ │ ├── FirebaseAuthSource.kt
│ │ │ │ │ │ ├── FirebaseDatabaseSource.kt
│ │ │ │ │ │ └── FirebaseStorageSource.kt
│ │ │ │ │ └── repository/
│ │ │ │ │ ├── AuthRepository.kt
│ │ │ │ │ ├── DatabaseRepository.kt
│ │ │ │ │ └── StorageRepository.kt
│ │ │ │ └── model/
│ │ │ │ ├── ChatWithUserInfo.kt
│ │ │ │ ├── CreateUser.kt
│ │ │ │ └── Login.kt
│ │ │ ├── ui/
│ │ │ │ ├── DefaultBindings.kt
│ │ │ │ ├── DefaultViewModel.kt
│ │ │ │ ├── chat/
│ │ │ │ │ ├── ChatFragment.kt
│ │ │ │ │ ├── ChatViewModel.kt
│ │ │ │ │ ├── MessagesBindings.kt
│ │ │ │ │ └── MessagesListAdapter.kt
│ │ │ │ ├── chats/
│ │ │ │ │ ├── ChatsBindings.kt
│ │ │ │ │ ├── ChatsFragment.kt
│ │ │ │ │ ├── ChatsListAdapter.kt
│ │ │ │ │ └── ChatsViewModel.kt
│ │ │ │ ├── main/
│ │ │ │ │ ├── MainActivity.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ ├── notifications/
│ │ │ │ │ ├── NotificationsBindings.kt
│ │ │ │ │ ├── NotificationsFragment.kt
│ │ │ │ │ ├── NotificationsListAdapter.kt
│ │ │ │ │ └── NotificationsViewModel.kt
│ │ │ │ ├── profile/
│ │ │ │ │ ├── ProfileFragment.kt
│ │ │ │ │ └── ProfileViewModel.kt
│ │ │ │ ├── settings/
│ │ │ │ │ ├── SettingsFragment.kt
│ │ │ │ │ └── SettingsViewModel.kt
│ │ │ │ ├── start/
│ │ │ │ │ ├── StartFragment.kt
│ │ │ │ │ ├── StartViewModel.kt
│ │ │ │ │ ├── createAccount/
│ │ │ │ │ │ ├── CreateAccountFragment.kt
│ │ │ │ │ │ └── CreateAccountViewModel.kt
│ │ │ │ │ └── login/
│ │ │ │ │ ├── LoginFragment.kt
│ │ │ │ │ └── LoginViewModel.kt
│ │ │ │ └── users/
│ │ │ │ ├── UsersBindings.kt
│ │ │ │ ├── UsersFragment.kt
│ │ │ │ ├── UsersListAdapter.kt
│ │ │ │ └── UsersViewModel.kt
│ │ │ └── util/
│ │ │ ├── FileConverterUtil.kt
│ │ │ ├── FirebaseUtil.kt
│ │ │ ├── LiveDataExt.kt
│ │ │ ├── SharedPreferencesUtil.kt
│ │ │ ├── TextUtil.kt
│ │ │ └── ViewExt.kt
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_baseline_chat_bubble_24.xml
│ │ │ ├── ic_baseline_error_24.xml
│ │ │ ├── ic_baseline_notifications_24.xml
│ │ │ ├── ic_baseline_people_24.xml
│ │ │ ├── ic_baseline_person_24.xml
│ │ │ ├── ic_baseline_settings_24.xml
│ │ │ ├── round_circle_online_green.xml
│ │ │ └── round_circle_primary.xml
│ │ ├── drawable-v24/
│ │ │ ├── rounded_rectangle_primary.xml
│ │ │ └── rounded_rectangle_secondary.xml
│ │ ├── font/
│ │ │ ├── nunito.xml
│ │ │ ├── nunito_bold.xml
│ │ │ ├── nunito_extrabold.xml
│ │ │ └── nunito_semibold.xml
│ │ ├── layout/
│ │ │ ├── activity_main.xml
│ │ │ ├── fragment_chat.xml
│ │ │ ├── fragment_chats.xml
│ │ │ ├── fragment_create_account.xml
│ │ │ ├── fragment_login.xml
│ │ │ ├── fragment_notifications.xml
│ │ │ ├── fragment_profile.xml
│ │ │ ├── fragment_settings.xml
│ │ │ ├── fragment_start.xml
│ │ │ ├── fragment_users.xml
│ │ │ ├── list_item_chat.xml
│ │ │ ├── list_item_message_received.xml
│ │ │ ├── list_item_message_sent.xml
│ │ │ ├── list_item_notification.xml
│ │ │ ├── list_item_user.xml
│ │ │ ├── toolbar_addon_chat.xml
│ │ │ └── toolbar_main.xml
│ │ ├── menu/
│ │ │ └── bottom_nav_menu.xml
│ │ ├── mipmap-anydpi-v26/
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── navigation/
│ │ │ └── mobile_navigation.xml
│ │ └── values/
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── font_certs.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── preloaded_fonts.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test/
│ └── java/
│ └── com/
│ └── fredrikbogg/
│ └── android_chat_app/
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto
================================================
FILE: .gitignore
================================================
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# 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/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
================================================
FILE: .idea/.name
================================================
Android-Chat-App
================================================
FILE: .idea/codeStyles/Project.xml
================================================
.*:id
http://schemas.android.com/apk/res/android
.*:name
http://schemas.android.com/apk/res/android
.*
http://schemas.android.com/apk/res/android
ANDROID_ATTRIBUTE_ORDER
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
================================================
FILE: .idea/jarRepositories.xml
================================================
================================================
FILE: .idea/misc.xml
================================================
================================================
FILE: .idea/render.experimental.xml
================================================
================================================
FILE: .idea/runConfigurations.xml
================================================
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Fredrik Bogg
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
================================================
# Chat App Android

## Introduction
This is a demo application built with the goal to create a fun and challenging application based on the MVVM architectural pattern.
See below for more information.
## Technologies & Architecture
#### Technologies
Android, Kotlin
#### Architecture
Model-View-ViewModel (MVVM)
#### Firebase
* Authentication
* Realtime Database
* Storage
#### Architecture Components
[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), [LiveData](https://developer.android.com/topic/libraries/architecture/livedata), [DataBinding](https://developer.android.com/topic/libraries/data-binding),
[Navigation](https://developer.android.com/guide/navigation/)
## Features
**Start:** Login/create account
**Chats:** List of chats, online status, update on change
**Notifications:** Accept/decline friend requests, notifications symbol
**Users:** List of users
**Settings:** Change image, change status, logout
**Chat:** Send and show messages sorted by timestamp, online status, custom toolbar, update on change
**Profile:** Add/remove friend, accept/decline friend request
**General:** Auto login, bottom navigation, error messages with snackbar, progress bar
## Screenshots
### Start | Login | Create Account
### Chats | Notifications | Users
### Settings | Chat | Profile
### Firebase
## Setup
#### Requirements
* Basic knowledge about Android Studio
* Basic knowledge about Firebase
#### Firebase
* Setup Authentication and use the Sign-in method 'Email/Password'
* Setup Realtime Database
* Setup Storage
* Replace the file [google-services.json](app/google-services.json)
* Note: Download the google-services.json file after the Firebase services are set up to automatically include the services in the json file.
* Note: When updating the google-services.json file then make sure to invalidate the caches as well as doing a clean + rebuild.
#### Project
1. Download and open the project in Android Studio
2. Connect your Android phone or use the emulator to start the application
================================================
FILE: app/.gitignore
================================================
/build
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "com.fredrikbogg.android_chat_app"
minSdkVersion 26
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
buildFeatures {
dataBinding = true
viewBinding = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
//Navigation, lifecycle
implementation 'androidx.navigation:navigation-fragment:2.3.0'
implementation 'androidx.navigation:navigation-ui:2.3.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
// Firebase
implementation 'com.google.firebase:firebase-database:19.3.1'
implementation 'com.google.firebase:firebase-auth:19.3.2'
implementation 'com.google.firebase:firebase-storage:19.1.1'
// Picasso
implementation 'com.squareup.picasso:picasso:2.71828'
implementation 'jp.wasabeef:picasso-transformations:2.2.1'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
================================================
FILE: app/google-services.json
================================================
-EDIT THIS-
================================================
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/fredrikbogg/android_chat_app/ExampleInstrumentedTest.kt
================================================
package com.fredrikbogg.android_chat_app
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.fredrikbogg.android_chat_app", appContext.packageName)
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/App.kt
================================================
package com.fredrikbogg.android_chat_app
import android.app.Application
import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
class App : Application() {
override fun onCreate() {
super.onCreate()
application = this
}
companion object {
lateinit var application: Application
private set
var myUserID: String = ""
get() {
field = SharedPreferencesUtil.getUserID(application.applicationContext).orEmpty()
return field
}
private set
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/Event.kt
================================================
package com.fredrikbogg.android_chat_app.data
import androidx.lifecycle.Observer
open class Event(private val content: T) {
private var isHandled = false
fun getContentIfNotHandled(): T? {
return if (isHandled) {
null
} else {
isHandled = true
content
}
}
}
class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> {
override fun onChanged(event: Event?) {
event?.getContentIfNotHandled()?.let { onEventUnhandledContent(it) }
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/Result.kt
================================================
package com.fredrikbogg.android_chat_app.data
sealed class Result {
data class Success(val data: T? = null, val msg: String? = null) : Result()
class Error(val msg: String? = null) : Result()
object Loading : Result()
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Chat.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.entity
import com.google.firebase.database.PropertyName
data class Chat(
@get:PropertyName("lastMessage") @set:PropertyName("lastMessage") var lastMessage: Message = Message(),
@get:PropertyName("info") @set:PropertyName("info") var info: ChatInfo = ChatInfo()
)
data class ChatInfo(
@get:PropertyName("id") @set:PropertyName("id") var id: String = ""
)
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Message.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.entity
import com.google.firebase.database.PropertyName
import java.util.*
data class Message(
@get:PropertyName("senderID") @set:PropertyName("senderID") var senderID: String = "",
@get:PropertyName("text") @set:PropertyName("text") var text: String = "",
@get:PropertyName("epochTimeMs") @set:PropertyName("epochTimeMs") var epochTimeMs: Long = Date().time,
@get:PropertyName("seen") @set:PropertyName("seen") var seen: Boolean = false
)
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/User.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.entity
import com.google.firebase.database.PropertyName
data class User(
@get:PropertyName("info") @set:PropertyName("info") var info: UserInfo = UserInfo(),
@get:PropertyName("friends") @set:PropertyName("friends") var friends: HashMap = HashMap(),
@get:PropertyName("notifications") @set:PropertyName("notifications") var notifications: HashMap = HashMap(),
@get:PropertyName("sentRequests") @set:PropertyName("sentRequests") var sentRequests: HashMap = HashMap()
)
data class UserFriend(
@get:PropertyName("userID") @set:PropertyName("userID") var userID: String = ""
)
data class UserInfo(
@get:PropertyName("id") @set:PropertyName("id") var id: String = "",
@get:PropertyName("displayName") @set:PropertyName("displayName") var displayName: String = "",
@get:PropertyName("status") @set:PropertyName("status") var status: String = "No status",
@get:PropertyName("profileImageUrl") @set:PropertyName("profileImageUrl") var profileImageUrl: String = "",
@get:PropertyName("online") @set:PropertyName("online") var online: Boolean = false
)
data class UserNotification(
@get:PropertyName("userID") @set:PropertyName("userID") var userID: String = ""
)
data class UserRequest(
@get:PropertyName("userID") @set:PropertyName("userID") var userID: String = ""
)
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseAuthSource.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.remote
import com.fredrikbogg.android_chat_app.data.model.CreateUser
import com.fredrikbogg.android_chat_app.data.model.Login
import com.fredrikbogg.android_chat_app.data.Result
import com.google.android.gms.tasks.Task
import com.google.firebase.auth.AuthResult
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.FirebaseUser
class FirebaseAuthStateObserver {
private var authListener: FirebaseAuth.AuthStateListener? = null
private var instance: FirebaseAuth? = null
fun start(valueEventListener: FirebaseAuth.AuthStateListener, instance: FirebaseAuth) {
this.authListener = valueEventListener
this.instance = instance
this.instance!!.addAuthStateListener(authListener!!)
}
fun clear() {
authListener?.let { instance?.removeAuthStateListener(it) }
}
}
class FirebaseAuthSource {
companion object {
val authInstance = FirebaseAuth.getInstance()
}
private fun attachAuthObserver(b: ((Result) -> Unit)): FirebaseAuth.AuthStateListener {
return FirebaseAuth.AuthStateListener {
if (it.currentUser == null) {
b.invoke(Result.Error("No user"))
} else { b.invoke(Result.Success(it.currentUser)) }
}
}
fun loginWithEmailAndPassword(login: Login): Task {
return authInstance.signInWithEmailAndPassword(login.email, login.password)
}
fun createUser(createUser: CreateUser): Task {
return authInstance.createUserWithEmailAndPassword(createUser.email, createUser.password)
}
fun logout() {
authInstance.signOut()
}
fun attachAuthStateObserver(firebaseAuthStateObserver: FirebaseAuthStateObserver, b: ((Result) -> Unit)) {
val listener = attachAuthObserver(b)
firebaseAuthStateObserver.start(listener, authInstance)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseDatabaseSource.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.remote
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.data.db.entity.*
import com.fredrikbogg.android_chat_app.util.wrapSnapshotToArrayList
import com.fredrikbogg.android_chat_app.util.wrapSnapshotToClass
import com.google.android.gms.tasks.Task
import com.google.android.gms.tasks.TaskCompletionSource
import com.google.firebase.database.*
class FirebaseReferenceConnectedObserver {
private var valueEventListener: ValueEventListener? = null
private var dbRef: DatabaseReference? = null
private var userRef: DatabaseReference? = null
fun start(userID: String) {
this.userRef = FirebaseDataSource.dbInstance.reference.child("users/$userID/info/online")
this.valueEventListener = getEventListener(userID)
this.dbRef = FirebaseDataSource.dbInstance.getReference(".info/connected").apply { addValueEventListener(valueEventListener!!) }
}
private fun getEventListener(userID: String): ValueEventListener {
return (object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
val connected = snapshot.getValue(Boolean::class.java) ?: false
if (connected) {
FirebaseDataSource.dbInstance.reference.child("users/$userID/info/online").setValue(true)
userRef?.onDisconnect()?.setValue(false)
}
}
override fun onCancelled(error: DatabaseError) {}
})
}
fun clear() {
valueEventListener?.let { dbRef?.removeEventListener(it) }
userRef?.setValue(false)
valueEventListener = null
dbRef = null
userRef = null
}
}
class FirebaseReferenceValueObserver {
private var valueEventListener: ValueEventListener? = null
private var dbRef: DatabaseReference? = null
fun start(valueEventListener: ValueEventListener, reference: DatabaseReference) {
reference.addValueEventListener(valueEventListener)
this.valueEventListener = valueEventListener
this.dbRef = reference
}
fun clear() {
valueEventListener?.let { dbRef?.removeEventListener(it) }
valueEventListener = null
dbRef = null
}
}
class FirebaseReferenceChildObserver {
private var valueEventListener: ChildEventListener? = null
private var dbRef: DatabaseReference? = null
private var isObserving: Boolean = false
fun start(valueEventListener: ChildEventListener, reference: DatabaseReference) {
isObserving = true
reference.addChildEventListener(valueEventListener)
this.valueEventListener = valueEventListener
this.dbRef = reference
}
fun clear() {
valueEventListener?.let { dbRef?.removeEventListener(it) }
isObserving = false
valueEventListener = null
dbRef = null
}
fun isObserving(): Boolean {
return isObserving
}
}
// Task based
class FirebaseDataSource {
companion object {
val dbInstance = FirebaseDatabase.getInstance()
}
//region Private
private fun refToPath(path: String): DatabaseReference {
return dbInstance.reference.child(path)
}
private fun attachValueListenerToTaskCompletion(src: TaskCompletionSource): ValueEventListener {
return (object : ValueEventListener {
override fun onCancelled(error: DatabaseError) { src.setException(Exception(error.message)) }
override fun onDataChange(snapshot: DataSnapshot) { src.setResult(snapshot) }
})
}
private fun attachValueListenerToBlock(resultClassName: Class, b: ((Result) -> Unit)): ValueEventListener {
return (object : ValueEventListener {
override fun onCancelled(error: DatabaseError) { b.invoke(Result.Error(error.message)) }
override fun onDataChange(snapshot: DataSnapshot) {
if (wrapSnapshotToClass(resultClassName, snapshot) == null) {
b.invoke(Result.Error(msg = snapshot.key))
} else {
b.invoke(Result.Success(wrapSnapshotToClass(resultClassName, snapshot)))
}
}
})
}
private fun attachValueListenerToBlockWithList(resultClassName: Class, b: ((Result>) -> Unit)): ValueEventListener {
return (object : ValueEventListener {
override fun onCancelled(error: DatabaseError) { b.invoke(Result.Error(error.message)) }
override fun onDataChange(snapshot: DataSnapshot) {
b.invoke(Result.Success(wrapSnapshotToArrayList(resultClassName, snapshot)))
}
})
}
private fun attachChildListenerToBlock(resultClassName: Class, b: ((Result) -> Unit)): ChildEventListener {
return (object : ChildEventListener {
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
b.invoke(Result.Success(wrapSnapshotToClass(resultClassName, snapshot)))
}
override fun onCancelled(error: DatabaseError) { b.invoke(Result.Error(error.message)) }
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onChildRemoved(snapshot: DataSnapshot) {}
})
}
//endregion
//region Update
fun updateUserProfileImageUrl(userID: String, url: String) {
refToPath("users/$userID/info/profileImageUrl").setValue(url)
}
fun updateUserStatus(userID: String, status: String) {
refToPath("users/$userID/info/status").setValue(status)
}
fun updateLastMessage(chatID: String, message: Message) {
refToPath("chats/$chatID/lastMessage").setValue(message)
}
fun updateNewFriend(myUser: UserFriend, otherUser: UserFriend) {
refToPath("users/${myUser.userID}/friends/${otherUser.userID}").setValue(otherUser)
refToPath("users/${otherUser.userID}/friends/${myUser.userID}").setValue(myUser)
}
fun updateNewSentRequest(userID: String, userRequest: UserRequest) {
refToPath("users/${userID}/sentRequests/${userRequest.userID}").setValue(userRequest)
}
fun updateNewNotification(otherUserID: String, userNotification: UserNotification) {
refToPath("users/${otherUserID}/notifications/${userNotification.userID}").setValue(userNotification)
}
fun updateNewUser(user: User) {
refToPath("users/${user.info.id}").setValue(user)
}
fun updateNewChat(chat: Chat) {
refToPath("chats/${chat.info.id}").setValue(chat)
}
fun pushNewMessage(messagesID: String, message: Message) {
refToPath("messages/$messagesID").push().setValue(message)
}
//endregion
//region Remove
fun removeNotification(userID: String, notificationID: String) {
refToPath("users/${userID}/notifications/$notificationID").setValue(null)
}
fun removeFriend(userID: String, friendID: String) {
refToPath("users/${userID}/friends/$friendID").setValue(null)
refToPath("users/${friendID}/friends/$userID").setValue(null)
}
fun removeSentRequest(userID: String, sentRequestID: String) {
refToPath("users/${userID}/sentRequests/$sentRequestID").setValue(null)
}
fun removeChat(chatID: String) {
refToPath("chats/$chatID").setValue(null)
}
fun removeMessages(messagesID: String) {
refToPath("messages/$messagesID").setValue(null)
}
//endregion
//region Load
fun loadUserTask(userID: String): Task {
val src = TaskCompletionSource()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadUserInfoTask(userID: String): Task {
val src = TaskCompletionSource()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID/info").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadUsersTask(): Task {
val src = TaskCompletionSource()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadFriendsTask(userID: String): Task {
val src = TaskCompletionSource()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID/friends").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadChatTask(chatID: String): Task {
val src = TaskCompletionSource()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("chats/$chatID").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadNotificationsTask(userID: String): Task {
val src = TaskCompletionSource()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID/notifications").addListenerForSingleValueEvent(listener)
return src.task
}
//endregion
//region Value Observers
fun attachUserObserver(resultClassName: Class, userID: String, refObs: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
val listener = attachValueListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("users/$userID"))
}
fun attachUserInfoObserver(resultClassName: Class, userID: String, refObs: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
val listener = attachValueListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("users/$userID/info"))
}
fun attachUserNotificationsObserver(resultClassName: Class, userID: String, firebaseReferenceValueObserver: FirebaseReferenceValueObserver,
b: ((Result>) -> Unit)
) {
val listener = attachValueListenerToBlockWithList(resultClassName, b)
firebaseReferenceValueObserver.start(listener, refToPath("users/$userID/notifications"))
}
fun attachMessagesObserver(resultClassName: Class, messagesID: String, refObs: FirebaseReferenceChildObserver, b: ((Result) -> Unit)) {
val listener = attachChildListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("messages/$messagesID"))
}
fun attachChatObserver(resultClassName: Class, chatID: String, refObs: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
val listener = attachValueListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("chats/$chatID"))
}
//endregion
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseStorageSource.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.remote
import android.net.Uri
import com.google.android.gms.tasks.Task
import com.google.firebase.storage.FirebaseStorage
// Task based
class FirebaseStorageSource {
private val storageInstance = FirebaseStorage.getInstance()
fun uploadUserImage(userID: String, bArr: ByteArray): Task {
val path = "user_photos/$userID/profile_image"
val ref = storageInstance.reference.child(path)
return ref.putBytes(bArr).continueWithTask {
ref.downloadUrl
}
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/AuthRepository.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.repository
import com.fredrikbogg.android_chat_app.data.model.CreateUser
import com.fredrikbogg.android_chat_app.data.model.Login
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthSource
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthStateObserver
import com.fredrikbogg.android_chat_app.data.Result
import com.google.firebase.auth.FirebaseUser
class AuthRepository{
private val firebaseAuthService = FirebaseAuthSource()
fun observeAuthState(stateObserver: FirebaseAuthStateObserver, b: ((Result) -> Unit)){
firebaseAuthService.attachAuthStateObserver(stateObserver,b)
}
fun loginUser(login: Login, b: ((Result) -> Unit)) {
b.invoke(Result.Loading)
firebaseAuthService.loginWithEmailAndPassword(login).addOnSuccessListener {
b.invoke(Result.Success(it.user))
}.addOnFailureListener {
b.invoke(Result.Error(msg = it.message))
}
}
fun createUser(createUser: CreateUser, b: ((Result) -> Unit)) {
b.invoke(Result.Loading)
firebaseAuthService.createUser(createUser).addOnSuccessListener {
b.invoke(Result.Success(it.user))
}.addOnFailureListener {
b.invoke(Result.Error(msg = it.message))
}
}
fun logoutUser() {
firebaseAuthService.logout()
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/DatabaseRepository.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.repository
import com.fredrikbogg.android_chat_app.data.db.entity.*
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseDataSource
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceChildObserver
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.util.wrapSnapshotToArrayList
import com.fredrikbogg.android_chat_app.util.wrapSnapshotToClass
class DatabaseRepository {
private val firebaseDatabaseService = FirebaseDataSource()
//region Update
fun updateUserStatus(userID: String, status: String) {
firebaseDatabaseService.updateUserStatus(userID, status)
}
fun updateNewMessage(messagesID: String, message: Message) {
firebaseDatabaseService.pushNewMessage(messagesID, message)
}
fun updateNewUser(user: User) {
firebaseDatabaseService.updateNewUser(user)
}
fun updateNewFriend(myUser: UserFriend, otherUser: UserFriend) {
firebaseDatabaseService.updateNewFriend(myUser, otherUser)
}
fun updateNewSentRequest(userID: String, userRequest: UserRequest) {
firebaseDatabaseService.updateNewSentRequest(userID, userRequest)
}
fun updateNewNotification(otherUserID: String, userNotification: UserNotification) {
firebaseDatabaseService.updateNewNotification(otherUserID, userNotification)
}
fun updateChatLastMessage(chatID: String, message: Message) {
firebaseDatabaseService.updateLastMessage(chatID, message)
}
fun updateNewChat(chat: Chat){
firebaseDatabaseService.updateNewChat(chat)
}
fun updateUserProfileImageUrl(userID: String, url: String){
firebaseDatabaseService.updateUserProfileImageUrl(userID, url)
}
//endregion
//region Remove
fun removeNotification(userID: String, notificationID: String) {
firebaseDatabaseService.removeNotification(userID, notificationID)
}
fun removeFriend(userID: String, friendID: String) {
firebaseDatabaseService.removeFriend(userID, friendID)
}
fun removeSentRequest(otherUserID: String, myUserID: String) {
firebaseDatabaseService.removeSentRequest(otherUserID, myUserID)
}
fun removeChat(chatID: String) {
firebaseDatabaseService.removeChat(chatID)
}
fun removeMessages(messagesID: String){
firebaseDatabaseService.removeMessages(messagesID)
}
//endregion
//region Load Single
fun loadUser(userID: String, b: ((Result) -> Unit)) {
firebaseDatabaseService.loadUserTask(userID).addOnSuccessListener {
b.invoke(Result.Success(wrapSnapshotToClass(User::class.java, it)))
}.addOnFailureListener { b.invoke(Result.Error(it.message)) }
}
fun loadUserInfo(userID: String, b: ((Result) -> Unit)) {
firebaseDatabaseService.loadUserInfoTask(userID).addOnSuccessListener {
b.invoke(Result.Success(wrapSnapshotToClass(UserInfo::class.java, it)))
}.addOnFailureListener { b.invoke(Result.Error(it.message)) }
}
fun loadChat(chatID: String, b: ((Result) -> Unit)) {
firebaseDatabaseService.loadChatTask(chatID).addOnSuccessListener {
b.invoke(Result.Success(wrapSnapshotToClass(Chat::class.java, it)))
}.addOnFailureListener { b.invoke(Result.Error(it.message)) }
}
//endregion
//region Load List
fun loadUsers(b: ((Result>) -> Unit)) {
b.invoke(Result.Loading)
firebaseDatabaseService.loadUsersTask().addOnSuccessListener {
val usersList = wrapSnapshotToArrayList(User::class.java, it)
b.invoke(Result.Success(usersList))
}.addOnFailureListener { b.invoke(Result.Error(it.message)) }
}
fun loadFriends(userID: String, b: ((Result>) -> Unit)) {
b.invoke(Result.Loading)
firebaseDatabaseService.loadFriendsTask(userID).addOnSuccessListener {
val friendsList = wrapSnapshotToArrayList(UserFriend::class.java, it)
b.invoke(Result.Success(friendsList))
}.addOnFailureListener { b.invoke(Result.Error(it.message)) }
}
fun loadNotifications(userID: String, b: ((Result>) -> Unit)) {
b.invoke(Result.Loading)
firebaseDatabaseService.loadNotificationsTask(userID).addOnSuccessListener {
val notificationsList = wrapSnapshotToArrayList(UserNotification::class.java, it)
b.invoke(Result.Success(notificationsList))
}.addOnFailureListener { b.invoke(Result.Error(it.message)) }
}
//endregion
//#region Load and Observe
fun loadAndObserveUser(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
firebaseDatabaseService.attachUserObserver(User::class.java, userID, observer, b)
}
fun loadAndObserveUserInfo(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
firebaseDatabaseService.attachUserInfoObserver(UserInfo::class.java, userID, observer, b)
}
fun loadAndObserveUserNotifications(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result>) -> Unit)){
firebaseDatabaseService.attachUserNotificationsObserver(UserNotification::class.java, userID, observer, b)
}
fun loadAndObserveMessagesAdded(messagesID: String, observer: FirebaseReferenceChildObserver, b: ((Result) -> Unit)) {
firebaseDatabaseService.attachMessagesObserver(Message::class.java, messagesID, observer, b)
}
fun loadAndObserveChat(chatID: String, observer: FirebaseReferenceValueObserver, b: ((Result) -> Unit)) {
firebaseDatabaseService.attachChatObserver(Chat::class.java, chatID, observer, b)
}
//endregion
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/StorageRepository.kt
================================================
package com.fredrikbogg.android_chat_app.data.db.repository
import android.net.Uri
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseStorageSource
import com.fredrikbogg.android_chat_app.data.Result
class StorageRepository {
private val firebaseStorageService = FirebaseStorageSource()
fun updateUserProfileImage(userID: String, byteArray: ByteArray, b: (Result) -> Unit) {
b.invoke(Result.Loading)
firebaseStorageService.uploadUserImage(userID, byteArray).addOnSuccessListener {
b.invoke((Result.Success(it)))
}.addOnFailureListener {
b.invoke(Result.Error(it.message))
}
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/model/ChatWithUserInfo.kt
================================================
package com.fredrikbogg.android_chat_app.data.model
import com.fredrikbogg.android_chat_app.data.db.entity.Chat
import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
data class ChatWithUserInfo(
var mChat: Chat,
var mUserInfo: UserInfo
)
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/model/CreateUser.kt
================================================
package com.fredrikbogg.android_chat_app.data.model
data class CreateUser(
var displayName: String = "",
var email: String = "",
var password: String = ""
)
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/data/model/Login.kt
================================================
package com.fredrikbogg.android_chat_app.data.model
data class Login(
var email: String = "",
var password: String = ""
)
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultBindings.kt
================================================
package com.fredrikbogg.android_chat_app.ui
import android.annotation.SuppressLint
import android.widget.ImageView
import android.widget.TextView
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.fredrikbogg.android_chat_app.R
import com.squareup.picasso.Picasso
import jp.wasabeef.picasso.transformations.BlurTransformation
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
@BindingAdapter("bind_image_url_blur")
fun bindBlurImageWithPicasso(imageView: ImageView, url: String?) {
if (!url.isNullOrBlank()) {
Picasso.get().load(url).error(R.drawable.ic_baseline_error_24)
.transform(BlurTransformation(imageView.context, 15, 1)).into(imageView)
}
}
@BindingAdapter("bind_image_url")
fun bindImageWithPicasso(imageView: ImageView, url: String?) {
when (url) {
null -> Unit
"" -> imageView.setBackgroundResource(R.drawable.ic_baseline_person_24)
else -> Picasso.get().load(url).error(R.drawable.ic_baseline_error_24).into(imageView)
}
}
@SuppressLint("SimpleDateFormat")
@BindingAdapter("bind_epochTimeMsToDate_with_days_ago")
fun TextView.bindEpochTimeMsToDateWithDaysAgo(epochTimeMs: Long) {
val numOfDays = TimeUnit.MILLISECONDS.toDays(Date().time - epochTimeMs)
this.text = when {
numOfDays == 1.toLong() -> "Yesterday"
numOfDays > 1.toLong() -> "$numOfDays days ago"
else -> {
val pat =
SimpleDateFormat().toLocalizedPattern().replace("\\W?[YyMd]+\\W?".toRegex(), " ")
val formatter = SimpleDateFormat(pat, Locale.getDefault())
formatter.format(Date(epochTimeMs))
}
}
}
@SuppressLint("SimpleDateFormat")
@BindingAdapter("bind_epochTimeMsToDate")
fun TextView.bindEpochTimeMsToDate(epochTimeMs: Long) {
if (epochTimeMs > 0) {
val currentTimeMs = Date().time
val numOfDays = TimeUnit.MILLISECONDS.toDays(currentTimeMs - epochTimeMs)
val replacePattern = when {
numOfDays >= 1.toLong() -> "Yy"
else -> "YyMd"
}
val pat = SimpleDateFormat().toLocalizedPattern().replace("\\W?[$replacePattern]+\\W?".toRegex(), " ")
val formatter = SimpleDateFormat(pat, Locale.getDefault())
this.text = formatter.format(Date(epochTimeMs))
}
}
@BindingAdapter("bind_disable_item_animator")
fun bindDisableRecyclerViewItemAnimator(recyclerView: RecyclerView, disable: Boolean) {
if (disable) {
recyclerView.itemAnimator = null
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.fredrikbogg.android_chat_app.data.Event
import com.fredrikbogg.android_chat_app.data.Result
abstract class DefaultViewModel : ViewModel() {
protected val mSnackBarText = MutableLiveData>()
val snackBarText: LiveData> = mSnackBarText
private val mDataLoading = MutableLiveData>()
val dataLoading: LiveData> = mDataLoading
protected fun onResult(mutableLiveData: MutableLiveData? = null, result: Result) {
when (result) {
is Result.Loading -> mDataLoading.value = Event(true)
is Result.Error -> {
mDataLoading.value = Event(false)
result.msg?.let { mSnackBarText.value = Event(it) }
}
is Result.Success -> {
mDataLoading.value = Event(false)
result.data?.let { mutableLiveData?.value = it }
result.msg?.let { mSnackBarText.value = Event(it) }
}
}
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chat
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import com.fredrikbogg.android_chat_app.databinding.FragmentChatBinding
import com.fredrikbogg.android_chat_app.databinding.ToolbarAddonChatBinding
import kotlinx.android.synthetic.main.fragment_chat.*
class ChatFragment : Fragment() {
companion object {
const val ARGS_KEY_USER_ID = "bundle_user_id"
const val ARGS_KEY_OTHER_USER_ID = "bundle_other_user_id"
const val ARGS_KEY_CHAT_ID = "bundle_other_chat_id"
}
private val viewModel: ChatViewModel by viewModels {
ChatViewModelFactory(
requireArguments().getString(ARGS_KEY_USER_ID)!!,
requireArguments().getString(ARGS_KEY_OTHER_USER_ID)!!,
requireArguments().getString(ARGS_KEY_CHAT_ID)!!
)
}
private lateinit var viewDataBinding: FragmentChatBinding
private lateinit var listAdapter: MessagesListAdapter
private lateinit var listAdapterObserver: RecyclerView.AdapterDataObserver
private lateinit var toolbarAddonChatBinding: ToolbarAddonChatBinding
override fun onDestroy() {
super.onDestroy()
removeCustomToolbar()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding =
FragmentChatBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setHasOptionsMenu(true)
toolbarAddonChatBinding =
ToolbarAddonChatBinding.inflate(inflater, container, false)
.apply { viewmodel = viewModel }
toolbarAddonChatBinding.lifecycleOwner = this.viewLifecycleOwner
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupCustomToolbar()
setupListAdapter()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
findNavController().popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun removeCustomToolbar() {
val supportActionBar = (activity as AppCompatActivity?)!!.supportActionBar
supportActionBar!!.setDisplayShowCustomEnabled(false)
supportActionBar.customView = null
}
private fun setupCustomToolbar() {
val supportActionBar = (activity as AppCompatActivity?)!!.supportActionBar
supportActionBar!!.setDisplayShowCustomEnabled(true)
supportActionBar.customView = toolbarAddonChatBinding.root
}
private fun setupListAdapter() {
val viewModel = viewDataBinding.viewmodel
if (viewModel != null) {
listAdapterObserver = (object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
messagesRecyclerView.scrollToPosition(positionStart)
}
})
listAdapter =
MessagesListAdapter(viewModel, requireArguments().getString(ARGS_KEY_USER_ID)!!)
listAdapter.registerAdapterDataObserver(listAdapterObserver)
viewDataBinding.messagesRecyclerView.adapter = listAdapter
} else {
throw Exception("The viewmodel is not initialized")
}
}
override fun onDestroyView() {
super.onDestroyView()
listAdapter.unregisterAdapterDataObserver(listAdapterObserver)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chat
import androidx.lifecycle.*
import com.fredrikbogg.android_chat_app.data.db.entity.Chat
import com.fredrikbogg.android_chat_app.data.db.entity.Message
import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceChildObserver
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.util.addNewItem
class ChatViewModelFactory(private val myUserID: String, private val otherUserID: String, private val chatID: String) :
ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return ChatViewModel(myUserID, otherUserID, chatID) as T
}
}
class ChatViewModel(private val myUserID: String, private val otherUserID: String, private val chatID: String) : DefaultViewModel() {
private val dbRepository: DatabaseRepository = DatabaseRepository()
private val _otherUser: MutableLiveData = MutableLiveData()
private val _addedMessage = MutableLiveData()
private val fbRefMessagesChildObserver = FirebaseReferenceChildObserver()
private val fbRefUserInfoObserver = FirebaseReferenceValueObserver()
val messagesList = MediatorLiveData>()
val newMessageText = MutableLiveData()
val otherUser: LiveData = _otherUser
init {
setupChat()
checkAndUpdateLastMessageSeen()
}
override fun onCleared() {
super.onCleared()
fbRefMessagesChildObserver.clear()
fbRefUserInfoObserver.clear()
}
private fun checkAndUpdateLastMessageSeen() {
dbRepository.loadChat(chatID) { result: Result ->
if (result is Result.Success && result.data != null) {
result.data.lastMessage.let {
if (!it.seen && it.senderID != myUserID) {
it.seen = true
dbRepository.updateChatLastMessage(chatID, it)
}
}
}
}
}
private fun setupChat() {
dbRepository.loadAndObserveUserInfo(otherUserID, fbRefUserInfoObserver) { result: Result ->
onResult(_otherUser, result)
if (result is Result.Success && !fbRefMessagesChildObserver.isObserving()) {
loadAndObserveNewMessages()
}
}
}
private fun loadAndObserveNewMessages() {
messagesList.addSource(_addedMessage) { messagesList.addNewItem(it) }
dbRepository.loadAndObserveMessagesAdded(
chatID,
fbRefMessagesChildObserver
) { result: Result ->
onResult(_addedMessage, result)
}
}
fun sendMessagePressed() {
if (!newMessageText.value.isNullOrBlank()) {
val newMsg = Message(myUserID, newMessageText.value!!)
dbRepository.updateNewMessage(chatID, newMsg)
dbRepository.updateChatLastMessage(chatID, newMsg)
newMessageText.value = null
}
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesBindings.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chat
import android.view.View
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.fredrikbogg.android_chat_app.data.db.entity.Message
import kotlin.math.abs
@BindingAdapter("bind_messages_list")
fun bindMessagesList(listView: RecyclerView, items: List?) {
items?.let {
(listView.adapter as MessagesListAdapter).submitList(items)
listView.scrollToPosition(items.size - 1)
}
}
@BindingAdapter("bind_message", "bind_message_viewModel")
fun View.bindShouldMessageShowTimeText(message: Message, viewModel: ChatViewModel) {
val halfHourInMilli = 1800000
val index = viewModel.messagesList.value!!.indexOf(message)
if (index == 0) {
this.visibility = View.VISIBLE
} else {
val messageBefore = viewModel.messagesList.value!![index - 1]
if (abs(messageBefore.epochTimeMs - message.epochTimeMs) > halfHourInMilli) {
this.visibility = View.VISIBLE
} else {
this.visibility = View.GONE
}
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesListAdapter.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chat
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.fredrikbogg.android_chat_app.data.db.entity.Message
import com.fredrikbogg.android_chat_app.databinding.ListItemMessageReceivedBinding
import com.fredrikbogg.android_chat_app.databinding.ListItemMessageSentBinding
class MessagesListAdapter internal constructor(private val viewModel: ChatViewModel, private val userId: String) : ListAdapter(MessageDiffCallback()) {
private val holderTypeMessageReceived = 1
private val holderTypeMessageSent = 2
class ReceivedViewHolder(private val binding: ListItemMessageReceivedBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: ChatViewModel, item: Message) {
binding.viewmodel = viewModel
binding.message = item
binding.executePendingBindings()
}
}
class SentViewHolder(private val binding: ListItemMessageSentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: ChatViewModel, item: Message) {
binding.viewmodel = viewModel
binding.message = item
binding.executePendingBindings()
}
}
override fun getItemViewType(position: Int): Int {
return if (getItem(position).senderID != userId) {
holderTypeMessageReceived
} else {
holderTypeMessageSent
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder.itemViewType) {
holderTypeMessageSent -> (holder as SentViewHolder).bind(
viewModel,
getItem(position)
)
holderTypeMessageReceived -> (holder as ReceivedViewHolder).bind(
viewModel,
getItem(position)
)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
return when (viewType) {
holderTypeMessageSent -> {
val binding = ListItemMessageSentBinding.inflate(layoutInflater, parent, false)
SentViewHolder(binding)
}
holderTypeMessageReceived -> {
val binding = ListItemMessageReceivedBinding.inflate(layoutInflater, parent, false)
ReceivedViewHolder(binding)
}
else -> {
throw Exception("Error reading holder type")
}
}
}
}
class MessageDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: Message, newItem: Message): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Message, newItem: Message): Boolean {
return oldItem.epochTimeMs == newItem.epochTimeMs
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsBindings.kt
================================================
@file:Suppress("unused")
package com.fredrikbogg.android_chat_app.ui.chats
import android.view.View
import android.widget.TextView
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
import com.fredrikbogg.android_chat_app.data.db.entity.Message
@BindingAdapter("bind_chats_list")
fun bindChatsList(listView: RecyclerView, items: List?) {
items?.let { (listView.adapter as ChatsListAdapter).submitList(items) }
}
@BindingAdapter("bind_chat_message_text", "bind_chat_message_text_viewModel")
fun TextView.bindMessageYouToText(message: Message, viewModel: ChatsViewModel) {
this.text = if (message.senderID == viewModel.myUserID) {
"You: " + message.text
} else {
message.text
}
}
@BindingAdapter("bind_message_view", "bind_message_textView", "bind_message", "bind_myUserID")
fun View.bindMessageSeen(view: View, textView: TextView, message: Message, myUserID: String) {
if (message.senderID != myUserID && !message.seen) {
view.visibility = View.VISIBLE
textView.setTextAppearance(R.style.MessageNotSeen)
// textView.alpha = 1f
} else {
view.visibility = View.INVISIBLE
textView.setTextAppearance(R.style.MessageSeen)
// textView.alpha = 1f
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chats
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.App
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
import com.fredrikbogg.android_chat_app.databinding.FragmentChatsBinding
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.ui.chat.ChatFragment
import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
class ChatsFragment : Fragment() {
private val viewModel: ChatsViewModel by viewModels { ChatsViewModelFactory(App.myUserID) }
private lateinit var viewDataBinding: FragmentChatsBinding
private lateinit var listAdapter: ChatsListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
viewDataBinding =
FragmentChatsBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupListAdapter()
setupObservers()
}
private fun setupListAdapter() {
val viewModel = viewDataBinding.viewmodel
if (viewModel != null) {
listAdapter = ChatsListAdapter(viewModel)
viewDataBinding.chatsRecyclerView.adapter = listAdapter
} else {
throw Exception("The viewmodel is not initialized")
}
}
private fun setupObservers() {
viewModel.selectedChat.observe(viewLifecycleOwner,
EventObserver { navigateToChat(it) })
}
private fun navigateToChat(chatWithUserInfo: ChatWithUserInfo) {
val bundle = bundleOf(
ChatFragment.ARGS_KEY_USER_ID to App.myUserID,
ChatFragment.ARGS_KEY_OTHER_USER_ID to chatWithUserInfo.mUserInfo.id,
ChatFragment.ARGS_KEY_CHAT_ID to convertTwoUserIDs(App.myUserID, chatWithUserInfo.mUserInfo.id)
)
findNavController().navigate(R.id.action_navigation_chats_to_chatFragment, bundle)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsListAdapter.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chats
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.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
import com.fredrikbogg.android_chat_app.databinding.ListItemChatBinding
class ChatsListAdapter internal constructor(private val viewModel: ChatsViewModel) :
ListAdapter<(ChatWithUserInfo), ChatsListAdapter.ViewHolder>(ChatDiffCallback()) {
class ViewHolder(private val binding: ListItemChatBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: ChatsViewModel, item: ChatWithUserInfo) {
binding.viewmodel = viewModel
binding.chatwithuserinfo = item
binding.executePendingBindings()
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(viewModel, getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemChatBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
class ChatDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: ChatWithUserInfo, itemWithUserInfo: ChatWithUserInfo): Boolean {
return oldItem == itemWithUserInfo
}
override fun areContentsTheSame(oldItem: ChatWithUserInfo, itemWithUserInfo: ChatWithUserInfo): Boolean {
return oldItem.mChat.info.id == itemWithUserInfo.mChat.info.id
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.chats
import androidx.lifecycle.*
import com.fredrikbogg.android_chat_app.data.Event
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.data.db.entity.Chat
import com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo
import com.fredrikbogg.android_chat_app.data.db.entity.UserFriend
import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.util.addNewItem
import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
import com.fredrikbogg.android_chat_app.util.updateItemAt
class ChatsViewModelFactory(private val myUserID: String) :
ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return ChatsViewModel(myUserID) as T
}
}
class ChatsViewModel(val myUserID: String) : DefaultViewModel() {
private val repository: DatabaseRepository = DatabaseRepository()
private val firebaseReferenceObserverList = ArrayList()
private val _updatedChatWithUserInfo = MutableLiveData()
private val _selectedChat = MutableLiveData>()
var selectedChat: LiveData> = _selectedChat
val chatsList = MediatorLiveData>()
init {
chatsList.addSource(_updatedChatWithUserInfo) { newChat ->
val chat = chatsList.value?.find { it.mChat.info.id == newChat.mChat.info.id }
if (chat == null) {
chatsList.addNewItem(newChat)
} else {
chatsList.updateItemAt(newChat, chatsList.value!!.indexOf(chat))
}
}
setupChats()
}
override fun onCleared() {
super.onCleared()
firebaseReferenceObserverList.forEach { it.clear() }
}
private fun setupChats() {
loadFriends()
}
private fun loadFriends() {
repository.loadFriends(myUserID) { result: Result> ->
onResult(null, result)
if (result is Result.Success) result.data?.forEach { loadUserInfo(it) }
}
}
private fun loadUserInfo(userFriend: UserFriend) {
repository.loadUserInfo(userFriend.userID) { result: Result ->
onResult(null, result)
if (result is Result.Success) result.data?.let { loadAndObserveChat(it) }
}
}
private fun loadAndObserveChat(userInfo: UserInfo) {
val observer = FirebaseReferenceValueObserver()
firebaseReferenceObserverList.add(observer)
repository.loadAndObserveChat(convertTwoUserIDs(myUserID, userInfo.id), observer) { result: Result ->
if (result is Result.Success) {
_updatedChatWithUserInfo.value = result.data?.let { ChatWithUserInfo(it, userInfo) }
} else if (result is Result.Error) {
chatsList.value?.let {
val newList = mutableListOf().apply { addAll(it) }
newList.removeIf { it2 -> result.msg.toString().contains(it2.mUserInfo.id) }
chatsList.value = newList
}
}
}
}
fun selectChatWithUserInfoPressed(chat: ChatWithUserInfo) {
_selectedChat.value = Event(chat)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainActivity.kt
================================================
package com.fredrikbogg.android_chat_app.ui.main
import android.os.Bundle
import android.view.View
import android.widget.ProgressBar
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseDataSource
import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.bottomnavigation.BottomNavigationView
class MainActivity : AppCompatActivity() {
private lateinit var navView: BottomNavigationView
private lateinit var mainProgressBar: ProgressBar
private lateinit var mainToolbar: Toolbar
private lateinit var notificationsBadge: BadgeDrawable
private val viewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainToolbar = findViewById(R.id.main_toolbar)
navView = findViewById(R.id.nav_view)
mainProgressBar = findViewById(R.id.main_progressBar)
notificationsBadge =
navView.getOrCreateBadge(R.id.navigation_notifications).apply { isVisible = false }
setSupportActionBar(mainToolbar)
val navController = findNavController(R.id.nav_host_fragment)
navController.addOnDestinationChangedListener { _, destination, _ ->
when (destination.id) {
R.id.profileFragment -> navView.visibility = View.GONE
R.id.chatFragment -> navView.visibility = View.GONE
R.id.startFragment -> navView.visibility = View.GONE
R.id.loginFragment -> navView.visibility = View.GONE
R.id.createAccountFragment -> navView.visibility = View.GONE
else -> navView.visibility = View.VISIBLE
}
showGlobalProgressBar(false)
currentFocus?.rootView?.forceHideKeyboard()
}
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_chats,
R.id.navigation_notifications,
R.id.navigation_users,
R.id.navigation_settings,
R.id.startFragment
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
override fun onPause() {
super.onPause()
FirebaseDataSource.dbInstance.goOffline()
}
override fun onResume() {
FirebaseDataSource.dbInstance.goOnline()
setupViewModelObservers()
super.onResume()
}
private fun setupViewModelObservers() {
viewModel.userNotificationsList.observe(this, {
if (it.size > 0) {
notificationsBadge.number = it.size
notificationsBadge.isVisible = true
} else {
notificationsBadge.isVisible = false
}
})
}
fun showGlobalProgressBar(show: Boolean) {
if (show) mainProgressBar.visibility = View.VISIBLE
else mainProgressBar.visibility = View.GONE
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.main
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.fredrikbogg.android_chat_app.App
import com.fredrikbogg.android_chat_app.data.db.entity.UserNotification
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseAuthStateObserver
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceConnectedObserver
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.data.Result
import com.google.firebase.auth.FirebaseUser
class MainViewModel : ViewModel() {
private val dbRepository = DatabaseRepository()
private val authRepository = AuthRepository()
private val _userNotificationsList = MutableLiveData>()
private val fbRefNotificationsObserver = FirebaseReferenceValueObserver()
private val fbAuthStateObserver = FirebaseAuthStateObserver()
private val fbRefConnectedObserver = FirebaseReferenceConnectedObserver()
private var userID = App.myUserID
var userNotificationsList: LiveData> = _userNotificationsList
init {
setupAuthObserver()
}
override fun onCleared() {
super.onCleared()
fbRefNotificationsObserver.clear()
fbRefConnectedObserver.clear()
fbAuthStateObserver.clear()
}
private fun setupAuthObserver(){
authRepository.observeAuthState(fbAuthStateObserver) { result: Result ->
if (result is Result.Success) {
userID = result.data!!.uid
startObservingNotifications()
fbRefConnectedObserver.start(userID)
} else {
fbRefConnectedObserver.clear()
stopObservingNotifications()
}
}
}
private fun startObservingNotifications() {
dbRepository.loadAndObserveUserNotifications(userID, fbRefNotificationsObserver) { result: Result> ->
if (result is Result.Success) {
_userNotificationsList.value = result.data
}
}
}
private fun stopObservingNotifications() {
fbRefNotificationsObserver.clear()
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsBindings.kt
================================================
package com.fredrikbogg.android_chat_app.ui.notifications
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
@BindingAdapter("bind_notifications_list")
fun bindNotificationsList(listView: RecyclerView, items: List?) {
items?.let { (listView.adapter as NotificationsListAdapter).submitList(items) }
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.notifications
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.fredrikbogg.android_chat_app.App
import com.fredrikbogg.android_chat_app.databinding.FragmentNotificationsBinding
class NotificationsFragment : Fragment() {
private val viewModel: NotificationsViewModel by viewModels { NotificationsViewModelFactory(App.myUserID) }
private lateinit var viewDataBinding: FragmentNotificationsBinding
private lateinit var listAdapter: NotificationsListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = FragmentNotificationsBinding.inflate(inflater, container, false)
.apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupListAdapter()
}
private fun setupListAdapter() {
val viewModel = viewDataBinding.viewmodel
if (viewModel != null) {
listAdapter = NotificationsListAdapter(viewModel)
viewDataBinding.usersRecyclerView.adapter = listAdapter
} else {
throw Exception("The viewmodel is not initialized")
}
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsListAdapter.kt
================================================
package com.fredrikbogg.android_chat_app.ui.notifications
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.fredrikbogg.android_chat_app.data.db.entity.UserInfo
import com.fredrikbogg.android_chat_app.databinding.ListItemNotificationBinding
class NotificationsListAdapter internal constructor(private val viewModel: NotificationsViewModel) :
ListAdapter(UserInfoDiffCallback()) {
class ViewHolder(private val binding: ListItemNotificationBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: NotificationsViewModel, item: UserInfo) {
binding.viewmodel = viewModel
binding.userinfo = item
binding.executePendingBindings()
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(viewModel, getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemNotificationBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
class UserInfoDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: UserInfo, newItem: UserInfo): Boolean {
return oldItem.id == newItem.id
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.notifications
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.fredrikbogg.android_chat_app.data.db.entity.*
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.util.addNewItem
import com.fredrikbogg.android_chat_app.util.removeItem
import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
class NotificationsViewModelFactory(private val myUserID: String) :
ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return NotificationsViewModel(myUserID) as T
}
}
class NotificationsViewModel(private val myUserID: String) : DefaultViewModel() {
private val dbRepository: DatabaseRepository = DatabaseRepository()
private val updatedUserInfo = MutableLiveData()
private val userNotificationsList = MutableLiveData>()
val usersInfoList = MediatorLiveData>()
init {
usersInfoList.addSource(updatedUserInfo) { usersInfoList.addNewItem(it) }
loadNotifications()
}
private fun loadNotifications() {
dbRepository.loadNotifications(myUserID) { result: Result> ->
onResult(userNotificationsList, result)
if (result is Result.Success) result.data?.forEach { loadUserInfo(it) }
}
}
private fun loadUserInfo(userNotification: UserNotification) {
dbRepository.loadUserInfo(userNotification.userID) { result: Result ->
onResult(updatedUserInfo, result)
}
}
private fun updateNotification(otherUserInfo: UserInfo, removeOnly: Boolean) {
val userNotification = userNotificationsList.value?.find {
it.userID == otherUserInfo.id
}
if (userNotification != null) {
if (!removeOnly) {
dbRepository.updateNewFriend(UserFriend(myUserID), UserFriend(otherUserInfo.id))
val newChat = Chat().apply {
info.id = convertTwoUserIDs(myUserID, otherUserInfo.id)
lastMessage = Message(seen = true, text = "Say hello!")
}
dbRepository.updateNewChat(newChat)
}
dbRepository.removeNotification(myUserID, otherUserInfo.id)
dbRepository.removeSentRequest(otherUserInfo.id, myUserID)
usersInfoList.removeItem(otherUserInfo)
userNotificationsList.removeItem(userNotification)
}
}
fun acceptNotificationPressed(userInfo: UserInfo) {
updateNotification(userInfo, false)
}
fun declineNotificationPressed(userInfo: UserInfo) {
updateNotification(userInfo, true)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.profile
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.App
import com.fredrikbogg.android_chat_app.databinding.FragmentProfileBinding
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.util.showSnackBar
import com.fredrikbogg.android_chat_app.ui.main.MainActivity
import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
class ProfileFragment : Fragment() {
companion object {
const val ARGS_KEY_USER_ID = "bundle_user_id"
}
private val viewModel: ProfileViewModel by viewModels {
ProfileViewModelFactory(App.myUserID, requireArguments().getString(ARGS_KEY_USER_ID)!!)
}
private lateinit var viewDataBinding: FragmentProfileBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
viewDataBinding = FragmentProfileBinding.inflate(inflater, container, false)
.apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupObservers()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
findNavController().popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun setupObservers() {
viewModel.dataLoading.observe(viewLifecycleOwner,
EventObserver { (activity as MainActivity).showGlobalProgressBar(it) })
viewModel.snackBarText.observe(viewLifecycleOwner,
EventObserver { text ->
view?.showSnackBar(text)
view?.forceHideKeyboard()
})
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.profile
import androidx.lifecycle.*
import com.fredrikbogg.android_chat_app.data.db.entity.*
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.util.convertTwoUserIDs
class ProfileViewModelFactory(private val myUserID: String, private val otherUserID: String) :
ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return ProfileViewModel(myUserID, otherUserID) as T
}
}
enum class LayoutState {
IS_FRIEND, NOT_FRIEND, ACCEPT_DECLINE, REQUEST_SENT
}
class ProfileViewModel(private val myUserID: String, private val userID: String) :
DefaultViewModel() {
private val repository: DatabaseRepository = DatabaseRepository()
private val firebaseReferenceObserver = FirebaseReferenceValueObserver()
private val _myUser: MutableLiveData = MutableLiveData()
private val _otherUser: MutableLiveData = MutableLiveData()
val otherUser: LiveData = _otherUser
val layoutState = MediatorLiveData()
init {
layoutState.addSource(_myUser) { updateLayoutState(it, _otherUser.value) }
setupProfile()
}
override fun onCleared() {
super.onCleared()
firebaseReferenceObserver.clear()
}
private fun updateLayoutState(myUser: User?, otherUser: User?) {
if (myUser != null && otherUser != null) {
layoutState.value = when {
myUser.friends[otherUser.info.id] != null -> LayoutState.IS_FRIEND
myUser.notifications[otherUser.info.id] != null -> LayoutState.ACCEPT_DECLINE
myUser.sentRequests[otherUser.info.id] != null -> LayoutState.REQUEST_SENT
else -> LayoutState.NOT_FRIEND
}
}
}
private fun setupProfile() {
repository.loadUser(userID) { result: Result ->
onResult(_otherUser, result)
if (result is Result.Success) {
repository.loadAndObserveUser(myUserID, firebaseReferenceObserver) { result2: Result ->
onResult(_myUser, result2)
}
}
}
}
fun addFriendPressed() {
repository.updateNewSentRequest(myUserID, UserRequest(_otherUser.value!!.info.id))
repository.updateNewNotification(_otherUser.value!!.info.id, UserNotification(myUserID))
}
fun removeFriendPressed() {
repository.removeFriend(myUserID, _otherUser.value!!.info.id)
repository.removeChat(convertTwoUserIDs(myUserID, _otherUser.value!!.info.id))
repository.removeMessages(convertTwoUserIDs(myUserID, _otherUser.value!!.info.id))
}
fun acceptFriendRequestPressed() {
repository.updateNewFriend(UserFriend(myUserID), UserFriend(_otherUser.value!!.info.id))
val newChat = Chat().apply {
info.id = convertTwoUserIDs(myUserID, _otherUser.value!!.info.id)
lastMessage = Message(seen = true, text = "Say hello!")
}
repository.updateNewChat(newChat)
repository.removeNotification(myUserID, _otherUser.value!!.info.id)
repository.removeSentRequest(_otherUser.value!!.info.id, myUserID)
}
fun declineFriendRequestPressed() {
repository.removeSentRequest(myUserID, _otherUser.value!!.info.id)
repository.removeNotification(myUserID, _otherUser.value!!.info.id)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.settings
import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.App
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.databinding.FragmentSettingsBinding
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
import com.fredrikbogg.android_chat_app.util.convertFileToByteArray
class SettingsFragment : Fragment() {
private val viewModel: SettingsViewModel by viewModels { SettingsViewModelFactory(App.myUserID) }
private lateinit var viewDataBinding: FragmentSettingsBinding
private val selectImageIntentRequestCode = 1
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding = FragmentSettingsBinding.inflate(inflater, container, false)
.apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupObservers()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
findNavController().popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && requestCode == selectImageIntentRequestCode) {
data?.data?.let { uri ->
convertFileToByteArray(requireContext(), uri).let {
viewModel.changeUserImage(it)
}
}
}
}
private fun setupObservers() {
viewModel.editStatusEvent.observe(viewLifecycleOwner,
EventObserver { showEditStatusDialog() })
viewModel.editImageEvent.observe(viewLifecycleOwner,
EventObserver { startSelectImageIntent() })
viewModel.logoutEvent.observe(viewLifecycleOwner,
EventObserver {
SharedPreferencesUtil.removeUserID(requireContext())
navigateToStart()
})
}
private fun showEditStatusDialog() {
val input = EditText(requireActivity() as Context)
AlertDialog.Builder(requireActivity()).apply {
setTitle("Status:")
setView(input)
setPositiveButton("Ok") { _, _ ->
val textInput = input.text.toString()
if (!textInput.isBlank() && textInput.length <= 40) {
viewModel.changeUserStatus(textInput)
}
}
setNegativeButton("Cancel") { _, _ -> }
show()
}
}
private fun startSelectImageIntent() {
val selectImageIntent = Intent(Intent.ACTION_GET_CONTENT)
selectImageIntent.type = "image/*"
startActivityForResult(selectImageIntent, selectImageIntentRequestCode)
}
private fun navigateToStart() {
findNavController().navigate(R.id.action_navigation_settings_to_startFragment)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.settings
import android.net.Uri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.fredrikbogg.android_chat_app.data.db.entity.UserInfo
import com.fredrikbogg.android_chat_app.data.db.remote.FirebaseReferenceValueObserver
import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.data.db.repository.StorageRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.data.Event
import com.fredrikbogg.android_chat_app.data.Result
class SettingsViewModelFactory(private val userID: String) : ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return SettingsViewModel(userID) as T
}
}
class SettingsViewModel(private val userID: String) : DefaultViewModel() {
private val dbRepository: DatabaseRepository = DatabaseRepository()
private val storageRepository = StorageRepository()
private val authRepository = AuthRepository()
private val _userInfo: MutableLiveData = MutableLiveData()
val userInfo: LiveData = _userInfo
private val _editStatusEvent = MutableLiveData>()
val editStatusEvent: LiveData> = _editStatusEvent
private val _editImageEvent = MutableLiveData>()
val editImageEvent: LiveData> = _editImageEvent
private val _logoutEvent = MutableLiveData>()
val logoutEvent: LiveData> = _logoutEvent
private val firebaseReferenceObserver = FirebaseReferenceValueObserver()
init {
loadAndObserveUserInfo()
}
override fun onCleared() {
super.onCleared()
firebaseReferenceObserver.clear()
}
private fun loadAndObserveUserInfo() {
dbRepository.loadAndObserveUserInfo(userID, firebaseReferenceObserver)
{ result: Result -> onResult(_userInfo, result) }
}
fun changeUserStatus(status: String) {
dbRepository.updateUserStatus(userID, status)
}
fun changeUserImage(byteArray: ByteArray) {
storageRepository.updateUserProfileImage(userID, byteArray) { result: Result ->
onResult(null, result)
if (result is Result.Success) {
dbRepository.updateUserProfileImageUrl(userID, result.data.toString())
}
}
}
fun changeUserImagePressed() {
_editImageEvent.value = Event(Unit)
}
fun changeUserStatusPressed() {
_editStatusEvent.value = Event(Unit)
}
fun logoutUserPressed() {
authRepository.logoutUser()
_logoutEvent.value = Event(Unit)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.start
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 androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.databinding.FragmentStartBinding
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
class StartFragment : Fragment() {
private val viewModel by viewModels()
private lateinit var viewDataBinding: FragmentStartBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
viewDataBinding =
FragmentStartBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setHasOptionsMenu(false)
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupObservers()
if (userIsAlreadyLoggedIn()) {
navigateDirectlyToChats()
}
}
private fun userIsAlreadyLoggedIn(): Boolean {
return SharedPreferencesUtil.getUserID(requireContext()) != null
}
private fun setupObservers() {
viewModel.loginEvent.observe(viewLifecycleOwner, EventObserver { navigateToLogin() })
viewModel.createAccountEvent.observe(
viewLifecycleOwner, EventObserver { navigateToCreateAccount() })
}
private fun navigateDirectlyToChats() {
findNavController().navigate(R.id.action_startFragment_to_navigation_chats)
}
private fun navigateToLogin() {
findNavController().navigate(R.id.action_startFragment_to_loginFragment)
}
private fun navigateToCreateAccount() {
findNavController().navigate(R.id.action_startFragment_to_createAccountFragment)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.start
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.fredrikbogg.android_chat_app.data.Event
class StartViewModel : ViewModel() {
private val _loginEvent = MutableLiveData>()
private val _createAccountEvent = MutableLiveData>()
val loginEvent: LiveData> = _loginEvent
val createAccountEvent: LiveData> = _createAccountEvent
fun goToLoginPressed() {
_loginEvent.value = Event(Unit)
}
fun goToCreateAccountPressed() {
_createAccountEvent.value = Event(Unit)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.start.createAccount
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.databinding.FragmentCreateAccountBinding
import com.fredrikbogg.android_chat_app.ui.main.MainActivity
import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
import com.fredrikbogg.android_chat_app.util.showSnackBar
class CreateAccountFragment : Fragment() {
private val viewModel by viewModels()
private lateinit var viewDataBinding: FragmentCreateAccountBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
viewDataBinding = FragmentCreateAccountBinding.inflate(inflater, container, false)
.apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupObservers()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
findNavController().popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun setupObservers() {
viewModel.dataLoading.observe(viewLifecycleOwner,
EventObserver { (activity as MainActivity).showGlobalProgressBar(it) })
viewModel.snackBarText.observe(viewLifecycleOwner,
EventObserver { text ->
view?.showSnackBar(text)
view?.forceHideKeyboard()
})
viewModel.isCreatedEvent.observe(viewLifecycleOwner, EventObserver {
SharedPreferencesUtil.saveUserID(requireContext(), it.uid)
navigateToChats()
})
}
private fun navigateToChats() {
findNavController().navigate(R.id.action_createAccountFragment_to_navigation_chats)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.start.createAccount
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.fredrikbogg.android_chat_app.data.Event
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.data.db.entity.User
import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.data.model.CreateUser
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.util.isEmailValid
import com.fredrikbogg.android_chat_app.util.isTextValid
import com.google.firebase.auth.FirebaseUser
class CreateAccountViewModel : DefaultViewModel() {
private val dbRepository = DatabaseRepository()
private val authRepository = AuthRepository()
private val mIsCreatedEvent = MutableLiveData>()
val isCreatedEvent: LiveData> = mIsCreatedEvent
val displayNameText = MutableLiveData() // Two way
val emailText = MutableLiveData() // Two way
val passwordText = MutableLiveData() // Two way
val isCreatingAccount = MutableLiveData()
private fun createAccount() {
isCreatingAccount.value = true
val createUser =
CreateUser(displayNameText.value!!, emailText.value!!, passwordText.value!!)
authRepository.createUser(createUser) { result: Result ->
onResult(null, result)
if (result is Result.Success) {
mIsCreatedEvent.value = Event(result.data!!)
dbRepository.updateNewUser(User().apply {
info.id = result.data.uid
info.displayName = createUser.displayName
})
}
if (result is Result.Success || result is Result.Error) isCreatingAccount.value = false
}
}
fun createAccountPressed() {
if (!isTextValid(2, displayNameText.value)) {
mSnackBarText.value = Event("Display name is too short")
return
}
if (!isEmailValid(emailText.value.toString())) {
mSnackBarText.value = Event("Invalid email format")
return
}
if (!isTextValid(6, passwordText.value)) {
mSnackBarText.value = Event("Password is too short")
return
}
createAccount()
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.start.login
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.databinding.FragmentLoginBinding
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.util.showSnackBar
import com.fredrikbogg.android_chat_app.ui.main.MainActivity
import com.fredrikbogg.android_chat_app.util.SharedPreferencesUtil
import com.fredrikbogg.android_chat_app.util.forceHideKeyboard
class LoginFragment : Fragment() {
private val viewModel by viewModels()
private lateinit var viewDataBinding: FragmentLoginBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
viewDataBinding = FragmentLoginBinding.inflate(inflater, container, false)
.apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
setHasOptionsMenu(true)
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupObservers()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
findNavController().popBackStack()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun setupObservers() {
viewModel.dataLoading.observe(viewLifecycleOwner,
EventObserver { (activity as MainActivity).showGlobalProgressBar(it) })
viewModel.snackBarText.observe(viewLifecycleOwner,
EventObserver { text ->
view?.showSnackBar(text)
view?.forceHideKeyboard()
})
viewModel.isLoggedInEvent.observe(viewLifecycleOwner, EventObserver {
SharedPreferencesUtil.saveUserID(requireContext(), it.uid)
navigateToChats()
})
}
private fun navigateToChats() {
findNavController().navigate(R.id.action_loginFragment_to_navigation_chats)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.start.login
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.fredrikbogg.android_chat_app.data.model.Login
import com.fredrikbogg.android_chat_app.data.db.repository.AuthRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.data.Event
import com.fredrikbogg.android_chat_app.data.Result
import com.fredrikbogg.android_chat_app.util.isEmailValid
import com.fredrikbogg.android_chat_app.util.isTextValid
import com.google.firebase.auth.FirebaseUser
class LoginViewModel : DefaultViewModel() {
private val authRepository = AuthRepository()
private val _isLoggedInEvent = MutableLiveData>()
val isLoggedInEvent: LiveData> = _isLoggedInEvent
val emailText = MutableLiveData() // Two way
val passwordText = MutableLiveData() // Two way
val isLoggingIn = MutableLiveData() // Two way
private fun login() {
isLoggingIn.value = true
val login = Login(emailText.value!!, passwordText.value!!)
authRepository.loginUser(login) { result: Result ->
onResult(null, result)
if (result is Result.Success) _isLoggedInEvent.value = Event(result.data!!)
if (result is Result.Success || result is Result.Error) isLoggingIn.value = false
}
}
fun loginPressed() {
if (!isEmailValid(emailText.value.toString())) {
mSnackBarText.value = Event("Invalid email format")
return
}
if (!isTextValid(6, passwordText.value)) {
mSnackBarText.value = Event("Password is too short")
return
}
login()
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersBindings.kt
================================================
package com.fredrikbogg.android_chat_app.ui.users
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.fredrikbogg.android_chat_app.data.db.entity.User
@BindingAdapter("bind_users_list")
fun bindUsersList(listView: RecyclerView, items: List?) {
items?.let { (listView.adapter as UsersListAdapter).submitList(items) }
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersFragment.kt
================================================
package com.fredrikbogg.android_chat_app.ui.users
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.fredrikbogg.android_chat_app.App
import com.fredrikbogg.android_chat_app.R
import com.fredrikbogg.android_chat_app.databinding.FragmentUsersBinding
import com.fredrikbogg.android_chat_app.data.EventObserver
import com.fredrikbogg.android_chat_app.ui.profile.ProfileFragment
class UsersFragment : Fragment() {
private val viewModel: UsersViewModel by viewModels { UsersViewModelFactory(App.myUserID) }
private lateinit var viewDataBinding: FragmentUsersBinding
private lateinit var listAdapter: UsersListAdapter
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewDataBinding =
FragmentUsersBinding.inflate(inflater, container, false).apply { viewmodel = viewModel }
viewDataBinding.lifecycleOwner = this.viewLifecycleOwner
return viewDataBinding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupListAdapter()
setupObservers()
}
private fun setupListAdapter() {
val viewModel = viewDataBinding.viewmodel
if (viewModel != null) {
listAdapter = UsersListAdapter(viewModel)
viewDataBinding.usersRecyclerView.adapter = listAdapter
} else {
throw Exception("The viewmodel is not initialized")
}
}
private fun setupObservers() {
viewModel.selectedUser.observe(viewLifecycleOwner, EventObserver { navigateToProfile(it.info.id) })
}
private fun navigateToProfile(userID: String) {
val bundle = bundleOf(ProfileFragment.ARGS_KEY_USER_ID to userID)
findNavController().navigate(R.id.action_navigation_users_to_profileFragment, bundle)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersListAdapter.kt
================================================
package com.fredrikbogg.android_chat_app.ui.users
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.fredrikbogg.android_chat_app.data.db.entity.User
import com.fredrikbogg.android_chat_app.databinding.ListItemUserBinding
class UsersListAdapter internal constructor(private val viewModel: UsersViewModel) :
ListAdapter(UserDiffCallback()) {
class ViewHolder(private val binding: ListItemUserBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(viewModel: UsersViewModel, item: User) {
binding.viewmodel = viewModel
binding.user = item
binding.executePendingBindings()
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(viewModel, getItem(position))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = ListItemUserBinding.inflate(layoutInflater, parent, false)
return ViewHolder(binding)
}
}
class UserDiffCallback : DiffUtil.ItemCallback() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
return oldItem.info.id == newItem.info.id
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersViewModel.kt
================================================
package com.fredrikbogg.android_chat_app.ui.users
import androidx.lifecycle.*
import com.fredrikbogg.android_chat_app.data.db.entity.User
import com.fredrikbogg.android_chat_app.data.db.repository.DatabaseRepository
import com.fredrikbogg.android_chat_app.ui.DefaultViewModel
import com.fredrikbogg.android_chat_app.data.Event
import com.fredrikbogg.android_chat_app.data.Result
class UsersViewModelFactory(private val myUserID: String) :
ViewModelProvider.Factory {
override fun create(modelClass: Class): T {
return UsersViewModel(myUserID) as T
}
}
class UsersViewModel(private val myUserID: String) : DefaultViewModel() {
private val repository: DatabaseRepository = DatabaseRepository()
private val _selectedUser = MutableLiveData>()
var selectedUser: LiveData> = _selectedUser
private val updatedUsersList = MutableLiveData>()
val usersList = MediatorLiveData>()
init {
usersList.addSource(updatedUsersList) { mutableList ->
usersList.value = updatedUsersList.value?.filter { it.info.id != myUserID }
}
loadUsers()
}
private fun loadUsers() {
repository.loadUsers { result: Result> ->
onResult(updatedUsersList, result)
}
}
fun selectUser(user: User) {
_selectedUser.value = Event(user)
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/util/FileConverterUtil.kt
================================================
package com.fredrikbogg.android_chat_app.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import java.io.ByteArrayOutputStream
import java.io.InputStream
fun convertFileToByteArray(context: Context, uri: Uri): ByteArray {
val inputStream: InputStream? = context.contentResolver.openInputStream(uri)
val bitmap = BitmapFactory.decodeStream(inputStream)
val byteArrayOutputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream)
return byteArrayOutputStream.toByteArray()
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/util/FirebaseUtil.kt
================================================
package com.fredrikbogg.android_chat_app.util
import com.google.firebase.database.DataSnapshot
fun wrapSnapshotToClass(className: Class, snap: DataSnapshot): T? {
return snap.getValue(className)
}
fun wrapSnapshotToArrayList(className: Class, snap: DataSnapshot): MutableList {
val arrayList: MutableList = arrayListOf()
for (child in snap.children) {
child.getValue(className)?.let { arrayList.add(it) }
}
return arrayList
}
// Always returns the same combined id when comparing the two users id's
fun convertTwoUserIDs(userID1: String, userID2: String): String {
return if (userID1 < userID2) {
userID2 + userID1
} else {
userID1 + userID2
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/util/LiveDataExt.kt
================================================
package com.fredrikbogg.android_chat_app.util
import androidx.lifecycle.MutableLiveData
fun MutableLiveData>.addNewItem(item: T) {
val newList = mutableListOf()
this.value?.let { newList.addAll(it) }
newList.add(item)
this.value = newList
}
fun MutableLiveData>.updateItemAt(item: T, index: Int) {
val newList = mutableListOf()
this.value?.let { newList.addAll(it) }
newList[index] = item
this.value = newList
}
fun MutableLiveData>.removeItem(item: T) {
val newList = mutableListOf()
this.value?.let { newList.addAll(it) }
newList.remove(item)
this.value = newList
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/util/SharedPreferencesUtil.kt
================================================
package com.fredrikbogg.android_chat_app.util
import android.content.Context
import android.content.SharedPreferences
object SharedPreferencesUtil {
private const val PACKAGE_NAME = "com.fredrikbogg.android_chat_app"
private const val KEY_USER_ID = "user_info"
private fun getPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(PACKAGE_NAME, Context.MODE_PRIVATE)
}
fun getUserID(context: Context): String? {
return getPrefs(context).getString(KEY_USER_ID, null)
}
fun saveUserID(context: Context, userID: String) {
getPrefs(context).edit().putString(KEY_USER_ID, userID).apply()
}
fun removeUserID(context: Context) {
getPrefs(context).edit().remove(KEY_USER_ID).apply()
}
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/util/TextUtil.kt
================================================
package com.fredrikbogg.android_chat_app.util
fun isEmailValid(email: CharSequence): Boolean {
return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
}
fun isTextValid(minLength: Int, text: String?): Boolean {
if (text.isNullOrBlank() || text.length < minLength) {
return false
}
return true
}
================================================
FILE: app/src/main/java/com/fredrikbogg/android_chat_app/util/ViewExt.kt
================================================
package com.fredrikbogg.android_chat_app.util
import android.content.Context
import android.view.View
import android.view.inputmethod.InputMethodManager
import com.fredrikbogg.android_chat_app.R
import com.google.android.material.snackbar.Snackbar
fun View.forceHideKeyboard() {
val inputManager: InputMethodManager =
this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(this.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
}
fun View.showSnackBar(text: String) {
Snackbar.make(this.rootView.findViewById(R.id.container), text, Snackbar.LENGTH_SHORT).show()
}
================================================
FILE: app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_baseline_error_24.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_baseline_notifications_24.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_baseline_people_24.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_baseline_person_24.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_baseline_settings_24.xml
================================================
================================================
FILE: app/src/main/res/drawable/round_circle_online_green.xml
================================================
================================================
FILE: app/src/main/res/drawable/round_circle_primary.xml
================================================
================================================
FILE: app/src/main/res/drawable-v24/rounded_rectangle_primary.xml
================================================
================================================
FILE: app/src/main/res/drawable-v24/rounded_rectangle_secondary.xml
================================================
================================================
FILE: app/src/main/res/font/nunito.xml
================================================
================================================
FILE: app/src/main/res/font/nunito_bold.xml
================================================
================================================
FILE: app/src/main/res/font/nunito_extrabold.xml
================================================
================================================
FILE: app/src/main/res/font/nunito_semibold.xml
================================================
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_chat.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_chats.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_create_account.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_login.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_notifications.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_profile.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_settings.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_start.xml
================================================
================================================
FILE: app/src/main/res/layout/fragment_users.xml
================================================
================================================
FILE: app/src/main/res/layout/list_item_chat.xml
================================================
================================================
FILE: app/src/main/res/layout/list_item_message_received.xml
================================================
================================================
FILE: app/src/main/res/layout/list_item_message_sent.xml
================================================
================================================
FILE: app/src/main/res/layout/list_item_notification.xml
================================================
================================================
FILE: app/src/main/res/layout/list_item_user.xml
================================================
================================================
FILE: app/src/main/res/layout/toolbar_addon_chat.xml
================================================
================================================
FILE: app/src/main/res/layout/toolbar_main.xml
================================================
================================================
FILE: app/src/main/res/menu/bottom_nav_menu.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
================================================
FILE: app/src/main/res/navigation/mobile_navigation.xml
================================================
================================================
FILE: app/src/main/res/values/colors.xml
================================================
#2D9CDB
@color/colorPrimary
#F3D231
#ff6262
================================================
FILE: app/src/main/res/values/dimens.xml
================================================
================================================
FILE: app/src/main/res/values/font_certs.xml
================================================
- @array/com_google_android_gms_fonts_certs_dev
- @array/com_google_android_gms_fonts_certs_prod
-
MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
-
MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
================================================
FILE: app/src/main/res/values/ic_launcher_background.xml
================================================
#FFFFFF
================================================
FILE: app/src/main/res/values/preloaded_fonts.xml
================================================
- @font/nunito
- @font/nunito_bold
- @font/nunito_extrabold
- @font/nunito_semibold
================================================
FILE: app/src/main/res/values/strings.xml
================================================
Chat App
Chats
Notifications
Users
Settings
Profile
Chat
User image
Accept
Decline
Enter message
SEND
Create a new account
Display name
Email
Password
Create
Login to your account
Add Friend
Remove Friend
Accept Friend Request
Request sent
Decline Friend Request
Change Image
Logout
Change Status
Create account
New friend request
login
Welcome to Quick Chat
Login or create an account to get started
Chat icon
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/test/java/com/fredrikbogg/android_chat_app/ExampleUnitTest.kt
================================================
package com.fredrikbogg.android_chat_app
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.0"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Wed Aug 05 11:42:06 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
================================================
FILE: gradlew
================================================
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: settings.gradle
================================================
include ':app'
rootProject.name = "Android-Chat-App"