Showing preview only (231K chars total). Download the full file or copy to clipboard to get everything.
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
================================================
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
================================================
FILE: .idea/jarRepositories.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>
================================================
FILE: .idea/misc.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
================================================
FILE: .idea/render.experimental.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="showDecorations" value="true" />
</component>
</project>
================================================
FILE: .idea/runConfigurations.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
================================================
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
<p align = "left" >
<img width="250" height="500" src="github_images/start.png">
<img width="250" height="500" src="github_images/login.png">
<img width="250" height="500" src="github_images/create.png">
</p>
### Chats | Notifications | Users
<p align = "left" >
<img width="250" height="500" src="github_images/chats.png">
<img width="250" height="500" src="github_images/notifications.png">
<img width="250" height="500" src="github_images/users.png">
</p>
### Settings | Chat | Profile
<p align = "left" >
<img width="250" height="500" src="github_images/settings.png">
<img width="250" height="500" src="github_images/chat.png">
<img width="250" height="500" src="github_images/profile.png">
</p>
### Firebase
<p align = "left" >
<img width="378" height="332" src="github_images/db.png">
</p>
## 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)
* <b>Note:</b> Download the google-services.json file after the Firebase services are set up to automatically include the services in the json file.
* <b>Note:</b> 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.fredrikbogg.android_chat_app">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name="com.fredrikbogg.android_chat_app.App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="com.fredrikbogg.android_chat_app.ui.main.MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
</application>
</manifest>
================================================
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<out T>(private val content: T) {
private var isHandled = false
fun getContentIfNotHandled(): T? {
return if (isHandled) {
null
} else {
isHandled = true
content
}
}
}
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit) : Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
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<out R> {
data class Success<out T>(val data: T? = null, val msg: String? = null) : Result<T>()
class Error(val msg: String? = null) : Result<Nothing>()
object Loading : Result<Nothing>()
}
================================================
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<String, UserFriend> = HashMap(),
@get:PropertyName("notifications") @set:PropertyName("notifications") var notifications: HashMap<String, UserNotification> = HashMap(),
@get:PropertyName("sentRequests") @set:PropertyName("sentRequests") var sentRequests: HashMap<String, UserRequest> = 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<FirebaseUser>) -> 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<AuthResult> {
return authInstance.signInWithEmailAndPassword(login.email, login.password)
}
fun createUser(createUser: CreateUser): Task<AuthResult> {
return authInstance.createUserWithEmailAndPassword(createUser.email, createUser.password)
}
fun logout() {
authInstance.signOut()
}
fun attachAuthStateObserver(firebaseAuthStateObserver: FirebaseAuthStateObserver, b: ((Result<FirebaseUser>) -> 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<DataSnapshot>): ValueEventListener {
return (object : ValueEventListener {
override fun onCancelled(error: DatabaseError) { src.setException(Exception(error.message)) }
override fun onDataChange(snapshot: DataSnapshot) { src.setResult(snapshot) }
})
}
private fun <T> attachValueListenerToBlock(resultClassName: Class<T>, b: ((Result<T>) -> 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 <T> attachValueListenerToBlockWithList(resultClassName: Class<T>, b: ((Result<MutableList<T>>) -> 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 <T> attachChildListenerToBlock(resultClassName: Class<T>, b: ((Result<T>) -> 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<DataSnapshot> {
val src = TaskCompletionSource<DataSnapshot>()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadUserInfoTask(userID: String): Task<DataSnapshot> {
val src = TaskCompletionSource<DataSnapshot>()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID/info").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadUsersTask(): Task<DataSnapshot> {
val src = TaskCompletionSource<DataSnapshot>()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadFriendsTask(userID: String): Task<DataSnapshot> {
val src = TaskCompletionSource<DataSnapshot>()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID/friends").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadChatTask(chatID: String): Task<DataSnapshot> {
val src = TaskCompletionSource<DataSnapshot>()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("chats/$chatID").addListenerForSingleValueEvent(listener)
return src.task
}
fun loadNotificationsTask(userID: String): Task<DataSnapshot> {
val src = TaskCompletionSource<DataSnapshot>()
val listener = attachValueListenerToTaskCompletion(src)
refToPath("users/$userID/notifications").addListenerForSingleValueEvent(listener)
return src.task
}
//endregion
//region Value Observers
fun <T> attachUserObserver(resultClassName: Class<T>, userID: String, refObs: FirebaseReferenceValueObserver, b: ((Result<T>) -> Unit)) {
val listener = attachValueListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("users/$userID"))
}
fun <T> attachUserInfoObserver(resultClassName: Class<T>, userID: String, refObs: FirebaseReferenceValueObserver, b: ((Result<T>) -> Unit)) {
val listener = attachValueListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("users/$userID/info"))
}
fun <T> attachUserNotificationsObserver(resultClassName: Class<T>, userID: String, firebaseReferenceValueObserver: FirebaseReferenceValueObserver,
b: ((Result<MutableList<T>>) -> Unit)
) {
val listener = attachValueListenerToBlockWithList(resultClassName, b)
firebaseReferenceValueObserver.start(listener, refToPath("users/$userID/notifications"))
}
fun <T> attachMessagesObserver(resultClassName: Class<T>, messagesID: String, refObs: FirebaseReferenceChildObserver, b: ((Result<T>) -> Unit)) {
val listener = attachChildListenerToBlock(resultClassName, b)
refObs.start(listener, refToPath("messages/$messagesID"))
}
fun <T> attachChatObserver(resultClassName: Class<T>, chatID: String, refObs: FirebaseReferenceValueObserver, b: ((Result<T>) -> 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<Uri> {
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<FirebaseUser>) -> Unit)){
firebaseAuthService.attachAuthStateObserver(stateObserver,b)
}
fun loginUser(login: Login, b: ((Result<FirebaseUser>) -> 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<FirebaseUser>) -> 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<User>) -> 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<UserInfo>) -> 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<Chat>) -> 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<MutableList<User>>) -> 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<List<UserFriend>>) -> 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<MutableList<UserNotification>>) -> 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<User>) -> Unit)) {
firebaseDatabaseService.attachUserObserver(User::class.java, userID, observer, b)
}
fun loadAndObserveUserInfo(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result<UserInfo>) -> Unit)) {
firebaseDatabaseService.attachUserInfoObserver(UserInfo::class.java, userID, observer, b)
}
fun loadAndObserveUserNotifications(userID: String, observer: FirebaseReferenceValueObserver, b: ((Result<MutableList<UserNotification>>) -> Unit)){
firebaseDatabaseService.attachUserNotificationsObserver(UserNotification::class.java, userID, observer, b)
}
fun loadAndObserveMessagesAdded(messagesID: String, observer: FirebaseReferenceChildObserver, b: ((Result<Message>) -> Unit)) {
firebaseDatabaseService.attachMessagesObserver(Message::class.java, messagesID, observer, b)
}
fun loadAndObserveChat(chatID: String, observer: FirebaseReferenceValueObserver, b: ((Result<Chat>) -> 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<Uri>) -> 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<Event<String>>()
val snackBarText: LiveData<Event<String>> = mSnackBarText
private val mDataLoading = MutableLiveData<Event<Boolean>>()
val dataLoading: LiveData<Event<Boolean>> = mDataLoading
protected fun <T> onResult(mutableLiveData: MutableLiveData<T>? = null, result: Result<T>) {
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 <T : ViewModel?> create(modelClass: Class<T>): 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<UserInfo> = MutableLiveData()
private val _addedMessage = MutableLiveData<Message>()
private val fbRefMessagesChildObserver = FirebaseReferenceChildObserver()
private val fbRefUserInfoObserver = FirebaseReferenceValueObserver()
val messagesList = MediatorLiveData<MutableList<Message>>()
val newMessageText = MutableLiveData<String>()
val otherUser: LiveData<UserInfo> = _otherUser
init {
setupChat()
checkAndUpdateLastMessageSeen()
}
override fun onCleared() {
super.onCleared()
fbRefMessagesChildObserver.clear()
fbRefUserInfoObserver.clear()
}
private fun checkAndUpdateLastMessageSeen() {
dbRepository.loadChat(chatID) { result: Result<Chat> ->
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<UserInfo> ->
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<Message> ->
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<Message>?) {
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<Message, RecyclerView.ViewHolder>(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<Message>() {
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<ChatWithUserInfo>?) {
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<ChatWithUserInfo>() {
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 <T : ViewModel?> create(modelClass: Class<T>): T {
return ChatsViewModel(myUserID) as T
}
}
class ChatsViewModel(val myUserID: String) : DefaultViewModel() {
private val repository: DatabaseRepository = DatabaseRepository()
private val firebaseReferenceObserverList = ArrayList<FirebaseReferenceValueObserver>()
private val _updatedChatWithUserInfo = MutableLiveData<ChatWithUserInfo>()
private val _selectedChat = MutableLiveData<Event<ChatWithUserInfo>>()
var selectedChat: LiveData<Event<ChatWithUserInfo>> = _selectedChat
val chatsList = MediatorLiveData<MutableList<ChatWithUserInfo>>()
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<List<UserFriend>> ->
onResult(null, result)
if (result is Result.Success) result.data?.forEach { loadUserInfo(it) }
}
}
private fun loadUserInfo(userFriend: UserFriend) {
repository.loadUserInfo(userFriend.userID) { result: Result<UserInfo> ->
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<Chat> ->
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<ChatWithUserInfo>().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<MutableList<UserNotification>>()
private val fbRefNotificationsObserver = FirebaseReferenceValueObserver()
private val fbAuthStateObserver = FirebaseAuthStateObserver()
private val fbRefConnectedObserver = FirebaseReferenceConnectedObserver()
private var userID = App.myUserID
var userNotificationsList: LiveData<MutableList<UserNotification>> = _userNotificationsList
init {
setupAuthObserver()
}
override fun onCleared() {
super.onCleared()
fbRefNotificationsObserver.clear()
fbRefConnectedObserver.clear()
fbAuthStateObserver.clear()
}
private fun setupAuthObserver(){
authRepository.observeAuthState(fbAuthStateObserver) { result: Result<FirebaseUser> ->
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<MutableList<UserNotification>> ->
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<UserInfo>?) {
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<UserInfo, NotificationsListAdapter.ViewHolder>(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<UserInfo>() {
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 <T : ViewModel?> create(modelClass: Class<T>): T {
return NotificationsViewModel(myUserID) as T
}
}
class NotificationsViewModel(private val myUserID: String) : DefaultViewModel() {
private val dbRepository: DatabaseRepository = DatabaseRepository()
private val updatedUserInfo = MutableLiveData<UserInfo>()
private val userNotificationsList = MutableLiveData<MutableList<UserNotification>>()
val usersInfoList = MediatorLiveData<MutableList<UserInfo>>()
init {
usersInfoList.addSource(updatedUserInfo) { usersInfoList.addNewItem(it) }
loadNotifications()
}
private fun loadNotifications() {
dbRepository.loadNotifications(myUserID) { result: Result<MutableList<UserNotification>> ->
onResult(userNotificationsList, result)
if (result is Result.Success) result.data?.forEach { loadUserInfo(it) }
}
}
private fun loadUserInfo(userNotification: UserNotification) {
dbRepository.loadUserInfo(userNotification.userID) { result: Result<UserInfo> ->
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 <T : ViewModel?> create(modelClass: Class<T>): 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<User> = MutableLiveData()
private val _otherUser: MutableLiveData<User> = MutableLiveData()
val otherUser: LiveData<User> = _otherUser
val layoutState = MediatorLiveData<LayoutState>()
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<User> ->
onResult(_otherUser, result)
if (result is Result.Success) {
repository.loadAndObserveUser(myUserID, firebaseReferenceObserver) { result2: Result<User> ->
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 <T : ViewModel?> create(modelClass: Class<T>): 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<UserInfo> = MutableLiveData()
val userInfo: LiveData<UserInfo> = _userInfo
private val _editStatusEvent = MutableLiveData<Event<Unit>>()
val editStatusEvent: LiveData<Event<Unit>> = _editStatusEvent
private val _editImageEvent = MutableLiveData<Event<Unit>>()
val editImageEvent: LiveData<Event<Unit>> = _editImageEvent
private val _logoutEvent = MutableLiveData<Event<Unit>>()
val logoutEvent: LiveData<Event<Unit>> = _logoutEvent
private val firebaseReferenceObserver = FirebaseReferenceValueObserver()
init {
loadAndObserveUserInfo()
}
override fun onCleared() {
super.onCleared()
firebaseReferenceObserver.clear()
}
private fun loadAndObserveUserInfo() {
dbRepository.loadAndObserveUserInfo(userID, firebaseReferenceObserver)
{ result: Result<UserInfo> -> onResult(_userInfo, result) }
}
fun changeUserStatus(status: String) {
dbRepository.updateUserStatus(userID, status)
}
fun changeUserImage(byteArray: ByteArray) {
storageRepository.updateUserProfileImage(userID, byteArray) { result: Result<Uri> ->
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<StartViewModel>()
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<Event<Unit>>()
private val _createAccountEvent = MutableLiveData<Event<Unit>>()
val loginEvent: LiveData<Event<Unit>> = _loginEvent
val createAccountEvent: LiveData<Event<Unit>> = _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<CreateAccountViewModel>()
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<Event<FirebaseUser>>()
val isCreatedEvent: LiveData<Event<FirebaseUser>> = mIsCreatedEvent
val displayNameText = MutableLiveData<String>() // Two way
val emailText = MutableLiveData<String>() // Two way
val passwordText = MutableLiveData<String>() // Two way
val isCreatingAccount = MutableLiveData<Boolean>()
private fun createAccount() {
isCreatingAccount.value = true
val createUser =
CreateUser(displayNameText.value!!, emailText.value!!, passwordText.value!!)
authRepository.createUser(createUser) { result: Result<FirebaseUser> ->
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<LoginViewModel>()
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<Event<FirebaseUser>>()
val isLoggedInEvent: LiveData<Event<FirebaseUser>> = _isLoggedInEvent
val emailText = MutableLiveData<String>() // Two way
val passwordText = MutableLiveData<String>() // Two way
val isLoggingIn = MutableLiveData<Boolean>() // Two way
private fun login() {
isLoggingIn.value = true
val login = Login(emailText.value!!, passwordText.value!!)
authRepository.loginUser(login) { result: Result<FirebaseUser> ->
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<User>?) {
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<User, UsersListAdapter.ViewHolder>(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<User>() {
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 <T : ViewModel?> create(modelClass: Class<T>): T {
return UsersViewModel(myUserID) as T
}
}
class UsersViewModel(private val myUserID: String) : DefaultViewModel() {
private val repository: DatabaseRepository = DatabaseRepository()
private val _selectedUser = MutableLiveData<Event<User>>()
var selectedUser: LiveData<Event<User>> = _selectedUser
private val updatedUsersList = MutableLiveData<MutableList<User>>()
val usersList = MediatorLiveData<List<User>>()
init {
usersList.addSource(updatedUsersList) { mutableList ->
usersList.value = updatedUsersList.value?.filter { it.info.id != myUserID }
}
loadUsers()
}
private fun loadUsers() {
repository.loadUsers { result: Result<MutableList<User>> ->
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 <T> wrapSnapshotToClass(className: Class<T>, snap: DataSnapshot): T? {
return snap.getValue(className)
}
fun <T> wrapSnapshotToArrayList(className: Class<T>, snap: DataSnapshot): MutableList<T> {
val arrayList: MutableList<T> = 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 <T> MutableLiveData<MutableList<T>>.addNewItem(item: T) {
val newList = mutableListOf<T>()
this.value?.let { newList.addAll(it) }
newList.add(item)
this.value = newList
}
fun <T> MutableLiveData<MutableList<T>>.updateItemAt(item: T, index: Int) {
val newList = mutableListOf<T>()
this.value?.let { newList.addAll(it) }
newList[index] = item
this.value = newList
}
fun <T> MutableLiveData<MutableList<T>>.removeItem(item: T) {
val newList = mutableListOf<T>()
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
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M20,2H4c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_error_24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_notifications_24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_people_24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_person_24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/ic_baseline_settings_24.xml
================================================
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector>
================================================
FILE: app/src/main/res/drawable/round_circle_online_green.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/holo_green_light" />
<stroke android:color="@android:color/white" android:width="1.5dp" />
<size
android:width="20dp"
android:height="20dp" />
</shape>
================================================
FILE: app/src/main/res/drawable/round_circle_primary.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorPrimary" />
<size
android:width="20dp"
android:height="20dp" />
</shape>
================================================
FILE: app/src/main/res/drawable-v24/rounded_rectangle_primary.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorPrimary" />
<corners android:radius="10dp" />
</shape>
================================================
FILE: app/src/main/res/drawable-v24/rounded_rectangle_secondary.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#F1F1F1" />
<corners android:radius="5dp" />
</shape>
================================================
FILE: app/src/main/res/font/nunito.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="Nunito"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
================================================
FILE: app/src/main/res/font/nunito_bold.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Nunito&weight=700"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
================================================
FILE: app/src/main/res/font/nunito_extrabold.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Nunito&weight=800"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
================================================
FILE: app/src/main/res/font/nunito_semibold.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:app="http://schemas.android.com/apk/res-auto"
app:fontProviderAuthority="com.google.android.gms.fonts"
app:fontProviderPackage="com.google.android.gms"
app:fontProviderQuery="name=Nunito&weight=600"
app:fontProviderCerts="@array/com_google_android_gms_fonts_certs">
</font-family>
================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/main_toolbar"
layout="@layout/toolbar_main"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="?attr/actionBarSize"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/nav_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/mobile_navigation"
tools:ignore="FragmentTagUsage" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:menu="@menu/bottom_nav_menu" />
<ProgressBar
android:id="@+id/main_progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
================================================
FILE: app/src/main/res/layout/fragment_chat.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.chat.ChatViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/textContentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messagesRecyclerView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:focusable="true"
android:focusableInTouchMode="true"
app:bind_disable_item_animator="@{true}"
app:bind_messages_list="@{viewmodel.messagesList}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toTopOf="@id/layoutChatbox"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_item_message_received" />
<View
android:layout_width="0dp"
android:layout_height="2dp"
android:layout_marginBottom="0dp"
android:background="#dfdfdf"
app:layout_constraintBottom_toTopOf="@+id/layoutChatbox"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<LinearLayout
android:id="@+id/layoutChatbox"
android:layout_width="0dp"
android:layout_height="50dp"
android:background="@android:color/white"
android:minHeight="48dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent">
<EditText
android:id="@+id/editTextMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_weight="1"
android:background="@android:color/transparent"
android:hint="@string/enter_message"
android:importantForAutofill="no"
android:inputType="text"
android:maxLines="6"
android:text="@={viewmodel.newMessageText}" />
<Button
android:id="@+id/sendBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:clickable="true"
android:onClick="@{() -> viewmodel.sendMessagePressed()}"
android:text="@string/send"
android:textSize="14sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_chats.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.chats.ChatsViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chatsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bind_chats_list="@{viewmodel.chatsList}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_item_user" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_create_account.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.start.createAccount.CreateAccountViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="fill_parent">
<LinearLayout
android:id="@+id/textContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="70dp"
android:layout_marginEnd="20dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/titleText"
style="@style/BoldText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:gravity="start"
android:text="@string/create_a_new_account"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:boxBackgroundColor="@android:color/transparent">
<EditText
android:id="@+id/editTextDisplayName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/display_name"
android:importantForAutofill="no"
android:inputType="textCapWords"
android:maxLength="25"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@={viewmodel.displayNameText}"
tools:background="@android:color/transparent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:padding="0dp"
app:boxBackgroundColor="@android:color/transparent">
<EditText
android:id="@+id/editTextEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/email"
android:importantForAutofill="no"
android:inputType="textEmailAddress"
android:maxLength="25"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@={viewmodel.emailText}"
tools:background="@android:color/transparent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:padding="0dp"
app:boxBackgroundColor="@android:color/transparent">
<EditText
android:id="@+id/editTextPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLength="25"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@={viewmodel.passwordText}"
tools:background="@android:color/transparent" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<Button
android:id="@+id/loginButton"
android:layout_width="300dp"
android:layout_height="60dp"
android:layout_marginTop="98dp"
android:backgroundTint="@color/colorAccent"
android:enabled="@{!viewmodel.isCreatingAccount()}"
android:onClick="@{() -> viewmodel.createAccountPressed()}"
android:text="@string/create"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline50"
tools:enabled="@{true}" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline50"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent=".5" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_login.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.start.login.LoginViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="fill_parent">
<LinearLayout
android:id="@+id/textContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="70dp"
android:layout_marginEnd="20dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/titleText"
style="@style/BoldText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:gravity="start"
android:text="@string/login_to_your_account"
android:textSize="30sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:boxBackgroundColor="@android:color/transparent">
<EditText
android:id="@+id/editTextEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/email"
android:importantForAutofill="no"
android:inputType="textEmailAddress"
android:maxLength="25"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@={viewmodel.emailText}"
tools:background="@android:color/transparent" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editTextPasswordInputLayout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:padding="0dp"
app:boxBackgroundColor="@android:color/transparent">
<EditText
android:id="@+id/editTextPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:ems="10"
android:hint="@string/password"
android:importantForAutofill="no"
android:inputType="textPassword"
android:maxLength="25"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:text="@={viewmodel.passwordText}"
tools:background="@android:color/transparent" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<Button
android:id="@+id/loginButton"
android:layout_width="300dp"
android:layout_height="60dp"
android:layout_marginTop="98dp"
android:enabled="@{!viewmodel.isLoggingIn()}"
android:onClick="@{() -> viewmodel.loginPressed()}"
android:text="@string/login"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline50"
tools:enabled="@{true}" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline50"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent=".5" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_notifications.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.notifications.NotificationsViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/usersRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bind_notifications_list="@{viewmodel.usersInfoList}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_item_notification" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_profile.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="com.fredrikbogg.android_chat_app.ui.chats.ChatsViewModel" />
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.profile.ProfileViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="fill_parent">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline60"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent=".6" />
<ImageView
android:id="@+id/blurredUserImage"
android:layout_width="0dp"
android:layout_height="170dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url_blur="@{viewmodel.otherUser.info.profileImageUrl}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/userImageCardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="60dp"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="@id/blurredUserImage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/blurredUserImage"
app:strokeColor="@android:color/white"
app:strokeWidth="2dp">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_margin="0dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url="@{viewmodel.otherUser.info.profileImageUrl}"
tools:src="@tools:sample/avatars[0]" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:id="@+id/nameText"
style="@style/BoldText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@{viewmodel.otherUser.info.displayName}"
android:textSize="36sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/userImageCardView"
tools:text="Name" />
<TextView
android:id="@+id/statusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:maxLength="40"
style="@style/MessageSeen"
android:singleLine="true"
android:text="@{viewmodel.otherUser.info.status}"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/nameText"
tools:text="Status" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/statesLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="42dp"
android:layout_marginTop="42dp"
android:layout_marginEnd="42dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline60">
<Button
android:id="@+id/addFriendButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="@{() -> viewmodel.addFriendPressed()}"
android:text="@string/add_friend"
android:textSize="16sp"
android:visibility="@{viewmodel.layoutState == viewmodel.layoutState.NOT_FRIEND? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<Button
android:id="@+id/removeFriendButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="@{() -> viewmodel.removeFriendPressed()}"
android:text="@string/remove_friend"
android:textSize="16sp"
android:visibility="@{viewmodel.layoutState == viewmodel.layoutState.IS_FRIEND? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<Button
android:id="@+id/requestSentButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:enabled="false"
android:text="@string/request_sent"
android:textSize="16sp"
android:visibility="@{viewmodel.layoutState == viewmodel.layoutState.REQUEST_SENT? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<Button
android:id="@+id/acceptRequestButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:onClick="@{() -> viewmodel.acceptFriendRequestPressed()}"
android:text="@string/accept_friend_request"
android:textSize="16sp"
android:visibility="@{viewmodel.layoutState == viewmodel.layoutState.ACCEPT_DECLINE? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<Button
android:id="@+id/declineRequestButton"
android:layout_width="match_parent"
android:backgroundTint="@color/textError"
android:layout_height="55dp"
android:layout_marginTop="5dp"
android:onClick="@{() -> viewmodel.declineFriendRequestPressed()}"
android:text="@string/decline_friend_request"
android:textSize="16sp"
android:visibility="@{viewmodel.layoutState == viewmodel.layoutState.ACCEPT_DECLINE? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toBottomOf="@id/acceptRequestButton"
tools:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_settings.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.settings.SettingsViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize">
<Button
android:id="@+id/logoutButton"
style="@style/VeryBoldText"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="42dp"
android:backgroundTint="@color/textError"
android:onClick="@{() -> viewmodel.logoutUserPressed()}"
android:text="@string/logout"
android:textColor="@android:color/white"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<ImageView
android:id="@+id/blurredUserImage"
android:layout_width="0dp"
android:layout_height="170dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url_blur="@{viewmodel.userInfo.profileImageUrl}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/userImageCardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="60dp"
app:cardPreventCornerOverlap="true"
app:layout_constraintBottom_toBottomOf="@id/blurredUserImage"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/blurredUserImage"
app:strokeColor="@android:color/white"
app:strokeWidth="2dp">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_margin="0dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url="@{viewmodel.userInfo.profileImageUrl}"
tools:src="@tools:sample/avatars[0]" />
</com.google.android.material.card.MaterialCardView>
<LinearLayout
android:id="@+id/contentLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@id/logoutButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/userImageCardView">
<TextView
android:id="@+id/nameText"
style="@style/BoldText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@{viewmodel.userInfo.displayName}"
android:textSize="36sp"
tools:text="Name" />
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
style="@style/MessageSeen"
android:text="@{viewmodel.userInfo.status}"
android:textSize="16sp"
tools:text="This is a status message" />
<Button
android:id="@+id/changeImageButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginStart="24dp"
android:layout_marginTop="62dp"
android:layout_marginEnd="24dp"
android:onClick="@{() -> viewmodel.changeUserImagePressed()}"
android:text="@string/change_image"
android:textSize="14sp" />
<Button
android:id="@+id/changeStatusButton"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginStart="24dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="24dp"
android:onClick="@{() -> viewmodel.changeUserStatusPressed()}"
android:text="@string/change_status"
android:textSize="14sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_start.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.start.StartViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="fill_parent">
<TextView
android:id="@+id/textView2"
style="@style/BoldText"
android:layout_width="280dp"
android:layout_height="wrap_content"
android:layout_marginTop="35dp"
android:gravity="center_horizontal"
android:text="@string/welcome_to_quick_chat"
android:textSize="36sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageView" />
<Button
android:id="@+id/loginButton"
android:layout_width="0dp"
android:layout_height="55dp"
android:layout_marginStart="20dp"
android:layout_marginTop="50dp"
android:layout_marginEnd="20dp"
android:onClick="@{() -> viewmodel.goToLoginPressed()}"
android:text="@string/login"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/guideline50" />
<Button
android:id="@+id/createAccountButton"
android:layout_width="0dp"
android:layout_height="55dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="20dp"
android:backgroundTint="@color/colorAccent"
android:onClick="@{() -> viewmodel.goToCreateAccountPressed()}"
android:text="@string/create_account"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/loginButton" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline50"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent=".5" />
<ImageView
android:id="@+id/imageView"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_marginTop="40dp"
android:src="@drawable/chat_box"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteY="73dp"
android:contentDescription="@string/chat_icon" />
<TextView
android:id="@+id/textView"
style="@style/BoldText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:alpha=".8"
android:gravity="center"
android:text="@string/login_or_create_an_account_to_get_started"
android:textSize="16sp"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/createAccountButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/fragment_users.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.users.UsersViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="?attr/actionBarSize">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/usersRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:bind_users_list="@{viewmodel.usersList}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/list_item_user" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>
================================================
FILE: app/src/main/res/layout/list_item_chat.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.chats.ChatsViewModel" />
<variable
name="chatwithuserinfo"
type="com.fredrikbogg.android_chat_app.data.model.ChatWithUserInfo" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="12dp"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> viewmodel.selectChatWithUserInfoPressed(chatwithuserinfo)}"
app:bind_message="@{chatwithuserinfo.MChat.lastMessage}"
app:bind_message_textView="@{messageText}"
app:bind_message_view="@{notSeenView}"
app:bind_myUserID="@{viewmodel.myUserID}">
<androidx.cardview.widget.CardView
android:id="@+id/imageCardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="29dp"
app:cardElevation="0dp"
app:cardPreventCornerOverlap="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/userProfileImage"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_margin="0dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url="@{chatwithuserinfo.MUserInfo.profileImageUrl}"
tools:src="@tools:sample/avatars[0]" />
</androidx.cardview.widget.CardView>
<View
android:id="@+id/onlineView"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/round_circle_online_green"
android:visibility="@{chatwithuserinfo.MUserInfo.online == true? View.VISIBLE : View.INVISIBLE}"
app:layout_constraintBottom_toBottomOf="@id/imageCardView"
app:layout_constraintRight_toRightOf="@id/imageCardView"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginTop="3dp"
android:layout_marginBottom="3dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/imageCardView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/displayNameText"
style="@style/BoldText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="@{chatwithuserinfo.MUserInfo.displayName}"
android:textSize="18sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/timeText"
app:layout_constraintTop_toTopOf="parent"
tools:text="Display name" />
<TextView
android:id="@+id/messageText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:singleLine="true"
android:textSize="14sp"
app:bind_chat_message_text="@{chatwithuserinfo.MChat.lastMessage}"
app:bind_chat_message_text_viewModel="@{viewmodel}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/notSeenLayout"
tools:text="Message"
tools:textAppearance="@style/MessageNotSeen" />
<TextView
android:id="@+id/timeText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="12sp"
app:bind_epochTimeMsToDate_with_days_ago="@{chatwithuserinfo.MChat.lastMessage.epochTimeMs}"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="11:00 AM" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/notSeenLayout"
android:layout_width="54dp"
android:layout_height="23dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent">
<View
android:id="@+id/notSeenView"
android:layout_width="8dp"
android:layout_height="8dp"
android:background="@drawable/round_circle_primary"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/list_item_message_received.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.chat.ChatViewModel" />
<variable
name="message"
type="com.fredrikbogg.android_chat_app.data.db.entity.Message" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/timeText"
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center"
android:textSize="12sp"
app:bind_epochTimeMsToDate="@{message.epochTimeMs}"
app:bind_message="@{message}"
app:bind_message_viewModel="@{viewmodel}"
tools:text="11:40" />
<TextView
android:id="@+id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="12dp"
android:background="@drawable/rounded_rectangle_secondary"
android:maxWidth="240dp"
android:paddingLeft="12dp"
android:paddingTop="6dp"
android:paddingRight="12dp"
android:paddingBottom="6dp"
android:text="@{message.text}"
android:textColor="@android:color/black"
android:textSize="16sp"
tools:text="This is a message" />
</LinearLayout>
</layout>
================================================
FILE: app/src/main/res/layout/list_item_message_sent.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.chat.ChatViewModel" />
<variable
name="message"
type="com.fredrikbogg.android_chat_app.data.db.entity.Message" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/timeText"
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center"
android:textSize="12sp"
app:bind_epochTimeMsToDate="@{message.epochTimeMs}"
app:bind_message="@{message}"
app:bind_message_viewModel="@{viewmodel}"
tools:text="11:40" />
<TextView
android:id="@+id/messageText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="12dp"
android:background="@drawable/rounded_rectangle_primary"
android:maxWidth="240dp"
android:paddingLeft="12dp"
android:paddingTop="6dp"
android:paddingRight="12dp"
android:paddingBottom="6dp"
android:text="@{message.text}"
android:textColor="@android:color/white"
android:textSize="16sp"
tools:text="This is a message" />
</LinearLayout>
</layout>
================================================
FILE: app/src/main/res/layout/list_item_notification.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.notifications.NotificationsViewModel" />
<variable
name="userinfo"
type="com.fredrikbogg.android_chat_app.data.db.entity.UserInfo" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="12dp">
<androidx.cardview.widget.CardView
android:id="@+id/chatCardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="29dp"
app:cardPreventCornerOverlap="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/userProfileImage"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_margin="0dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url="@{userinfo.profileImageUrl}"
tools:src="@tools:sample/avatars[0]" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/displayNameText"
style="@style/BoldText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:singleLine="true"
android:text="@{userinfo.displayName}"
android:textSize="18sp"
app:layout_constraintLeft_toRightOf="@id/chatCardView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/chatCardView"
tools:text="Display name" />
<TextView
android:id="@+id/requestText"
style="@style/BoldText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="7dp"
android:text="@string/new_friend_request"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/chatCardView"
app:layout_constraintLeft_toRightOf="@id/chatCardView" />
<Button
android:id="@+id/acceptButton"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_marginTop="10dp"
android:onClick="@{() -> viewmodel.acceptNotificationPressed(userinfo)}"
android:text="@string/accept"
app:layout_constraintLeft_toLeftOf="@id/requestText"
app:layout_constraintTop_toBottomOf="@id/requestText" />
<Button
android:id="@+id/declineButton"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_marginStart="24dp"
android:backgroundTint="@color/textError"
android:onClick="@{() -> viewmodel.declineNotificationPressed(userinfo)}"
android:text="@string/decline"
app:layout_constraintLeft_toRightOf="@id/acceptButton"
app:layout_constraintTop_toTopOf="@id/acceptButton" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/list_item_user.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.users.UsersViewModel" />
<variable
name="user"
type="com.fredrikbogg.android_chat_app.data.db.entity.User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="12dp"
android:clickable="true"
android:focusable="true"
android:onClick="@{() -> viewmodel.selectUser(user)}">
<androidx.cardview.widget.CardView
android:id="@+id/imageCardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="29dp"
app:cardPreventCornerOverlap="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/userProfileImage"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_margin="0dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url="@{user.info.profileImageUrl}"
tools:src="@tools:sample/avatars[0]" />
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/displayNameText"
style="@style/BoldText"
android:layout_width="0dp"
android:layout_marginTop="3dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:singleLine="true"
android:text="@{user.info.displayName}"
android:textSize="18sp"
app:layout_constraintLeft_toRightOf="@id/imageCardView"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/imageCardView"
tools:text="Display name" />
<TextView
android:id="@+id/statusText"
style="@style/MessageSeen"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="3dp"
android:layout_marginStart="16dp"
android:singleLine="true"
android:text="@{user.info.status}"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/imageCardView"
app:layout_constraintLeft_toRightOf="@id/imageCardView"
app:layout_constraintRight_toRightOf="parent"
tools:text="Status" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
================================================
FILE: app/src/main/res/layout/toolbar_addon_chat.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="viewmodel"
type="com.fredrikbogg.android_chat_app.ui.chat.ChatViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:minHeight="?attr/actionBarSize"
tools:layout_height="?attr/actionBarSize">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/imageContentLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_centerInParent="true">
<androidx.cardview.widget.CardView
android:id="@+id/imageCardView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:cardCornerRadius="22dp"
app:cardElevation="0dp"
app:cardPreventCornerOverlap="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/userProfileImage"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_margin="0dp"
android:contentDescription="@string/user_image"
android:scaleType="centerCrop"
app:bind_image_url="@{viewmodel.otherUser.profileImageUrl}"
tools:src="@drawable/ic_baseline_person_24" />
</androidx.cardview.widget.CardView>
<View
android:id="@+id/onlineView"
android:layout_width="11dp"
android:layout_height="11dp"
android:layout_marginStart="35dp"
android:background="@drawable/round_circle_online_green"
android:visibility="@{viewmodel.otherUser.online == true? View.VISIBLE : View.INVISIBLE}"
app:layout_constraintBottom_toBottomOf="@id/imageCardView"
app:layout_constraintRight_toRightOf="@id/imageCardView"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_centerInParent="true"
android:layout_marginStart="16dp"
android:layout_toEndOf="@id/imageContentLayout"
android:orientation="vertical">
<TextView
android:id="@+id/otherUserNameText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@{viewmodel.otherUser.displayName}"
android:textColor="@android:color/white"
android:textSize="18sp"
tools:text="John" />
<TextView
android:id="@+id/onlineStatusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha=".7"
android:maxLines="1"
android:text="@{viewmodel.otherUser.online == true? `Online` : `Offline`}"
android:textColor="@android:color/white"
android:textSize="14sp"
tools:text="Online" />
</LinearLayout>
</RelativeLayout>
</layout>
================================================
FILE: app/src/main/res/layout/toolbar_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:minHeight="?attr/actionBarSize"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark"
tools:ignore="Overdraw">
</androidx.appcompat.widget.Toolbar>
================================================
FILE: app/src/main/res/menu/bottom_nav_menu.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/navigation_chats"
android:icon="@drawable/ic_baseline_chat_bubble_24"
android:title="@string/title_chats" />
<item
android:id="@+id/navigation_notifications"
android:icon="@drawable/ic_baseline_notifications_24"
android:title="@string/title_notifications" />
<item
android:id="@+id/navigation_users"
android:icon="@drawable/ic_baseline_people_24"
android:title="@string/title_users" />
<item
android:id="@+id/navigation_settings"
android:icon="@drawable/ic_baseline_settings_24"
android:title="@string/title_settings" />
</menu>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
================================================
FILE: app/src/main/res/navigation/mobile_navigation.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
app:startDestination="@+id/startFragment">
<fragment
android:id="@+id/navigation_chats"
android:name="com.fredrikbogg.android_chat_app.ui.chats.ChatsFragment"
android:label="@string/title_chats"
tools:layout="@layout/fragment_chats">
<action
android:id="@+id/action_navigation_chats_to_chatFragment"
app:destination="@id/chatFragment" />
</fragment>
<fragment
android:id="@+id/navigation_notifications"
android:name="com.fredrikbogg.android_chat_app.ui.notifications.NotificationsFragment"
android:label="@string/title_notifications"
tools:layout="@layout/fragment_notifications" />
<fragment
android:id="@+id/navigation_users"
android:name="com.fredrikbogg.android_chat_app.ui.users.UsersFragment"
android:label="@string/title_users"
tools:layout="@layout/fragment_users">
<action
android:id="@+id/a
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
Condensed preview — 118 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (222K chars).
[
{
"path": ".gitattributes",
"chars": 66,
"preview": "# Auto detect text files and perform LF normalization\n* text=auto\n"
},
{
"path": ".gitignore",
"chars": 1436,
"preview": "# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Ge"
},
{
"path": ".idea/.name",
"chars": 16,
"preview": "Android-Chat-App"
},
{
"path": ".idea/codeStyles/Project.xml",
"chars": 4353,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <code_scheme name=\"Project\" version=\"173\">\n <JetCodeStyleSettings>"
},
{
"path": ".idea/codeStyles/codeStyleConfig.xml",
"chars": 142,
"preview": "<component name=\"ProjectCodeStyleConfiguration\">\n <state>\n <option name=\"USE_PER_PROJECT_SETTINGS\" value=\"true\" />\n "
},
{
"path": ".idea/jarRepositories.xml",
"chars": 1052,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RemoteRepositoriesConfiguration\">\n <r"
},
{
"path": ".idea/misc.xml",
"chars": 357,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"ProjectRootManager\" version=\"2\" language"
},
{
"path": ".idea/render.experimental.xml",
"chars": 173,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RenderSettings\">\n <option name=\"showD"
},
{
"path": ".idea/runConfigurations.xml",
"chars": 564,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n <component name=\"RunConfigurationProducerService\">\n <o"
},
{
"path": "LICENSE",
"chars": 1069,
"preview": "MIT License\n\nCopyright (c) 2020 Fredrik Bogg\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
},
{
"path": "README.md",
"chars": 2868,
"preview": "# Chat App Android\n\n\n## Introduction\nThis is a demo application built with the g"
},
{
"path": "app/.gitignore",
"chars": 6,
"preview": "/build"
},
{
"path": "app/build.gradle",
"chars": 2381,
"preview": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\napply p"
},
{
"path": "app/google-services.json",
"chars": 11,
"preview": "-EDIT THIS-"
},
{
"path": "app/proguard-rules.pro",
"chars": 750,
"preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
},
{
"path": "app/src/androidTest/java/com/fredrikbogg/android_chat_app/ExampleInstrumentedTest.kt",
"chars": 691,
"preview": "package com.fredrikbogg.android_chat_app\n\nimport androidx.test.platform.app.InstrumentationRegistry\nimport androidx.test"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 1118,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n package="
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/App.kt",
"chars": 581,
"preview": "package com.fredrikbogg.android_chat_app\n\nimport android.app.Application\nimport com.fredrikbogg.android_chat_app.util.Sh"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/Event.kt",
"chars": 570,
"preview": "package com.fredrikbogg.android_chat_app.data\n\nimport androidx.lifecycle.Observer\n\n\nopen class Event<out T>(private val "
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/Result.kt",
"chars": 269,
"preview": "package com.fredrikbogg.android_chat_app.data\n\n\nsealed class Result<out R> {\n data class Success<out T>(val data: T? "
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Chat.kt",
"chars": 418,
"preview": "package com.fredrikbogg.android_chat_app.data.db.entity\n\nimport com.google.firebase.database.PropertyName\n\n\ndata class C"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/Message.kt",
"chars": 507,
"preview": "package com.fredrikbogg.android_chat_app.data.db.entity\n\nimport com.google.firebase.database.PropertyName\nimport java.ut"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/entity/User.kt",
"chars": 1432,
"preview": "package com.fredrikbogg.android_chat_app.data.db.entity\n\nimport com.google.firebase.database.PropertyName\n\n\ndata class U"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseAuthSource.kt",
"chars": 1956,
"preview": "package com.fredrikbogg.android_chat_app.data.db.remote\n\nimport com.fredrikbogg.android_chat_app.data.model.CreateUser\ni"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseDatabaseSource.kt",
"chars": 11041,
"preview": "package com.fredrikbogg.android_chat_app.data.db.remote\n\nimport com.fredrikbogg.android_chat_app.data.Result\nimport com."
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/remote/FirebaseStorageSource.kt",
"chars": 562,
"preview": "package com.fredrikbogg.android_chat_app.data.db.remote\n\nimport android.net.Uri\nimport com.google.android.gms.tasks.Task"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/AuthRepository.kt",
"chars": 1438,
"preview": "package com.fredrikbogg.android_chat_app.data.db.repository\n\nimport com.fredrikbogg.android_chat_app.data.model.CreateUs"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/DatabaseRepository.kt",
"chars": 5987,
"preview": "package com.fredrikbogg.android_chat_app.data.db.repository\n\nimport com.fredrikbogg.android_chat_app.data.db.entity.*\nim"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/db/repository/StorageRepository.kt",
"chars": 665,
"preview": "package com.fredrikbogg.android_chat_app.data.db.repository\n\nimport android.net.Uri\nimport com.fredrikbogg.android_chat_"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/model/ChatWithUserInfo.kt",
"chars": 258,
"preview": "package com.fredrikbogg.android_chat_app.data.model\n\nimport com.fredrikbogg.android_chat_app.data.db.entity.Chat\nimport "
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/model/CreateUser.kt",
"chars": 169,
"preview": "package com.fredrikbogg.android_chat_app.data.model\n\ndata class CreateUser(\n var displayName: String = \"\",\n var em"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/data/model/Login.kt",
"chars": 130,
"preview": "package com.fredrikbogg.android_chat_app.data.model\n\ndata class Login(\n var email: String = \"\",\n var password: Str"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultBindings.kt",
"chars": 2573,
"preview": "package com.fredrikbogg.android_chat_app.ui\n\nimport android.annotation.SuppressLint\nimport android.widget.ImageView\nimpo"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/DefaultViewModel.kt",
"chars": 1170,
"preview": "package com.fredrikbogg.android_chat_app.ui\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLiveDat"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatFragment.kt",
"chars": 4034,
"preview": "package com.fredrikbogg.android_chat_app.ui.chat\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport and"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/ChatViewModel.kt",
"chars": 3341,
"preview": "package com.fredrikbogg.android_chat_app.ui.chat\n\nimport androidx.lifecycle.*\nimport com.fredrikbogg.android_chat_app.da"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesBindings.kt",
"chars": 1098,
"preview": "package com.fredrikbogg.android_chat_app.ui.chat\n\nimport android.view.View\nimport androidx.databinding.BindingAdapter\nim"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chat/MessagesListAdapter.kt",
"chars": 3104,
"preview": "package com.fredrikbogg.android_chat_app.ui.chat\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimpor"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsBindings.kt",
"chars": 1408,
"preview": "@file:Suppress(\"unused\")\n\npackage com.fredrikbogg.android_chat_app.ui.chats\n\nimport android.view.View\nimport android.wid"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsFragment.kt",
"chars": 2484,
"preview": "package com.fredrikbogg.android_chat_app.ui.chats\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport an"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsListAdapter.kt",
"chars": 1719,
"preview": "package com.fredrikbogg.android_chat_app.ui.chats\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimpo"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/chats/ChatsViewModel.kt",
"chars": 3599,
"preview": "package com.fredrikbogg.android_chat_app.ui.chats\n\nimport androidx.lifecycle.*\nimport com.fredrikbogg.android_chat_app.d"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainActivity.kt",
"chars": 3491,
"preview": "package com.fredrikbogg.android_chat_app.ui.main\n\nimport android.os.Bundle\nimport android.view.View\nimport android.widge"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/main/MainViewModel.kt",
"chars": 2485,
"preview": "package com.fredrikbogg.android_chat_app.ui.main\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableLi"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsBindings.kt",
"chars": 421,
"preview": "package com.fredrikbogg.android_chat_app.ui.notifications\n\nimport androidx.databinding.BindingAdapter\nimport androidx.re"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsFragment.kt",
"chars": 1552,
"preview": "package com.fredrikbogg.android_chat_app.ui.notifications\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\ni"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsListAdapter.kt",
"chars": 1664,
"preview": "package com.fredrikbogg.android_chat_app.ui.notifications\n\nimport android.view.LayoutInflater\nimport android.view.ViewGr"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/notifications/NotificationsViewModel.kt",
"chars": 3031,
"preview": "package com.fredrikbogg.android_chat_app.ui.notifications\n\nimport androidx.lifecycle.MediatorLiveData\nimport androidx.li"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileFragment.kt",
"chars": 2260,
"preview": "package com.fredrikbogg.android_chat_app.ui.profile\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport "
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/profile/ProfileViewModel.kt",
"chars": 3685,
"preview": "package com.fredrikbogg.android_chat_app.ui.profile\n\nimport androidx.lifecycle.*\nimport com.fredrikbogg.android_chat_app"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsFragment.kt",
"chars": 3818,
"preview": "package com.fredrikbogg.android_chat_app.ui.settings\n\nimport android.app.Activity.RESULT_OK\nimport android.content.Conte"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/settings/SettingsViewModel.kt",
"chars": 2915,
"preview": "package com.fredrikbogg.android_chat_app.ui.settings\n\nimport android.net.Uri\nimport androidx.lifecycle.LiveData\nimport a"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartFragment.kt",
"chars": 2124,
"preview": "package com.fredrikbogg.android_chat_app.ui.start\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport an"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/StartViewModel.kt",
"chars": 685,
"preview": "package com.fredrikbogg.android_chat_app.ui.start\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.MutableL"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountFragment.kt",
"chars": 2504,
"preview": "package com.fredrikbogg.android_chat_app.ui.start.createAccount\n\nimport android.os.Bundle\nimport android.view.LayoutInfl"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/createAccount/CreateAccountViewModel.kt",
"chars": 2514,
"preview": "package com.fredrikbogg.android_chat_app.ui.start.createAccount\n\nimport androidx.lifecycle.LiveData\nimport androidx.life"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginFragment.kt",
"chars": 2449,
"preview": "package com.fredrikbogg.android_chat_app.ui.start.login\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimp"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/start/login/LoginViewModel.kt",
"chars": 1790,
"preview": "package com.fredrikbogg.android_chat_app.ui.start.login\n\nimport androidx.lifecycle.LiveData\nimport androidx.lifecycle.Mu"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersBindings.kt",
"chars": 382,
"preview": "package com.fredrikbogg.android_chat_app.ui.users\n\nimport androidx.databinding.BindingAdapter\nimport androidx.recyclervi"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersFragment.kt",
"chars": 2137,
"preview": "package com.fredrikbogg.android_chat_app.ui.users\n\nimport android.os.Bundle\nimport android.view.LayoutInflater\nimport an"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersListAdapter.kt",
"chars": 1566,
"preview": "package com.fredrikbogg.android_chat_app.ui.users\n\nimport android.view.LayoutInflater\nimport android.view.ViewGroup\nimpo"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/ui/users/UsersViewModel.kt",
"chars": 1431,
"preview": "package com.fredrikbogg.android_chat_app.ui.users\n\nimport androidx.lifecycle.*\nimport com.fredrikbogg.android_chat_app.d"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/util/FileConverterUtil.kt",
"chars": 623,
"preview": "package com.fredrikbogg.android_chat_app.util\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport andr"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/util/FirebaseUtil.kt",
"chars": 725,
"preview": "package com.fredrikbogg.android_chat_app.util\n\nimport com.google.firebase.database.DataSnapshot\n\nfun <T> wrapSnapshotToC"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/util/LiveDataExt.kt",
"chars": 686,
"preview": "package com.fredrikbogg.android_chat_app.util\n\nimport androidx.lifecycle.MutableLiveData\n\nfun <T> MutableLiveData<Mutabl"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/util/SharedPreferencesUtil.kt",
"chars": 782,
"preview": "package com.fredrikbogg.android_chat_app.util\n\nimport android.content.Context\nimport android.content.SharedPreferences\n\n"
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/util/TextUtil.kt",
"chars": 334,
"preview": "package com.fredrikbogg.android_chat_app.util\n\nfun isEmailValid(email: CharSequence): Boolean {\n return android.util."
},
{
"path": "app/src/main/java/com/fredrikbogg/android_chat_app/util/ViewExt.kt",
"chars": 649,
"preview": "package com.fredrikbogg.android_chat_app.util\n\nimport android.content.Context\nimport android.view.View\nimport android.vi"
},
{
"path": "app/src/main/res/drawable/ic_baseline_chat_bubble_24.xml",
"chars": 398,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_error_24.xml",
"chars": 423,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_notifications_24.xml",
"chars": 508,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_people_24.xml",
"chars": 645,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_person_24.xml",
"chars": 444,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/ic_baseline_settings_24.xml",
"chars": 1223,
"preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:width=\"24dp\"\n android:height=\"24dp\"\n "
},
{
"path": "app/src/main/res/drawable/round_circle_online_green.xml",
"chars": 350,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:sha"
},
{
"path": "app/src/main/res/drawable/round_circle_primary.xml",
"chars": 263,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:sha"
},
{
"path": "app/src/main/res/drawable-v24/rounded_rectangle_primary.xml",
"chars": 233,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:sha"
},
{
"path": "app/src/main/res/drawable-v24/rounded_rectangle_secondary.xml",
"chars": 222,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:sha"
},
{
"path": "app/src/main/res/font/nunito.xml",
"chars": 355,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n app:font"
},
{
"path": "app/src/main/res/font/nunito_bold.xml",
"chars": 375,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n app:font"
},
{
"path": "app/src/main/res/font/nunito_extrabold.xml",
"chars": 375,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n app:font"
},
{
"path": "app/src/main/res/font/nunito_semibold.xml",
"chars": 375,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n app:font"
},
{
"path": "app/src/main/res/layout/activity_main.xml",
"chars": 2329,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<androidx.constraintlayout.widget.ConstraintLayout xmlns:android=\"http://schema"
},
{
"path": "app/src/main/res/layout/fragment_chat.xml",
"chars": 3330,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_chats.xml",
"chars": 1580,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_create_account.xml",
"chars": 6030,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_login.xml",
"chars": 4978,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_notifications.xml",
"chars": 1616,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_profile.xml",
"chars": 7649,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:ap"
},
{
"path": "app/src/main/res/layout/fragment_settings.xml",
"chars": 5465,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_start.xml",
"chars": 4072,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/fragment_users.xml",
"chars": 1580,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/list_item_chat.xml",
"chars": 6074,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/list_item_message_received.xml",
"chars": 1852,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/list_item_message_sent.xml",
"chars": 1846,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:ap"
},
{
"path": "app/src/main/res/layout/list_item_notification.xml",
"chars": 3790,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/list_item_user.xml",
"chars": 3090,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app"
},
{
"path": "app/src/main/res/layout/toolbar_addon_chat.xml",
"chars": 4180,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:app="
},
{
"path": "app/src/main/res/layout/toolbar_main.xml",
"chars": 488,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n\n<androidx.appcompat.widget.Toolbar xmlns:android=\"http://schemas.android.com/ap"
},
{
"path": "app/src/main/res/menu/bottom_nav_menu.xml",
"chars": 769,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<menu xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n <item\n "
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 265,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 265,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/navigation/mobile_navigation.xml",
"chars": 3859,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<navigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 263,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"colorPrimary\">#2D9CDB</color>\n <color name=\"color"
},
{
"path": "app/src/main/res/values/dimens.xml",
"chars": 96,
"preview": "<resources>\n <!-- Default screen margins, per the Android Design guidelines. -->\n</resources>"
},
{
"path": "app/src/main/res/values/font_certs.xml",
"chars": 3581,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <array name=\"com_google_android_gms_fonts_certs\">\n <item>@"
},
{
"path": "app/src/main/res/values/ic_launcher_background.xml",
"chars": 120,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
},
{
"path": "app/src/main/res/values/preloaded_fonts.xml",
"chars": 293,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <array name=\"preloaded_fonts\" translatable=\"false\">\n <item"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 1778,
"preview": "<resources>\n <string name=\"app_name\">Chat App</string>\n <string name=\"title_chats\">Chats</string>\n <string name"
},
{
"path": "app/src/main/res/values/styles.xml",
"chars": 1188,
"preview": "<resources>\n\n <style name=\"AppTheme\" parent=\"Theme.MaterialComponents.Light.NoActionBar\">\n <item name=\"colorPr"
},
{
"path": "app/src/test/java/com/fredrikbogg/android_chat_app/ExampleUnitTest.kt",
"chars": 356,
"preview": "package com.fredrikbogg.android_chat_app\n\nimport org.junit.Test\n\nimport org.junit.Assert.*\n\n/**\n * Example local unit te"
},
{
"path": "build.gradle",
"chars": 698,
"preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n ex"
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Wed Aug 05 11:42:06 CEST 2020\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER"
},
{
"path": "gradle.properties",
"chars": 1162,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 5296,
"preview": "#!/usr/bin/env sh\n\n##############################################################################\n##\n## Gradle start up"
},
{
"path": "gradlew.bat",
"chars": 2176,
"preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
},
{
"path": "settings.gradle",
"chars": 52,
"preview": "include ':app'\nrootProject.name = \"Android-Chat-App\""
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the dgewe/Chat-App-Android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 118 files (198.1 KB), approximately 51.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.