├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── profile.jpeg
│ │ │ │ ├── ic_firebase.xml
│ │ │ │ ├── ic_incognito.xml
│ │ │ │ ├── ic_google.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── colors.xml
│ │ │ ├── xml
│ │ │ │ ├── file_paths.xml
│ │ │ │ ├── remote_config_defaults.xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── io
│ │ │ │ └── devexpert
│ │ │ │ └── android_firebase
│ │ │ │ ├── model
│ │ │ │ ├── Note.kt
│ │ │ │ └── Contact.kt
│ │ │ │ ├── ui
│ │ │ │ ├── navigation
│ │ │ │ │ ├── Routes.kt
│ │ │ │ │ └── Navigation.kt
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ └── screens
│ │ │ │ │ ├── Screen.kt
│ │ │ │ │ ├── auth
│ │ │ │ │ ├── ForgotPasswordScreen.kt
│ │ │ │ │ ├── SignUpScreen.kt
│ │ │ │ │ └── LoginScreen.kt
│ │ │ │ │ ├── storage
│ │ │ │ │ └── CloudStorageScreen.kt
│ │ │ │ │ ├── db
│ │ │ │ │ ├── NotesScreen.kt
│ │ │ │ │ └── ContactsScreen.kt
│ │ │ │ │ └── HomeScreen.kt
│ │ │ │ ├── AndroidFirebaseApp.kt
│ │ │ │ ├── utils
│ │ │ │ ├── CloudStorageManager.kt
│ │ │ │ ├── AnalyticsManager.kt
│ │ │ │ ├── FirestoreManager.kt
│ │ │ │ ├── RealtimeManager.kt
│ │ │ │ └── AuthManager.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── MyFirebaseService.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── io
│ │ │ └── devexpert
│ │ │ └── android_firebase
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── io
│ │ └── devexpert
│ │ └── android_firebase
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── settings.gradle
├── README.md
├── gradle.properties
├── gradlew.bat
├── gradlew
└── .gitignore
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/drawable/profile.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/drawable/profile.jpeg
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devexpert-io/android-firebase-serie/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | android_firebase
3 | fcm_default_channel
4 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/model/Note.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.model
2 |
3 | data class Note(
4 | var id: String? = null,
5 | var userId: String = "",
6 | val title: String = "",
7 | val content: String = ""
8 | )
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 18 22:48:01 BOT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/model/Contact.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.model
2 |
3 | data class Contact(
4 | val key: String? = null,
5 | val name: String = "",
6 | val email: String = "",
7 | val phoneNumber: String = "",
8 | val uid: String = ""
9 | )
--------------------------------------------------------------------------------
/app/src/main/res/xml/remote_config_defaults.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | welcome_message
5 | | Firebase App
6 |
7 |
8 | is_button_visible
9 | true
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/navigation/Routes.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.navigation
2 |
3 | sealed class Routes(val route: String) {
4 | object Login : Routes("Login Screen")
5 | object Home : Routes("Home Screen")
6 | object SignUp : Routes("SignUp Screen")
7 | object ForgotPassword : Routes("ForgotPassword Screen")
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "android_firebase"
16 | include ':app'
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Firebase Android | Serie
2 |
3 | Repositorio con el código fuente de los videos de explicación con la integración de Firebase en Android con Kotlin y Jetpack Compose, los cuáles se encuentran en el canal de [DevExpert en YouTube](https://www.youtube.com/@devexpert_io/videos).
4 |
5 | ### Videos:
6 |
7 | | [](https://www.youtube.com/watch?v=fKESOlJmKGY "YouTube") | |
8 | | ------------- |:-------------:|
9 |
--------------------------------------------------------------------------------
/app/src/test/java/io/devexpert/android_firebase/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 | #FFF57C00
11 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/Screen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens
2 |
3 | import androidx.compose.foundation.layout.fillMaxSize
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.Surface
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import io.devexpert.android_firebase.ui.theme.Android_firebaseTheme
9 |
10 | @Composable
11 | fun Screen(content: @Composable () -> Unit) {
12 | Android_firebaseTheme {
13 | Surface(
14 | modifier = Modifier.fillMaxSize(),
15 | color = MaterialTheme.colorScheme.background
16 | ) {
17 | content()
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/AndroidFirebaseApp.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase
2 |
3 | import android.app.Application
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 |
7 | class AndroidFirebaseApp : Application() {
8 | companion object {
9 | const val FCM_CHANNEL_ID = "FCM_CHANNEL_ID"
10 | }
11 |
12 | override fun onCreate() {
13 | super.onCreate()
14 | if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
15 | val fcmChannel = NotificationChannel(FCM_CHANNEL_ID, "FCM_Channel", NotificationManager.IMPORTANCE_HIGH)
16 | val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
17 | manager.createNotificationChannel(fcmChannel)
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/io/devexpert/android_firebase/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("io.devexpert.android_firebase", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_firebase.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_incognito.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_google.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/utils/CloudStorageManager.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.utils
2 |
3 | import android.content.Context
4 | import android.net.Uri
5 | import com.google.firebase.ktx.Firebase
6 | import com.google.firebase.storage.ListResult
7 | import com.google.firebase.storage.StorageReference
8 | import com.google.firebase.storage.ktx.storage
9 | import kotlinx.coroutines.tasks.await
10 |
11 | class CloudStorageManager(context: Context) {
12 | private val storage = Firebase.storage
13 | private val storageRef = storage.reference
14 | private val authManager = AuthManager(context)
15 | private val userId = authManager.getCurrentUser()?.uid
16 |
17 | fun getStorageReference(): StorageReference {
18 | return storageRef.child("photos").child(userId ?: "")
19 | }
20 |
21 | suspend fun uploadFile(fileName: String, filePath: Uri) {
22 | val fileRef = getStorageReference().child(fileName)
23 | val uploadTask = fileRef.putFile(filePath)
24 | uploadTask.await()
25 | }
26 |
27 | suspend fun getUserImages(): List {
28 | val imageUrls = mutableListOf()
29 | val listResult: ListResult = getStorageReference().listAll().await()
30 | for (item in listResult.items) {
31 | val url = item.downloadUrl.await().toString()
32 | imageUrls.add(url)
33 | }
34 | return imageUrls
35 | }
36 | }
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/utils/AnalyticsManager.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.utils
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.DisposableEffect
7 | import com.google.firebase.analytics.FirebaseAnalytics
8 |
9 | class AnalyticsManager(context: Context) {
10 | private val firebaseAnalytics: FirebaseAnalytics by lazy { FirebaseAnalytics.getInstance(context) }
11 | private fun logEvent(eventName: String, params: Bundle) {
12 | firebaseAnalytics.logEvent(eventName, params)
13 | }
14 |
15 | fun logButtonClicked(buttonName: String) {
16 | val params = Bundle().apply {
17 | putString("button_name", buttonName)
18 | }
19 | logEvent("button_clicked", params)
20 | }
21 |
22 | @Composable
23 | fun logScreenView(screenName: String) {
24 | DisposableEffect(Unit) {
25 | onDispose {
26 | val params = Bundle().apply {
27 | putString(FirebaseAnalytics.Param.SCREEN_NAME, screenName)
28 | putString(FirebaseAnalytics.Param.SCREEN_CLASS, screenName)
29 | }
30 | logEvent(FirebaseAnalytics.Event.SCREEN_VIEW, params)
31 | }
32 | }
33 | }
34 |
35 | fun logError(error: String) {
36 | val params = Bundle().apply {
37 | putString("error", error)
38 | }
39 | logEvent("error", params)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/utils/FirestoreManager.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.utils
2 |
3 | import android.content.Context
4 | import com.google.firebase.firestore.FirebaseFirestore
5 | import io.devexpert.android_firebase.model.Note
6 | import kotlinx.coroutines.channels.awaitClose
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.callbackFlow
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.tasks.await
11 |
12 | class FirestoreManager(context: Context) {
13 | private val firestore = FirebaseFirestore.getInstance()
14 |
15 | private val auth = AuthManager(context)
16 | var userId = auth.getCurrentUser()?.uid
17 |
18 | suspend fun addNote(note: Note) {
19 | note.userId = userId.toString()
20 | firestore.collection("notes").add(note).await()
21 | }
22 |
23 | suspend fun updateNote(note: Note) {
24 | val noteRef = note.id?.let { firestore.collection("notes").document(it) }
25 | noteRef?.set(note)?.await()
26 | }
27 |
28 | suspend fun deleteNote(noteId: String) {
29 | val noteRef = firestore.collection("notes").document(noteId)
30 | noteRef.delete().await()
31 | }
32 |
33 | fun getNotesFlow(): Flow> = callbackFlow {
34 | val notesRef = firestore.collection("notes")
35 | .whereEqualTo("userId", userId).orderBy("title")
36 |
37 | val subscription = notesRef.addSnapshotListener { snapshot, _ ->
38 | snapshot?.let { querySnapshot ->
39 | val notes = mutableListOf()
40 | for (document in querySnapshot.documents) {
41 | val note = document.toObject(Note::class.java)
42 | note?.id = document.id
43 | note?.let { notes.add(it) }
44 | }
45 | trySend(notes).isSuccess
46 | }
47 | }
48 | awaitClose { subscription.remove() }
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase
2 |
3 | import android.content.pm.PackageManager
4 | import android.os.Build
5 | import android.os.Bundle
6 | import android.Manifest
7 | import android.util.Log
8 | import androidx.activity.ComponentActivity
9 | import androidx.activity.compose.setContent
10 | import androidx.activity.result.contract.ActivityResultContracts
11 | import androidx.core.content.ContextCompat
12 | import com.google.android.gms.tasks.OnCompleteListener
13 | import com.google.firebase.messaging.FirebaseMessaging
14 | import io.devexpert.android_firebase.ui.navigation.Navigation
15 | import io.devexpert.android_firebase.ui.theme.Android_firebaseTheme
16 |
17 | class MainActivity : ComponentActivity() {
18 |
19 | override fun onCreate(savedInstanceState: Bundle?) {
20 | super.onCreate(savedInstanceState)
21 |
22 | askNotificationPermission()
23 | tokenNew()
24 |
25 | setContent {
26 | Android_firebaseTheme {
27 | Navigation(this)
28 | }
29 | }
30 | }
31 |
32 | private fun askNotificationPermission() {
33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
34 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
35 | PackageManager.PERMISSION_GRANTED) {
36 |
37 | } else {
38 | requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
39 | }
40 | }
41 | }
42 |
43 | private val requestPermissionLauncher = registerForActivityResult(
44 | ActivityResultContracts.RequestPermission(),
45 | ) { isGranted: Boolean ->
46 | if (isGranted) {
47 |
48 | } else {
49 |
50 | }
51 | }
52 |
53 | private fun tokenNew() {
54 | FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
55 | if (!task.isSuccessful) {
56 | Log.w("FCM TOKEN", "Fetching FCM registration token failed", task.exception)
57 | return@OnCompleteListener
58 | }
59 | val token = task.result
60 | Log.d("FCM TOKEN", token.toString())
61 | })
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/utils/RealtimeManager.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.utils
2 |
3 | import android.content.Context
4 | import com.google.firebase.database.DataSnapshot
5 | import com.google.firebase.database.DatabaseError
6 | import com.google.firebase.database.DatabaseReference
7 | import com.google.firebase.database.FirebaseDatabase
8 | import com.google.firebase.database.ValueEventListener
9 | import io.devexpert.android_firebase.model.Contact
10 | import kotlinx.coroutines.channels.awaitClose
11 | import kotlinx.coroutines.flow.Flow
12 | import kotlinx.coroutines.flow.callbackFlow
13 |
14 | class RealtimeManager(context: Context) {
15 | private val databaseReference: DatabaseReference = FirebaseDatabase.getInstance().reference.child("contacts")
16 | private val authManager = AuthManager(context)
17 |
18 | fun addContact(contact: Contact) {
19 | val key = databaseReference.push().key
20 | if (key != null) {
21 | databaseReference.child(key).setValue(contact)
22 | }
23 | }
24 |
25 | fun deleteContact(contactId: String) {
26 | databaseReference.child(contactId).removeValue()
27 | }
28 |
29 | fun updateContact(contactId: String, updatedContact: Contact) {
30 | databaseReference.child(contactId).setValue(updatedContact)
31 | }
32 |
33 | fun getContactsFlow(): Flow> {
34 | val idFilter = authManager.getCurrentUser()?.uid
35 | val flow = callbackFlow {
36 | val listener = databaseReference.addValueEventListener(object : ValueEventListener {
37 | override fun onDataChange(snapshot: DataSnapshot) {
38 | val contacts = snapshot.children.mapNotNull { snapshot ->
39 | val contact = snapshot.getValue(Contact::class.java)
40 | snapshot.key?.let { contact?.copy(key = it) }
41 | }
42 | trySend(contacts.filter { it.uid == idFilter }).isSuccess
43 | }
44 | override fun onCancelled(error: DatabaseError) {
45 | close(error.toException())
46 | }
47 | })
48 | awaitClose { databaseReference.removeEventListener(listener) }
49 | }
50 | return flow
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/MyFirebaseService.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase
2 |
3 | import android.app.NotificationChannel
4 | import android.app.NotificationManager
5 | import android.app.PendingIntent
6 | import android.app.PendingIntent.FLAG_IMMUTABLE
7 | import android.content.Intent
8 | import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
9 | import android.os.Build
10 | import android.util.Log
11 | import androidx.core.app.NotificationCompat
12 | import com.google.firebase.messaging.FirebaseMessagingService
13 | import com.google.firebase.messaging.RemoteMessage
14 | import kotlin.random.Random
15 |
16 | class MyFirebaseService: FirebaseMessagingService() {
17 | private val random = Random
18 |
19 | override fun onMessageReceived(message: RemoteMessage) {
20 | message.notification?.let { message ->
21 | Log.i("FCM Title", "${message.title}")
22 | Log.i("FCM Body", "${message.body}")
23 | sendNotification(message)
24 | }
25 | }
26 |
27 | private fun sendNotification(message: RemoteMessage.Notification) {
28 | val intent = Intent(this, MainActivity::class.java).apply {
29 | addFlags(FLAG_ACTIVITY_CLEAR_TOP)
30 | }
31 | val pendingIntent = PendingIntent.getActivity(
32 | this, 0, intent, FLAG_IMMUTABLE
33 | )
34 | val channelId = this.getString(R.string.default_notification_channel_id)
35 | val notificationBuilder = NotificationCompat.Builder(this, channelId)
36 | .setContentTitle(message.title)
37 | .setContentText(message.body)
38 | .setPriority(NotificationCompat.PRIORITY_HIGH)
39 | .setSmallIcon(R.drawable.ic_google)
40 | .setAutoCancel(true)
41 | .setContentIntent(pendingIntent)
42 |
43 | val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
44 |
45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
46 | val channel = NotificationChannel(channelId, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH)
47 | manager.createNotificationChannel(channel)
48 | }
49 | manager.notify(random.nextInt(), notificationBuilder.build())
50 | }
51 |
52 | override fun onNewToken(token: String) {
53 | Log.d("FCM","New token: $token")
54 | }
55 |
56 | companion object {
57 | const val CHANNEL_NAME = "FCM notification channel"
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/navigation/Navigation.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.navigation
2 |
3 | import android.content.Context
4 | import androidx.compose.runtime.Composable
5 | import androidx.navigation.NavHostController
6 | import androidx.navigation.compose.NavHost
7 | import androidx.navigation.compose.composable
8 | import androidx.navigation.compose.rememberNavController
9 | import com.google.firebase.auth.FirebaseUser
10 | import io.devexpert.android_firebase.ui.screens.HomeScreen
11 | import io.devexpert.android_firebase.ui.screens.Screen
12 | import io.devexpert.android_firebase.ui.screens.auth.ForgotPasswordScreen
13 | import io.devexpert.android_firebase.ui.screens.auth.LoginScreen
14 | import io.devexpert.android_firebase.ui.screens.auth.SignUpScreen
15 | import io.devexpert.android_firebase.utils.AnalyticsManager
16 | import io.devexpert.android_firebase.utils.AuthManager
17 |
18 | @Composable
19 | fun Navigation(context: Context, navController: NavHostController = rememberNavController()) {
20 | var analytics: AnalyticsManager = AnalyticsManager(context)
21 | val authManager: AuthManager = AuthManager(context)
22 |
23 | val user: FirebaseUser? = authManager.getCurrentUser()
24 |
25 | Screen {
26 | NavHost(
27 | navController = navController,
28 | startDestination = if(user == null) Routes.Login.route else Routes.Home.route
29 | ) {
30 | composable(Routes.Login.route) {
31 | LoginScreen(
32 | analytics = analytics,
33 | auth = authManager,
34 | navigation = navController,
35 | )
36 | }
37 | composable(Routes.Home.route) {
38 | HomeScreen(
39 | analytics = analytics,
40 | auth = authManager,
41 | navigation = navController)
42 | }
43 | composable(Routes.SignUp.route) {
44 | SignUpScreen(
45 | analytics = analytics,
46 | auth = authManager,
47 | navigation = navController
48 | )
49 | }
50 | composable(Routes.ForgotPassword.route) {
51 | ForgotPasswordScreen(
52 | analytics = analytics,
53 | auth = authManager,
54 | navigation = navController
55 | )
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun Android_firebaseTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
42 |
45 |
46 |
47 |
50 |
51 |
52 |
53 |
54 |
57 |
60 |
63 |
64 |
65 |
66 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'com.google.gms.google-services'
5 | id 'com.google.firebase.crashlytics'
6 | }
7 |
8 | android {
9 | namespace 'io.devexpert.android_firebase'
10 | compileSdk 33
11 |
12 | defaultConfig {
13 | applicationId "io.devexpert.android_firebase"
14 | minSdk 24
15 | targetSdk 33
16 | versionCode 1
17 | versionName "1.0"
18 |
19 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | minifyEnabled false
28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
29 | }
30 | }
31 | compileOptions {
32 | sourceCompatibility JavaVersion.VERSION_1_8
33 | targetCompatibility JavaVersion.VERSION_1_8
34 | }
35 | kotlinOptions {
36 | jvmTarget = '1.8'
37 | allWarningsAsErrors = false
38 | freeCompilerArgs += [
39 | '-opt-in=androidx.compose.material3.ExperimentalMaterial3Api'
40 | ]
41 | }
42 | buildFeatures {
43 | compose true
44 | }
45 | composeOptions {
46 | kotlinCompilerExtensionVersion '1.3.2'
47 | }
48 | packagingOptions {
49 | resources {
50 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
51 | }
52 | }
53 | }
54 |
55 | dependencies {
56 |
57 | implementation 'androidx.core:core-ktx:1.8.0'
58 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
59 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
60 | implementation 'androidx.activity:activity-compose:1.5.1'
61 | implementation platform('androidx.compose:compose-bom:2022.10.00')
62 | implementation 'androidx.compose.ui:ui'
63 | implementation 'androidx.compose.ui:ui-graphics'
64 | implementation 'androidx.compose.ui:ui-tooling-preview'
65 | implementation 'androidx.compose.material3:material3'
66 |
67 | implementation platform('com.google.firebase:firebase-bom:32.2.0')
68 | implementation 'com.google.firebase:firebase-analytics-ktx'
69 | implementation 'com.google.firebase:firebase-auth-ktx'
70 | implementation 'com.google.firebase:firebase-database-ktx'
71 | implementation 'com.google.firebase:firebase-firestore-ktx'
72 | implementation 'com.google.firebase:firebase-storage-ktx'
73 | implementation 'com.google.firebase:firebase-crashlytics-ktx'
74 | implementation 'com.google.firebase:firebase-config-ktx'
75 | implementation 'com.google.firebase:firebase-messaging-ktx'
76 |
77 | implementation 'com.google.android.gms:play-services-auth:20.6.0'
78 |
79 | implementation 'androidx.navigation:navigation-compose:2.4.0-alpha10'
80 | implementation "io.coil-kt:coil-compose:2.4.0"
81 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/auth/ForgotPasswordScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens.auth
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.foundation.text.KeyboardOptions
14 | import androidx.compose.material3.Button
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.TextField
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.runtime.rememberCoroutineScope
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.text.TextStyle
27 | import androidx.compose.ui.text.input.KeyboardType
28 | import androidx.compose.ui.unit.dp
29 | import androidx.compose.ui.unit.sp
30 | import androidx.navigation.NavController
31 | import io.devexpert.android_firebase.ui.navigation.Routes
32 | import io.devexpert.android_firebase.ui.theme.Purple40
33 | import io.devexpert.android_firebase.utils.AnalyticsManager
34 | import io.devexpert.android_firebase.utils.AuthManager
35 | import io.devexpert.android_firebase.utils.AuthRes
36 | import kotlinx.coroutines.launch
37 |
38 | @Composable
39 | fun ForgotPasswordScreen(analytics: AnalyticsManager, auth: AuthManager, navigation: NavController) {
40 | analytics.logScreenView(screenName = Routes.ForgotPassword.route)
41 |
42 | val context = LocalContext.current
43 | var email by remember { mutableStateOf("") }
44 |
45 | val scope = rememberCoroutineScope()
46 |
47 | Column(
48 | modifier = Modifier.fillMaxSize(),
49 | verticalArrangement = Arrangement.Top,
50 | horizontalAlignment = Alignment.CenterHorizontally
51 | ) {
52 | Text(
53 | text = "Olvidó su contraseña",
54 | style = TextStyle(fontSize = 40.sp, color = Purple40)
55 | )
56 | Spacer(modifier = Modifier.height(50.dp))
57 | TextField(
58 | label = { Text(text = "Correo electrónico") },
59 | value = email,
60 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
61 | onValueChange = { email = it })
62 |
63 | Spacer(modifier = Modifier.height(30.dp))
64 | Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
65 | Button(
66 | onClick = {
67 | scope.launch {
68 | when(val res = auth.resetPassword(email)) {
69 | is AuthRes.Success -> {
70 | analytics.logButtonClicked(buttonName = "Reset password $email")
71 | Toast.makeText(context, "Correo enviado", Toast.LENGTH_SHORT).show()
72 | navigation.navigate(Routes.Login.route)
73 | }
74 | is AuthRes.Error -> {
75 | analytics.logError(error = "Reset password error $email : ${res.errorMessage}")
76 | Toast.makeText(context, "Error al enviar el correo", Toast.LENGTH_SHORT).show()
77 | }
78 | }
79 | }
80 | },
81 | shape = RoundedCornerShape(50.dp),
82 | modifier = Modifier
83 | .fillMaxWidth()
84 | .height(50.dp)
85 | ) {
86 | Text(text = "Recuperar contraseña")
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/utils/AuthManager.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.utils
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.activity.result.ActivityResultLauncher
6 | import com.google.android.gms.auth.api.identity.Identity
7 | import com.google.android.gms.auth.api.signin.GoogleSignIn
8 | import com.google.android.gms.auth.api.signin.GoogleSignInAccount
9 | import com.google.android.gms.auth.api.signin.GoogleSignInClient
10 | import com.google.android.gms.auth.api.signin.GoogleSignInOptions
11 | import com.google.android.gms.common.api.ApiException
12 | import com.google.android.gms.tasks.Task
13 | import com.google.firebase.auth.AuthCredential
14 | import com.google.firebase.auth.FirebaseAuth
15 | import com.google.firebase.auth.FirebaseUser
16 | import com.google.firebase.auth.ktx.auth
17 | import com.google.firebase.ktx.Firebase
18 | import io.devexpert.android_firebase.R
19 | import kotlinx.coroutines.tasks.await
20 |
21 | sealed class AuthRes {
22 | data class Success(val data: T): AuthRes()
23 | data class Error(val errorMessage: String): AuthRes()
24 | }
25 |
26 | class AuthManager(private val context: Context) {
27 | private val auth: FirebaseAuth by lazy { Firebase.auth }
28 |
29 | private val signInClient = Identity.getSignInClient(context)
30 |
31 | suspend fun signInAnonymously(): AuthRes {
32 | return try {
33 | val result = auth.signInAnonymously().await()
34 | AuthRes.Success(result.user ?: throw Exception("Error al iniciar sesión"))
35 | } catch(e: Exception) {
36 | AuthRes.Error(e.message ?: "Error al iniciar sesión")
37 | }
38 | }
39 |
40 |
41 |
42 | suspend fun createUserWithEmailAndPassword(email: String, password: String): AuthRes {
43 | return try {
44 | val authResult = auth.createUserWithEmailAndPassword(email, password).await()
45 | AuthRes.Success(authResult.user)
46 | } catch(e: Exception) {
47 | AuthRes.Error(e.message ?: "Error al crear el usuario")
48 | }
49 | }
50 |
51 | suspend fun signInWithEmailAndPassword(email: String, password: String): AuthRes {
52 | return try {
53 | val authResult = auth.signInWithEmailAndPassword(email, password).await()
54 | AuthRes.Success(authResult.user)
55 | } catch(e: Exception) {
56 | AuthRes.Error(e.message ?: "Error al iniciar sesión")
57 | }
58 | }
59 |
60 | suspend fun resetPassword(email: String): AuthRes {
61 | return try {
62 | auth.sendPasswordResetEmail(email).await()
63 | AuthRes.Success(Unit)
64 | } catch(e: Exception) {
65 | AuthRes.Error(e.message ?: "Error al restablecer la contraseña")
66 | }
67 | }
68 |
69 | fun signOut() {
70 | auth.signOut()
71 | signInClient.signOut()
72 | }
73 |
74 | fun getCurrentUser(): FirebaseUser?{
75 | return auth.currentUser
76 | }
77 |
78 | private val googleSignInClient: GoogleSignInClient by lazy {
79 | val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
80 | .requestIdToken(context.getString(R.string.default_web_client_id))
81 | .requestEmail()
82 | .build()
83 | GoogleSignIn.getClient(context, gso)
84 | }
85 |
86 | fun handleSignInResult(task: Task): AuthRes? {
87 | return try {
88 | val account = task.getResult(ApiException::class.java)
89 | AuthRes.Success(account)
90 | } catch (e: ApiException) {
91 | AuthRes.Error(e.message ?: "Google sign-in failed.")
92 | }
93 | }
94 |
95 | suspend fun signInWithGoogleCredential(credential: AuthCredential): AuthRes? {
96 | return try {
97 | val firebaseUser = auth.signInWithCredential(credential).await()
98 | firebaseUser.user?.let {
99 | AuthRes.Success(it)
100 | } ?: throw Exception("Sign in with Google failed.")
101 | } catch (e: Exception) {
102 | AuthRes.Error(e.message ?: "Sign in with Google failed.")
103 | }
104 | }
105 |
106 | fun signInWithGoogle(googleSignInLauncher: ActivityResultLauncher) {
107 | val signInIntent = googleSignInClient.signInIntent
108 | googleSignInLauncher.launch(signInIntent)
109 | }
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/auth/SignUpScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens.auth
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.foundation.text.ClickableText
15 | import androidx.compose.foundation.text.KeyboardOptions
16 | import androidx.compose.material3.Button
17 | import androidx.compose.material3.Text
18 | import androidx.compose.material3.TextField
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.rememberCoroutineScope
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.platform.LocalContext
28 | import androidx.compose.ui.text.AnnotatedString
29 | import androidx.compose.ui.text.TextStyle
30 | import androidx.compose.ui.text.font.FontFamily
31 | import androidx.compose.ui.text.input.KeyboardType
32 | import androidx.compose.ui.text.input.PasswordVisualTransformation
33 | import androidx.compose.ui.text.style.TextDecoration
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.sp
36 | import androidx.navigation.NavController
37 | import com.google.firebase.analytics.FirebaseAnalytics
38 | import io.devexpert.android_firebase.ui.navigation.Routes
39 | import io.devexpert.android_firebase.ui.theme.Purple40
40 | import io.devexpert.android_firebase.utils.AnalyticsManager
41 | import io.devexpert.android_firebase.utils.AuthManager
42 | import io.devexpert.android_firebase.utils.AuthRes
43 | import kotlinx.coroutines.launch
44 |
45 | @Composable
46 | fun SignUpScreen(analytics: AnalyticsManager, auth: AuthManager, navigation: NavController) {
47 | analytics.logScreenView(screenName = Routes.SignUp.route)
48 |
49 | val context = LocalContext.current
50 | var email by remember { mutableStateOf("") }
51 | var password by remember { mutableStateOf("") }
52 |
53 | val scope = rememberCoroutineScope()
54 |
55 | Column(
56 | modifier = Modifier.fillMaxSize(),
57 | verticalArrangement = Arrangement.Center,
58 | horizontalAlignment = Alignment.CenterHorizontally
59 | ) {
60 | Text(
61 | text = "Crear Cuenta",
62 | style = TextStyle(fontSize = 40.sp, color = Purple40)
63 | )
64 | Spacer(modifier = Modifier.height(50.dp))
65 | TextField(
66 | label = { Text(text = "Correo electrónico") },
67 | value = email,
68 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
69 | onValueChange = { email = it })
70 |
71 | Spacer(modifier = Modifier.height(20.dp))
72 | TextField(
73 | label = { Text(text = "Contraseña") },
74 | value = password,
75 | visualTransformation = PasswordVisualTransformation(),
76 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
77 | onValueChange = { password = it })
78 |
79 | Spacer(modifier = Modifier.height(30.dp))
80 | Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
81 | Button(
82 | onClick = {
83 | scope.launch {
84 | signUp(email, password, auth, analytics, context, navigation)
85 | }
86 | },
87 | shape = RoundedCornerShape(50.dp),
88 | modifier = Modifier
89 | .fillMaxWidth()
90 | .height(50.dp)
91 | ) {
92 | Text(text = "Registrarse")
93 | }
94 | }
95 |
96 | Spacer(modifier = Modifier.height(40.dp))
97 | ClickableText(
98 | text = AnnotatedString("¿Ya tienes cuenta? Inicia sesión"),
99 | onClick = {
100 | navigation.popBackStack()
101 | },
102 | style = TextStyle(
103 | fontSize = 14.sp,
104 | fontFamily = FontFamily.Default,
105 | textDecoration = TextDecoration.Underline,
106 | color = Purple40
107 | )
108 | )
109 | }
110 | }
111 |
112 | private suspend fun signUp(email: String, password: String, auth: AuthManager, analytics: AnalyticsManager, context: Context, navigation: NavController) {
113 | if(email.isNotEmpty() && password.isNotEmpty()) {
114 | when(val result = auth.createUserWithEmailAndPassword(email, password)) {
115 | is AuthRes.Success -> {
116 | analytics.logButtonClicked(FirebaseAnalytics.Event.SIGN_UP)
117 | Toast.makeText(context, "Registro exitoso", Toast.LENGTH_SHORT).show()
118 | navigation.popBackStack()
119 | }
120 | is AuthRes.Error -> {
121 | analytics.logButtonClicked("Error SignUp: ${result.errorMessage}")
122 | Toast.makeText(context, "Error SignUp: ${result.errorMessage}", Toast.LENGTH_SHORT).show()
123 | }
124 | }
125 | } else {
126 | Toast.makeText(context, "Existen campos vacios", Toast.LENGTH_SHORT).show()
127 | }
128 | }
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/storage/CloudStorageScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens.storage
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import android.net.Uri
7 | import android.widget.Toast
8 | import androidx.activity.compose.rememberLauncherForActivityResult
9 | import androidx.activity.result.contract.ActivityResultContracts
10 | import androidx.compose.foundation.Image
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.height
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.lazy.grid.GridCells
18 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
19 | import androidx.compose.material.icons.Icons
20 | import androidx.compose.material.icons.filled.Add
21 | import androidx.compose.material3.FloatingActionButton
22 | import androidx.compose.material3.Icon
23 | import androidx.compose.material3.Scaffold
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.LaunchedEffect
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.mutableStateOf
28 | import androidx.compose.runtime.remember
29 | import androidx.compose.runtime.rememberCoroutineScope
30 | import androidx.compose.runtime.setValue
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.layout.ContentScale
33 | import androidx.compose.ui.platform.LocalContext
34 | import androidx.compose.ui.unit.dp
35 | import androidx.core.content.ContextCompat
36 | import androidx.core.content.FileProvider
37 | import coil.compose.rememberAsyncImagePainter
38 | import coil.request.ImageRequest
39 | import coil.transform.RoundedCornersTransformation
40 | import io.devexpert.android_firebase.utils.CloudStorageManager
41 | import kotlinx.coroutines.launch
42 | import java.io.File
43 | import java.text.SimpleDateFormat
44 | import java.util.Date
45 | import java.util.Objects
46 |
47 | @Composable
48 | fun CloudStorageScreen(storage: CloudStorageManager) {
49 | val scope = rememberCoroutineScope()
50 | val context = LocalContext.current
51 | val file = context.createImageFile()
52 | val uri = FileProvider.getUriForFile(
53 | Objects.requireNonNull(context),
54 | "io.devexpert.android_firebase" + ".provider", file
55 | )
56 | var capturedImageUri by remember { mutableStateOf(Uri.EMPTY) }
57 |
58 | val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
59 | if (it) {
60 | Toast.makeText(context, "Foto tomada", Toast.LENGTH_SHORT).show()
61 | capturedImageUri = uri
62 | capturedImageUri?.let { uri ->
63 | scope.launch {
64 | storage.uploadFile(file.name, uri)
65 | }
66 | }
67 | } else {
68 | Toast.makeText(context, "No se pudo tomar la foto $it", Toast.LENGTH_SHORT).show()
69 | }
70 | }
71 |
72 | val permissionLauncher = rememberLauncherForActivityResult(
73 | ActivityResultContracts.RequestPermission()) {
74 | if (it) {
75 | Toast.makeText(context, "Permiso autorizado", Toast.LENGTH_SHORT).show()
76 | cameraLauncher.launch(uri)
77 | } else {
78 | Toast.makeText(context, "Permiso denegado", Toast.LENGTH_SHORT).show()
79 | }
80 | }
81 |
82 | Scaffold(
83 | floatingActionButton = {
84 | FloatingActionButton(
85 | onClick = {
86 | val permissionCheckResult =
87 | ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
88 | if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
89 | cameraLauncher.launch(uri)
90 | } else {
91 | permissionLauncher.launch(Manifest.permission.CAMERA)
92 | }
93 | },
94 | ) {
95 | Icon(imageVector = Icons.Default.Add, contentDescription = "Add Photo")
96 | }
97 | }
98 | ) { contentPadding ->
99 | Box(modifier = Modifier.padding(contentPadding)) {
100 | Column(
101 | modifier = Modifier
102 | .fillMaxSize()
103 | ) {
104 | var gallery by remember { mutableStateOf>(listOf()) }
105 | LaunchedEffect(Unit) {
106 | gallery = storage.getUserImages()
107 | }
108 | LazyVerticalGrid(
109 | columns = GridCells.Fixed(2),
110 | modifier = Modifier.fillMaxSize()
111 | ) {
112 | items(gallery.size) { index ->
113 | val imageUrl = gallery[index]
114 | CoilImage(
115 | imageUrl = imageUrl,
116 | contentDescription = null,
117 | modifier = Modifier
118 | .fillMaxWidth()
119 | .height(200.dp),
120 | contentScale = ContentScale.Crop
121 | )
122 | }
123 | }
124 | }
125 | }
126 | }
127 | }
128 |
129 | @Composable
130 | fun CoilImage(
131 | imageUrl: String,
132 | contentDescription: String?,
133 | modifier: Modifier = Modifier,
134 | contentScale: ContentScale = ContentScale.Fit
135 | ) {
136 | val painter = rememberAsyncImagePainter(
137 | ImageRequest
138 | .Builder(LocalContext.current)
139 | .data(data = imageUrl)
140 | .apply(block = fun ImageRequest.Builder.() {
141 | crossfade(true)
142 | transformations(RoundedCornersTransformation(topLeft = 20f, topRight = 20f, bottomLeft = 20f, bottomRight = 20f))
143 | })
144 | .build()
145 | )
146 |
147 | Image(
148 | painter = painter,
149 | contentDescription = contentDescription,
150 | modifier = modifier.padding(6.dp),
151 | contentScale = contentScale,
152 | )
153 | }
154 |
155 | fun Context.createImageFile(): File {
156 | val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
157 | val imageFileName = "JPEG_" + timeStamp + "_"
158 | return File.createTempFile(
159 | imageFileName,
160 | ".jpg",
161 | externalCacheDir
162 | )
163 | }
164 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin,gradle,intellij,java,firebase
2 | # Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio,kotlin,gradle,intellij,java,firebase
3 |
4 | ### Android ###
5 | # Gradle files
6 | .gradle/
7 | build/
8 |
9 | # Local configuration file (sdk path, etc)
10 | local.properties
11 |
12 | # Log/OS Files
13 | *.log
14 |
15 | # Android Studio generated files and folders
16 | captures/
17 | .externalNativeBuild/
18 | .cxx/
19 | *.apk
20 | output.json
21 |
22 | # IntelliJ
23 | *.iml
24 | .idea/
25 | misc.xml
26 | deploymentTargetDropDown.xml
27 | render.experimental.xml
28 |
29 | # Keystore files
30 | *.jks
31 | *.keystore
32 |
33 | # Google Services (e.g. APIs or Firebase)
34 | google-services.json
35 |
36 | # Android Profiling
37 | *.hprof
38 |
39 | ### Android Patch ###
40 | gen-external-apklibs
41 |
42 | # Replacement of .externalNativeBuild directories introduced
43 | # with Android Studio 3.5.
44 |
45 | ### Firebase ###
46 | .idea
47 | **/node_modules/*
48 | **/.firebaserc
49 |
50 | ### Firebase Patch ###
51 | .runtimeconfig.json
52 | .firebase/
53 |
54 | ### Intellij ###
55 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
56 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
57 |
58 | # User-specific stuff
59 | .idea/**/workspace.xml
60 | .idea/**/tasks.xml
61 | .idea/**/usage.statistics.xml
62 | .idea/**/dictionaries
63 | .idea/**/shelf
64 |
65 | # AWS User-specific
66 | .idea/**/aws.xml
67 |
68 | # Generated files
69 | .idea/**/contentModel.xml
70 |
71 | # Sensitive or high-churn files
72 | .idea/**/dataSources/
73 | .idea/**/dataSources.ids
74 | .idea/**/dataSources.local.xml
75 | .idea/**/sqlDataSources.xml
76 | .idea/**/dynamic.xml
77 | .idea/**/uiDesigner.xml
78 | .idea/**/dbnavigator.xml
79 |
80 | # Gradle
81 | .idea/**/gradle.xml
82 | .idea/**/libraries
83 |
84 | # Gradle and Maven with auto-import
85 | # When using Gradle or Maven with auto-import, you should exclude module files,
86 | # since they will be recreated, and may cause churn. Uncomment if using
87 | # auto-import.
88 | # .idea/artifacts
89 | # .idea/compiler.xml
90 | # .idea/jarRepositories.xml
91 | # .idea/modules.xml
92 | # .idea/*.iml
93 | # .idea/modules
94 | # *.iml
95 | # *.ipr
96 |
97 | # CMake
98 | cmake-build-*/
99 |
100 | # Mongo Explorer plugin
101 | .idea/**/mongoSettings.xml
102 |
103 | # File-based project format
104 | *.iws
105 |
106 | # IntelliJ
107 | out/
108 |
109 | # mpeltonen/sbt-idea plugin
110 | .idea_modules/
111 |
112 | # JIRA plugin
113 | atlassian-ide-plugin.xml
114 |
115 | # Cursive Clojure plugin
116 | .idea/replstate.xml
117 |
118 | # SonarLint plugin
119 | .idea/sonarlint/
120 |
121 | # Crashlytics plugin (for Android Studio and IntelliJ)
122 | com_crashlytics_export_strings.xml
123 | crashlytics.properties
124 | crashlytics-build.properties
125 | fabric.properties
126 |
127 | # Editor-based Rest Client
128 | .idea/httpRequests
129 |
130 | # Android studio 3.1+ serialized cache file
131 | .idea/caches/build_file_checksums.ser
132 |
133 | ### Intellij Patch ###
134 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
135 |
136 | # *.iml
137 | # modules.xml
138 | # .idea/misc.xml
139 | # *.ipr
140 |
141 | # Sonarlint plugin
142 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
143 | .idea/**/sonarlint/
144 |
145 | # SonarQube Plugin
146 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
147 | .idea/**/sonarIssues.xml
148 |
149 | # Markdown Navigator plugin
150 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
151 | .idea/**/markdown-navigator.xml
152 | .idea/**/markdown-navigator-enh.xml
153 | .idea/**/markdown-navigator/
154 |
155 | # Cache file creation bug
156 | # See https://youtrack.jetbrains.com/issue/JBR-2257
157 | .idea/$CACHE_FILE$
158 |
159 | # CodeStream plugin
160 | # https://plugins.jetbrains.com/plugin/12206-codestream
161 | .idea/codestream.xml
162 |
163 | # Azure Toolkit for IntelliJ plugin
164 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij
165 | .idea/**/azureSettings.xml
166 |
167 | ### Java ###
168 | # Compiled class file
169 | *.class
170 |
171 | # Log file
172 |
173 | # BlueJ files
174 | *.ctxt
175 |
176 | # Mobile Tools for Java (J2ME)
177 | .mtj.tmp/
178 |
179 | # Package Files #
180 | *.jar
181 | *.war
182 | *.nar
183 | *.ear
184 | *.zip
185 | *.tar.gz
186 | *.rar
187 |
188 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
189 | hs_err_pid*
190 | replay_pid*
191 |
192 | ### Kotlin ###
193 | # Compiled class file
194 |
195 | # Log file
196 |
197 | # BlueJ files
198 |
199 | # Mobile Tools for Java (J2ME)
200 |
201 | # Package Files #
202 |
203 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
204 |
205 | ### Gradle ###
206 | .gradle
207 | **/build/
208 | !src/**/build/
209 |
210 | # Ignore Gradle GUI config
211 | gradle-app.setting
212 |
213 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
214 | !gradle-wrapper.jar
215 |
216 | # Avoid ignore Gradle wrappper properties
217 | !gradle-wrapper.properties
218 |
219 | # Cache of project
220 | .gradletasknamecache
221 |
222 | # Eclipse Gradle plugin generated files
223 | # Eclipse Core
224 | .project
225 | # JDT-specific (Eclipse Java Development Tools)
226 | .classpath
227 |
228 | ### Gradle Patch ###
229 | # Java heap dump
230 |
231 | ### AndroidStudio ###
232 | # Covers files to be ignored for android development using Android Studio.
233 |
234 | # Built application files
235 | *.ap_
236 | *.aab
237 |
238 | # Files for the ART/Dalvik VM
239 | *.dex
240 |
241 | # Java class files
242 |
243 | # Generated files
244 | bin/
245 | gen/
246 |
247 | # Gradle files
248 |
249 | # Signing files
250 | .signing/
251 |
252 | # Local configuration file (sdk path, etc)
253 |
254 | # Proguard folder generated by Eclipse
255 | proguard/
256 |
257 | # Log Files
258 |
259 | # Android Studio
260 | /*/build/
261 | /*/local.properties
262 | /*/out
263 | /*/*/build
264 | /*/*/production
265 | .navigation/
266 | *.ipr
267 | *~
268 | *.swp
269 |
270 | # Keystore files
271 |
272 | # Google Services (e.g. APIs or Firebase)
273 | # google-services.json
274 |
275 | # Android Patch
276 |
277 | # External native build folder generated in Android Studio 2.2 and later
278 | .externalNativeBuild
279 |
280 | # NDK
281 | obj/
282 |
283 | # IntelliJ IDEA
284 | /out/
285 |
286 | # User-specific configurations
287 | .idea/caches/
288 | .idea/libraries/
289 | .idea/shelf/
290 | .idea/workspace.xml
291 | .idea/tasks.xml
292 | .idea/.name
293 | .idea/compiler.xml
294 | .idea/copyright/profiles_settings.xml
295 | .idea/encodings.xml
296 | .idea/misc.xml
297 | .idea/modules.xml
298 | .idea/scopes/scope_settings.xml
299 | .idea/dictionaries
300 | .idea/vcs.xml
301 | .idea/jsLibraryMappings.xml
302 | .idea/datasources.xml
303 | .idea/dataSources.ids
304 | .idea/sqlDataSources.xml
305 | .idea/dynamic.xml
306 | .idea/uiDesigner.xml
307 | .idea/assetWizardSettings.xml
308 | .idea/gradle.xml
309 | .idea/jarRepositories.xml
310 | .idea/navEditor.xml
311 |
312 | # Legacy Eclipse project files
313 | .cproject
314 | .settings/
315 |
316 | # Mobile Tools for Java (J2ME)
317 |
318 | # Package Files #
319 |
320 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml)
321 |
322 | ## Plugin-specific files:
323 |
324 | # mpeltonen/sbt-idea plugin
325 |
326 | # JIRA plugin
327 |
328 | # Mongo Explorer plugin
329 | .idea/mongoSettings.xml
330 |
331 | # Crashlytics plugin (for Android Studio and IntelliJ)
332 |
333 | ### AndroidStudio Patch ###
334 |
335 | !/gradle/wrapper/gradle-wrapper.jar
336 |
337 | ### macOS ###
338 | # General
339 | .DS_Store
340 | .AppleDouble
341 | .LSOverride
342 |
343 | # Icon must end with two \r
344 | Icon
345 |
346 |
347 | # Thumbnails
348 | ._*
349 |
350 | # Files that might appear in the root of a volume
351 | .DocumentRevisions-V100
352 | .fseventsd
353 | .Spotlight-V100
354 | .TemporaryItems
355 | .Trashes
356 | .VolumeIcon.icns
357 | .com.apple.timemachine.donotpresent
358 |
359 | # Directories potentially created on remote AFP share
360 | .AppleDB
361 | .AppleDesktop
362 | Network Trash Folder
363 | Temporary Items
364 | .apdisk
365 |
366 | ### macOS Patch ###
367 | # iCloud generated files
368 | *.icloud
369 |
370 |
371 | # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin,gradle,intellij,java,firebase
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/db/NotesScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens.db
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
15 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
16 | import androidx.compose.foundation.text.KeyboardOptions
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.filled.Add
19 | import androidx.compose.material.icons.filled.Delete
20 | import androidx.compose.material.icons.filled.List
21 | import androidx.compose.material3.AlertDialog
22 | import androidx.compose.material3.Button
23 | import androidx.compose.material3.Card
24 | import androidx.compose.material3.FloatingActionButton
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.IconButton
27 | import androidx.compose.material3.Scaffold
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.TextField
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.runtime.collectAsState
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.runtime.mutableStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.runtime.rememberCoroutineScope
36 | import androidx.compose.runtime.setValue
37 | import androidx.compose.ui.Alignment
38 | import androidx.compose.ui.Modifier
39 | import androidx.compose.ui.text.font.FontWeight
40 | import androidx.compose.ui.text.input.KeyboardType
41 | import androidx.compose.ui.text.style.TextAlign
42 | import androidx.compose.ui.unit.dp
43 | import androidx.compose.ui.unit.sp
44 | import io.devexpert.android_firebase.model.Note
45 | import io.devexpert.android_firebase.utils.FirestoreManager
46 | import kotlinx.coroutines.CoroutineScope
47 | import kotlinx.coroutines.Dispatchers
48 | import kotlinx.coroutines.launch
49 |
50 | @OptIn(ExperimentalFoundationApi::class)
51 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
52 | @Composable
53 | fun NotesScreen(firestore: FirestoreManager) {
54 | var showAddNoteDialog by remember { mutableStateOf(false) }
55 |
56 | val notes by firestore.getNotesFlow().collectAsState(emptyList())
57 |
58 | val scope = rememberCoroutineScope()
59 |
60 | Scaffold(
61 | floatingActionButton = {
62 | FloatingActionButton(
63 | onClick = {
64 | showAddNoteDialog = true
65 | },
66 | ) {
67 | Icon(imageVector = Icons.Default.Add, contentDescription = "Add Note")
68 | }
69 | if (showAddNoteDialog) {
70 | AddNoteDialog(
71 | onNoteAdded = { note ->
72 | scope.launch {
73 | firestore.addNote(note)
74 | }
75 | showAddNoteDialog = false
76 | },
77 | onDialogDismissed = { showAddNoteDialog = false },
78 | )
79 | }
80 | }
81 | ) {
82 | if(!notes.isNullOrEmpty()) {
83 | LazyVerticalStaggeredGrid(
84 | columns = StaggeredGridCells.Fixed(2),
85 | contentPadding = PaddingValues(4.dp)
86 | ) {
87 | notes.forEach {
88 | item {
89 | NoteItem(note = it, firestore = firestore)
90 | }
91 | }
92 | }
93 | } else{
94 | Column(
95 | modifier = Modifier
96 | .fillMaxSize()
97 | .padding(16.dp),
98 | verticalArrangement = Arrangement.Center,
99 | horizontalAlignment = Alignment.CenterHorizontally
100 | ) {
101 | Icon(imageVector = Icons.Default.List, contentDescription = null, modifier = Modifier.size(100.dp))
102 | Spacer(modifier = Modifier.height(16.dp))
103 | Text(text = "No se encontraron \nNotas",
104 | fontSize = 18.sp, fontWeight = FontWeight.Thin,
105 | textAlign = TextAlign.Center)
106 | }
107 | }
108 | }
109 | }
110 |
111 | @Composable
112 | fun NoteItem(note: Note, firestore: FirestoreManager) {
113 | var showDeleteNoteDialog by remember { mutableStateOf(false) }
114 |
115 | val onDeleteNoteConfirmed: () -> Unit = {
116 | CoroutineScope(Dispatchers.Default).launch {
117 | firestore.deleteNote(note.id ?: "")
118 | }
119 | }
120 |
121 | if (showDeleteNoteDialog) {
122 | DeleteNoteDialog(
123 | onConfirmDelete = {
124 | onDeleteNoteConfirmed()
125 | showDeleteNoteDialog = false
126 | },
127 | onDismiss = {
128 | showDeleteNoteDialog = false
129 | }
130 | )
131 | }
132 |
133 | Card(
134 | modifier = Modifier.padding(6.dp),
135 | ) {
136 | Column(
137 | modifier = Modifier
138 | .padding(12.dp)
139 | .fillMaxWidth()
140 | ) {
141 | Text(text = note.title,
142 | fontWeight = FontWeight.Bold,
143 | fontSize = 20.sp,)
144 | Spacer(modifier = Modifier.height(8.dp))
145 | Text(text = note.content,
146 | fontWeight = FontWeight.Thin,
147 | fontSize = 13.sp,
148 | lineHeight = 15.sp)
149 | IconButton(
150 | onClick = { showDeleteNoteDialog = true },
151 | ) {
152 | Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete Icon")
153 | }
154 | }
155 | }
156 | }
157 |
158 |
159 | @Composable
160 | fun AddNoteDialog(onNoteAdded: (Note) -> Unit, onDialogDismissed: () -> Unit) {
161 | var title by remember { mutableStateOf("") }
162 | var content by remember { mutableStateOf("") }
163 |
164 | AlertDialog(
165 | onDismissRequest = {},
166 | title = { Text(text = "Agregar Nota") },
167 | confirmButton = {
168 | Button(
169 | onClick = {
170 | val newNote = Note(
171 | title = title,
172 | content = content)
173 | onNoteAdded(newNote)
174 | title = ""
175 | content = ""
176 | }
177 | ) {
178 | Text(text = "Agregar")
179 | }
180 | },
181 | dismissButton = {
182 | Button(
183 | onClick = {
184 | onDialogDismissed()
185 | }
186 | ) {
187 | Text(text = "Cancelar")
188 | }
189 | },
190 | text = {
191 | Column {
192 | TextField(
193 | value = title,
194 | onValueChange = { title = it },
195 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
196 | label = { Text(text = "Título") }
197 | )
198 | Spacer(modifier = Modifier.height(8.dp))
199 | TextField(
200 | modifier = Modifier
201 | .fillMaxWidth()
202 | .height(100.dp),
203 | value = content,
204 | onValueChange = { content = it },
205 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
206 | maxLines = 4,
207 | label = { Text(text = "Contenido") }
208 | )
209 | }
210 | }
211 | )
212 | }
213 |
214 | @Composable
215 | fun DeleteNoteDialog(onConfirmDelete: () -> Unit, onDismiss: () -> Unit) {
216 | AlertDialog(
217 | onDismissRequest = onDismiss,
218 | title = { Text("Eliminar Nota") },
219 | text = { Text("¿Estás seguro que deseas eliminar la nota?") },
220 | confirmButton = {
221 | Button(
222 | onClick = onConfirmDelete
223 | ) {
224 | Text("Aceptar")
225 | }
226 | },
227 | dismissButton = {
228 | Button(
229 | onClick = onDismiss
230 | ) {
231 | Text("Cancelar")
232 | }
233 | }
234 | )
235 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/db/ContactsScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens.db
2 |
3 | import android.annotation.SuppressLint
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.lazy.LazyColumn
16 | import androidx.compose.foundation.text.KeyboardOptions
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.filled.Add
19 | import androidx.compose.material.icons.filled.Delete
20 | import androidx.compose.material.icons.filled.Person
21 | import androidx.compose.material3.AlertDialog
22 | import androidx.compose.material3.Button
23 | import androidx.compose.material3.Card
24 | import androidx.compose.material3.FloatingActionButton
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.IconButton
27 | import androidx.compose.material3.MaterialTheme
28 | import androidx.compose.material3.Scaffold
29 | import androidx.compose.material3.Text
30 | import androidx.compose.material3.TextField
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.runtime.collectAsState
33 | import androidx.compose.runtime.getValue
34 | import androidx.compose.runtime.mutableStateOf
35 | import androidx.compose.runtime.remember
36 | import androidx.compose.runtime.setValue
37 | import androidx.compose.ui.Alignment
38 | import androidx.compose.ui.Modifier
39 | import androidx.compose.ui.graphics.Color
40 | import androidx.compose.ui.text.font.FontWeight
41 | import androidx.compose.ui.text.input.KeyboardType
42 | import androidx.compose.ui.text.style.TextAlign
43 | import androidx.compose.ui.text.style.TextOverflow
44 | import androidx.compose.ui.unit.dp
45 | import androidx.compose.ui.unit.sp
46 | import io.devexpert.android_firebase.model.Contact
47 | import io.devexpert.android_firebase.utils.AuthManager
48 | import io.devexpert.android_firebase.utils.RealtimeManager
49 |
50 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
51 | @Composable
52 | fun ContactsScreen(realtime: RealtimeManager, authManager: AuthManager) {
53 | var showAddContactDialog by remember { mutableStateOf(false) }
54 |
55 | val contacts by realtime.getContactsFlow().collectAsState(emptyList())
56 |
57 | Scaffold(
58 | floatingActionButton = {
59 | FloatingActionButton(
60 | onClick = {
61 | showAddContactDialog = true
62 | },
63 | ) {
64 | Icon(imageVector = Icons.Default.Add, contentDescription = "Add Contact")
65 | }
66 |
67 | if (showAddContactDialog) {
68 | AddContactDialog(
69 | onContactAdded = { contact ->
70 | realtime.addContact(contact)
71 | showAddContactDialog = false
72 | },
73 | onDialogDismissed = { showAddContactDialog = false },
74 | authManager = authManager,
75 | )
76 | }
77 | }
78 | ) { _ ->
79 | if(!contacts.isNullOrEmpty()) {
80 | LazyColumn {
81 | contacts.forEach { contact ->
82 | item {
83 | ContactItem(contact = contact, realtime = realtime)
84 | }
85 | }
86 | }
87 | } else {
88 | Column(
89 | modifier = Modifier
90 | .fillMaxSize()
91 | .padding(16.dp),
92 | verticalArrangement = Arrangement.Center,
93 | horizontalAlignment = Alignment.CenterHorizontally
94 | ) {
95 | Icon(imageVector = Icons.Default.Person, contentDescription = null, modifier = Modifier.size(100.dp))
96 | Spacer(modifier = Modifier.height(16.dp))
97 | Text(text = "No se encontraron \nContactos",
98 | fontSize = 18.sp, fontWeight = FontWeight.Thin, textAlign = TextAlign.Center)
99 | }
100 | }
101 | }
102 | }
103 |
104 | @Composable
105 | fun ContactItem(contact: Contact, realtime: RealtimeManager) {
106 | var showDeleteContactDialog by remember { mutableStateOf(false) }
107 |
108 | val onDeleteContactConfirmed: () -> Unit = {
109 | realtime.deleteContact(contact.key ?: "")
110 | }
111 |
112 | if (showDeleteContactDialog) {
113 | DeleteContactDialog(
114 | onConfirmDelete = {
115 | onDeleteContactConfirmed()
116 | showDeleteContactDialog = false
117 | },
118 | onDismiss = {
119 | showDeleteContactDialog = false
120 | }
121 | )
122 | }
123 |
124 | Card(
125 | modifier = Modifier
126 | .padding(start = 15.dp, end = 15.dp, top = 15.dp, bottom = 0.dp)
127 | .fillMaxWidth())
128 | {
129 | Row(
130 | modifier = Modifier
131 | .fillMaxSize()
132 | .padding(16.dp),
133 | horizontalArrangement = Arrangement.SpaceBetween,
134 | verticalAlignment = Alignment.CenterVertically,
135 | ) {
136 | Column(modifier = Modifier.weight(3f)) {
137 | Text(
138 | text = contact.name,
139 | fontWeight = FontWeight.Bold,
140 | fontSize = 20.sp,
141 | maxLines = 1,
142 | overflow = TextOverflow.Ellipsis)
143 | Spacer(modifier = Modifier.height(4.dp))
144 | Text(
145 | text = contact.phoneNumber,
146 | fontWeight = FontWeight.Medium,
147 | fontSize = 15.sp,
148 | maxLines = 1,
149 | overflow = TextOverflow.Ellipsis)
150 | Spacer(modifier = Modifier.height(4.dp))
151 | Text(
152 | text = contact.email,
153 | fontWeight = FontWeight.Thin,
154 | fontSize = 12.sp,
155 | maxLines = 1,
156 | overflow = TextOverflow.Ellipsis)
157 | }
158 | Row(
159 | modifier = Modifier.weight(1f),
160 | horizontalArrangement = Arrangement.Center,
161 | ) {
162 | IconButton(
163 | onClick = {
164 | showDeleteContactDialog = true
165 | },
166 | ) {
167 | Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete Icon")
168 | }
169 | }
170 | }
171 | }
172 | }
173 |
174 | @Composable
175 | fun AddContactDialog(onContactAdded: (Contact) -> Unit, onDialogDismissed: () -> Unit, authManager: AuthManager) {
176 | var name by remember { mutableStateOf("") }
177 | var phoneNumber by remember { mutableStateOf("") }
178 | var email by remember { mutableStateOf("") }
179 | var uid = authManager.getCurrentUser()?.uid
180 |
181 | AlertDialog(
182 | onDismissRequest = {},
183 | title = { Text(text = "Agregar Contacto") },
184 | confirmButton = {
185 | Button(
186 | onClick = {
187 | val newContact = Contact(
188 | name = name,
189 | phoneNumber = phoneNumber,
190 | email = email,
191 | uid = uid.toString())
192 | onContactAdded(newContact)
193 | name = ""
194 | phoneNumber = ""
195 | email = ""
196 | }
197 | ) {
198 | Text(text = "Agregar")
199 | }
200 | },
201 | dismissButton = {
202 | Button(
203 | onClick = {
204 | onDialogDismissed()
205 | }
206 | ) {
207 | Text(text = "Cancelar")
208 | }
209 | },
210 | text = {
211 | Column {
212 | TextField(
213 | value = name,
214 | onValueChange = { name = it },
215 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Text),
216 | label = { Text(text = "Nombre") }
217 | )
218 | Spacer(modifier = Modifier.height(8.dp))
219 | TextField(
220 | value = phoneNumber,
221 | onValueChange = { phoneNumber = it },
222 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Phone),
223 | label = { Text(text = "Teléfono") }
224 | )
225 | Spacer(modifier = Modifier.height(8.dp))
226 | TextField(
227 | value = email,
228 | onValueChange = { email = it },
229 | keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Email),
230 | label = { Text(text = "Correo electrónico") }
231 | )
232 | }
233 | }
234 | )
235 | }
236 |
237 | @Composable
238 | fun DeleteContactDialog(onConfirmDelete: () -> Unit, onDismiss: () -> Unit) {
239 | AlertDialog(
240 | onDismissRequest = onDismiss,
241 | title = { Text("Eliminar contacto") },
242 | text = { Text("¿Estás seguro que deseas eliminar el contacto?") },
243 | confirmButton = {
244 | Button(
245 | onClick = onConfirmDelete
246 | ) {
247 | Text("Aceptar")
248 | }
249 | },
250 | dismissButton = {
251 | Button(
252 | onClick = onDismiss
253 | ) {
254 | Text("Cancelar")
255 | }
256 | }
257 | )
258 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/auth/LoginScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens.auth
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import androidx.activity.compose.rememberLauncherForActivityResult
6 | import androidx.activity.result.contract.ActivityResultContracts
7 | import androidx.compose.foundation.BorderStroke
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.layout.Arrangement
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.Row
14 | import androidx.compose.foundation.layout.Spacer
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.foundation.layout.fillMaxWidth
17 | import androidx.compose.foundation.layout.height
18 | import androidx.compose.foundation.layout.padding
19 | import androidx.compose.foundation.layout.size
20 | import androidx.compose.foundation.layout.width
21 | import androidx.compose.foundation.shape.RoundedCornerShape
22 | import androidx.compose.foundation.text.ClickableText
23 | import androidx.compose.foundation.text.KeyboardOptions
24 | import androidx.compose.material3.Button
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.Surface
27 | import androidx.compose.material3.Text
28 | import androidx.compose.material3.TextField
29 | import androidx.compose.runtime.Composable
30 | import androidx.compose.runtime.getValue
31 | import androidx.compose.runtime.mutableStateOf
32 | import androidx.compose.runtime.remember
33 | import androidx.compose.runtime.rememberCoroutineScope
34 | import androidx.compose.runtime.setValue
35 | import androidx.compose.ui.Alignment
36 | import androidx.compose.ui.Modifier
37 | import androidx.compose.ui.graphics.Color
38 | import androidx.compose.ui.platform.LocalContext
39 | import androidx.compose.ui.res.painterResource
40 | import androidx.compose.ui.text.AnnotatedString
41 | import androidx.compose.ui.text.TextStyle
42 | import androidx.compose.ui.text.font.FontFamily
43 | import androidx.compose.ui.text.input.KeyboardType
44 | import androidx.compose.ui.text.input.PasswordVisualTransformation
45 | import androidx.compose.ui.text.style.TextDecoration
46 | import androidx.compose.ui.unit.dp
47 | import androidx.compose.ui.unit.sp
48 | import androidx.navigation.NavController
49 | import com.google.android.gms.auth.api.signin.GoogleSignIn
50 | import com.google.firebase.analytics.FirebaseAnalytics
51 | import com.google.firebase.auth.GoogleAuthProvider
52 | import com.google.firebase.crashlytics.FirebaseCrashlytics
53 | import io.devexpert.android_firebase.R
54 | import io.devexpert.android_firebase.ui.navigation.Routes
55 | import io.devexpert.android_firebase.ui.theme.Purple40
56 | import io.devexpert.android_firebase.utils.AnalyticsManager
57 | import io.devexpert.android_firebase.utils.AuthManager
58 | import io.devexpert.android_firebase.utils.AuthRes
59 | import kotlinx.coroutines.launch
60 | import java.lang.RuntimeException
61 |
62 | @Composable
63 | fun LoginScreen(analytics: AnalyticsManager, auth: AuthManager, navigation: NavController) {
64 | analytics.logScreenView(screenName = Routes.Login.route)
65 |
66 | var email by remember { mutableStateOf("") }
67 | var password by remember { mutableStateOf("") }
68 | val context = LocalContext.current
69 | val scope = rememberCoroutineScope()
70 |
71 | val googleSignInLauncher = rememberLauncherForActivityResult(
72 | contract = ActivityResultContracts.StartActivityForResult()) { result ->
73 | when(val account = auth.handleSignInResult(GoogleSignIn.getSignedInAccountFromIntent(result.data))) {
74 | is AuthRes.Success -> {
75 | val credential = GoogleAuthProvider.getCredential(account?.data?.idToken, null)
76 | scope.launch {
77 | val fireUser = auth.signInWithGoogleCredential(credential)
78 | if (fireUser != null){
79 | Toast.makeText(context, "Bienvenidx", Toast.LENGTH_SHORT).show()
80 | navigation.navigate(Routes.Home.route){
81 | popUpTo(Routes.Login.route){
82 | inclusive = true
83 | }
84 | }
85 | }
86 | }
87 | }
88 | is AuthRes.Error -> {
89 | analytics.logError("Error SignIn: ${account.errorMessage}")
90 | Toast.makeText(context, "Error: ${account.errorMessage}", Toast.LENGTH_SHORT).show()
91 | }
92 | else -> {
93 | Toast.makeText(context, "Error desconocido", Toast.LENGTH_SHORT).show()
94 | }
95 | }
96 | }
97 |
98 | Box(modifier = Modifier.fillMaxSize()) {
99 | ClickableText(
100 | text = AnnotatedString("¿No tienes una cuenta? Regístrate"),
101 | modifier = Modifier
102 | .align(Alignment.BottomCenter)
103 | .padding(40.dp),
104 | onClick = {
105 | navigation.navigate(Routes.SignUp.route)
106 | analytics.logButtonClicked("Click: No tienes una cuenta? Regístrate")
107 | },
108 | style = TextStyle(
109 | fontSize = 14.sp,
110 | fontFamily = FontFamily.Default,
111 | textDecoration = TextDecoration.Underline,
112 | color = Purple40
113 | )
114 | )
115 | }
116 | Column(
117 | modifier = Modifier.fillMaxSize(),
118 | verticalArrangement = Arrangement.Center,
119 | horizontalAlignment = Alignment.CenterHorizontally
120 | ) {
121 | Image(
122 | painter = painterResource(id = R.drawable.ic_firebase),
123 | contentDescription = "Firebase",
124 | modifier = Modifier.size(100.dp)
125 | )
126 | Spacer(modifier = Modifier.height(10.dp))
127 | Text(
128 | text = "Firebase Android",
129 | style = TextStyle(fontSize = 30.sp))
130 | Spacer(modifier = Modifier.height(30.dp))
131 | TextField(
132 | label = { Text(text = "Correo electrónico") },
133 | value = email,
134 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
135 | onValueChange = { email = it })
136 | Spacer(modifier = Modifier.height(10.dp))
137 | TextField(
138 | label = { Text(text = "Contraseña") },
139 | value = password,
140 | visualTransformation = PasswordVisualTransformation(),
141 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
142 | onValueChange = { password = it })
143 | Spacer(modifier = Modifier.height(20.dp))
144 | Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
145 | Button(
146 | onClick = {
147 | scope.launch {
148 | emailPassSignIn(email, password, auth, analytics, context, navigation)
149 | }
150 | },
151 | shape = RoundedCornerShape(50.dp),
152 | modifier = Modifier
153 | .fillMaxWidth()
154 | .height(50.dp)
155 | ) {
156 | Text(text = "Iniciar Sesión".uppercase())
157 | }
158 | }
159 | Spacer(modifier = Modifier.height(20.dp))
160 | ClickableText(
161 | text = AnnotatedString("¿Olvidaste tu contraseña?"),
162 | onClick = {
163 | navigation.navigate(Routes.ForgotPassword.route) {
164 | popUpTo(Routes.Login.route) { inclusive = true }
165 | }
166 | analytics.logButtonClicked("Click: ¿Olvidaste tu contraseña?")
167 | },
168 | style = TextStyle(
169 | fontSize = 14.sp,
170 | fontFamily = FontFamily.Default,
171 | textDecoration = TextDecoration.Underline,
172 | color = Purple40
173 | )
174 | )
175 | Spacer(modifier = Modifier.height(25.dp))
176 | Text(text = "-------- o --------", style = TextStyle(color = Color.Gray))
177 | Spacer(modifier = Modifier.height(25.dp))
178 | SocialMediaButton(
179 | onClick = {
180 | scope.launch{
181 | incognitoSignIn(auth, analytics, context, navigation)
182 | }
183 | },
184 | text = "Continuar como invitado",
185 | icon = R.drawable.ic_incognito,
186 | color = Color(0xFF363636)
187 | )
188 | Spacer(modifier = Modifier.height(15.dp))
189 | SocialMediaButton(
190 | onClick = {
191 | auth.signInWithGoogle(googleSignInLauncher)
192 | },
193 | text = "Continuar con Google",
194 | icon = R.drawable.ic_google,
195 | color = Color(0xFFF1F1F1)
196 | )
197 | Spacer(modifier = Modifier.height(25.dp))
198 | ClickableText(
199 | text = AnnotatedString("Forzar cierre Crashlytics"),
200 | onClick = {
201 | val crashlytics = FirebaseCrashlytics.getInstance()
202 | crashlytics.setCustomKey("pruebaClave", "valor de la prueba clave")
203 | crashlytics.log("Mensaje personalizado desde un log")
204 |
205 | throw RuntimeException("Error forzado desde LoginScreen")
206 | },
207 | style = TextStyle(
208 | fontSize = 14.sp,
209 | fontFamily = FontFamily.Default,
210 | textDecoration = TextDecoration.Underline,
211 | color = Purple40
212 | )
213 | )
214 | }
215 | }
216 |
217 | private suspend fun incognitoSignIn(auth: AuthManager, analytics: AnalyticsManager, context: Context, navigation: NavController) {
218 | when(val result = auth.signInAnonymously()) {
219 | is AuthRes.Success -> {
220 | analytics.logButtonClicked("Click: Continuar como invitado")
221 | navigation.navigate(Routes.Home.route) {
222 | popUpTo(Routes.Login.route) {
223 | inclusive = true
224 | }
225 | }
226 | }
227 | is AuthRes.Error -> {
228 | analytics.logError("Error SignIn Incognito: ${result.errorMessage}")
229 | }
230 | }
231 | }
232 |
233 | private suspend fun emailPassSignIn(email: String, password: String, auth: AuthManager, analytics: AnalyticsManager, context: Context, navigation: NavController) {
234 | if(email.isNotEmpty() && password.isNotEmpty()) {
235 | when (val result = auth.signInWithEmailAndPassword(email, password)) {
236 | is AuthRes.Success -> {
237 | analytics.logButtonClicked("Click: Iniciar sesión correo & contraseña")
238 | navigation.navigate(Routes.Home.route) {
239 | popUpTo(Routes.Login.route) {
240 | inclusive = true
241 | }
242 | }
243 | }
244 |
245 | is AuthRes.Error -> {
246 | analytics.logButtonClicked("Error SignUp: ${result.errorMessage}")
247 | Toast.makeText(context, "Error SignUp: ${result.errorMessage}", Toast.LENGTH_SHORT).show()
248 | }
249 | }
250 | } else {
251 | Toast.makeText(context, "Existen campos vacios", Toast.LENGTH_SHORT).show()
252 | }
253 | }
254 |
255 | @Composable
256 | fun SocialMediaButton(onClick: () -> Unit, text: String, icon: Int, color: Color, ) {
257 | var click by remember { mutableStateOf(false) }
258 | Surface(
259 | onClick = onClick,
260 | modifier = Modifier.padding(start = 40.dp, end = 40.dp).clickable { click = !click },
261 | shape = RoundedCornerShape(50),
262 | border = BorderStroke(width = 1.dp, color = if(icon == R.drawable.ic_incognito) color else Color.Gray),
263 | color = color
264 | ) {
265 | Row(
266 | modifier = Modifier.padding(start = 12.dp, end = 16.dp, top = 12.dp, bottom = 12.dp).fillMaxWidth(),
267 | verticalAlignment = Alignment.CenterVertically,
268 | horizontalArrangement = Arrangement.Center
269 | ) {
270 | Icon(
271 | painter = painterResource(id = icon),
272 | modifier = Modifier.size(24.dp),
273 | contentDescription = text,
274 | tint = Color.Unspecified
275 | )
276 | Spacer(modifier = Modifier.width(8.dp))
277 | Text(text = "$text", color = if(icon == R.drawable.ic_incognito) Color.White else Color.Black)
278 | click = true
279 | }
280 | }
281 | }
--------------------------------------------------------------------------------
/app/src/main/java/io/devexpert/android_firebase/ui/screens/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package io.devexpert.android_firebase.ui.screens
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.RowScope
11 | import androidx.compose.foundation.layout.Spacer
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.layout.width
15 | import androidx.compose.foundation.shape.CircleShape
16 | import androidx.compose.material.icons.Icons
17 | import androidx.compose.material.icons.filled.AccountBox
18 | import androidx.compose.material.icons.filled.Face
19 | import androidx.compose.material.icons.filled.List
20 | import androidx.compose.material.icons.filled.Person
21 | import androidx.compose.material.icons.filled.Warning
22 | import androidx.compose.material.icons.outlined.ExitToApp
23 | import androidx.compose.material3.AlertDialog
24 | import androidx.compose.material3.Button
25 | import androidx.compose.material3.Icon
26 | import androidx.compose.material3.IconButton
27 | import androidx.compose.material3.NavigationBar
28 | import androidx.compose.material3.NavigationBarItem
29 | import androidx.compose.material3.Scaffold
30 | import androidx.compose.material3.Text
31 | import androidx.compose.material3.TopAppBar
32 | import androidx.compose.material3.TopAppBarDefaults
33 | import androidx.compose.runtime.Composable
34 | import androidx.compose.runtime.getValue
35 | import androidx.compose.runtime.key
36 | import androidx.compose.runtime.mutableStateOf
37 | import androidx.compose.runtime.remember
38 | import androidx.compose.runtime.setValue
39 | import androidx.compose.ui.Alignment
40 | import androidx.compose.ui.Modifier
41 | import androidx.compose.ui.draw.alpha
42 | import androidx.compose.ui.draw.clip
43 | import androidx.compose.ui.graphics.vector.ImageVector
44 | import androidx.compose.ui.layout.ContentScale
45 | import androidx.compose.ui.platform.LocalContext
46 | import androidx.compose.ui.res.painterResource
47 | import androidx.compose.ui.text.style.TextOverflow
48 | import androidx.compose.ui.unit.dp
49 | import androidx.compose.ui.unit.sp
50 | import androidx.navigation.NavController
51 | import androidx.navigation.NavDestination
52 | import androidx.navigation.NavDestination.Companion.hierarchy
53 | import androidx.navigation.NavHostController
54 | import androidx.navigation.compose.NavHost
55 | import androidx.navigation.compose.composable
56 | import androidx.navigation.compose.currentBackStackEntryAsState
57 | import androidx.navigation.compose.rememberNavController
58 | import coil.compose.AsyncImage
59 | import coil.request.ImageRequest
60 | import com.google.firebase.crashlytics.FirebaseCrashlytics
61 | import com.google.firebase.crashlytics.ktx.setCustomKeys
62 | import com.google.firebase.ktx.Firebase
63 | import com.google.firebase.remoteconfig.ConfigUpdate
64 | import com.google.firebase.remoteconfig.ConfigUpdateListener
65 | import com.google.firebase.remoteconfig.FirebaseRemoteConfig
66 | import com.google.firebase.remoteconfig.FirebaseRemoteConfigException
67 | import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
68 | import com.google.firebase.remoteconfig.ktx.get
69 | import com.google.firebase.remoteconfig.ktx.remoteConfig
70 | import io.devexpert.android_firebase.R
71 | import io.devexpert.android_firebase.ui.navigation.Routes
72 | import io.devexpert.android_firebase.ui.screens.db.ContactsScreen
73 | import io.devexpert.android_firebase.ui.screens.db.NotesScreen
74 | import io.devexpert.android_firebase.ui.screens.storage.CloudStorageScreen
75 | import io.devexpert.android_firebase.utils.AnalyticsManager
76 | import io.devexpert.android_firebase.utils.AuthManager
77 | import io.devexpert.android_firebase.utils.CloudStorageManager
78 | import io.devexpert.android_firebase.utils.FirestoreManager
79 | import io.devexpert.android_firebase.utils.RealtimeManager
80 | import java.lang.RuntimeException
81 |
82 | private lateinit var mFirebaseRemoteConfig: FirebaseRemoteConfig
83 | private var welcomeMessage by mutableStateOf("Bienvenidx")
84 | private var isButtonVisible by mutableStateOf(true)
85 |
86 | val WELCOME_MESSAGE_KEY = "welcome_message"
87 | val IS_BUTTON_VISIBLE_KEY = "is_button_visible"
88 |
89 | @Composable
90 | fun HomeScreen(analytics: AnalyticsManager, auth: AuthManager, navigation: NavController) {
91 | analytics.logScreenView(screenName = Routes.Home.route)
92 | val navController = rememberNavController()
93 |
94 | initRemoteConfig()
95 |
96 | val user = auth.getCurrentUser()
97 |
98 | var showDialog by remember { mutableStateOf(false) }
99 |
100 | val context = LocalContext.current
101 |
102 | val onLogoutConfirmed: () -> Unit = {
103 | auth.signOut()
104 | navigation.navigate(Routes.Login.route) {
105 | popUpTo(Routes.Home.route) {
106 | inclusive = true
107 | }
108 | }
109 | }
110 |
111 | Scaffold (
112 | topBar = {
113 | TopAppBar(
114 | title = {
115 | Row(
116 | horizontalArrangement = Arrangement.Start,
117 | verticalAlignment = Alignment.CenterVertically
118 | ) {
119 | if(user?.photoUrl != null) {
120 | AsyncImage(
121 | model = ImageRequest.Builder(LocalContext.current)
122 | .data(user?.photoUrl)
123 | .crossfade(true)
124 | .build(),
125 | contentDescription = "Imagen",
126 | placeholder = painterResource(id = R.drawable.profile),
127 | contentScale = ContentScale.Crop,
128 | modifier = Modifier
129 | .clip(CircleShape)
130 | .size(40.dp))
131 | } else {
132 | Image(
133 | painter = painterResource(R.drawable.profile),
134 | contentDescription = "Foto de perfil por defecto",
135 | modifier = Modifier
136 | .padding(end = 8.dp)
137 | .size(40.dp)
138 | .clip(CircleShape)
139 | )
140 | }
141 | Spacer(modifier = Modifier.width(10.dp))
142 | Column {
143 | Text(
144 | text = if(!user?.displayName.isNullOrEmpty()) "Hola ${user?.displayName}" else welcomeMessage,
145 | fontSize = 20.sp,
146 | maxLines = 1,
147 | overflow = TextOverflow.Ellipsis
148 | )
149 | Text(
150 | text = if(!user?.email.isNullOrEmpty()) "${user?.email}" else "Anónimo",
151 | fontSize = 12.sp,
152 | maxLines = 1,
153 | overflow = TextOverflow.Ellipsis)
154 | }
155 | }
156 | },
157 | colors = TopAppBarDefaults.smallTopAppBarColors(),
158 | actions = {
159 | IconButton(
160 | onClick = {
161 | val crashlytics = FirebaseCrashlytics.getInstance()
162 | crashlytics.setCustomKey("pruebaClaveHome", "Valor a enviar")
163 | crashlytics.log("Mensaje log desde HomeScreen")
164 | crashlytics.setUserId(user?.uid ?: "No Id Found")
165 | crashlytics.setCustomKeys {
166 | key("str", "hello")
167 | key("bool", true)
168 | key("int", 5)
169 | key("long", 5.8)
170 | key("float", 1.0f)
171 | key("double", 1.0)
172 | }
173 | throw RuntimeException("Error forzado desde HomeScreen")
174 | },
175 | modifier = Modifier.alpha(if (isButtonVisible) 1f else 0f)
176 | ) {
177 | Icon(Icons.Default.Warning , contentDescription = "Forzar Error")
178 | }
179 | IconButton(
180 | onClick = {
181 | showDialog = true
182 | }
183 | ) {
184 | Icon(Icons.Outlined.ExitToApp, contentDescription = "Cerrar sesión")
185 | }
186 | }
187 | )
188 | },
189 | bottomBar = {
190 | BottomBar(navController = navController)
191 | }
192 | ){ contentPadding ->
193 | Box(modifier = Modifier.padding(contentPadding)) {
194 | if (showDialog) {
195 | LogoutDialog(onConfirmLogout = {
196 | onLogoutConfirmed()
197 | showDialog = false
198 | }, onDismiss = { showDialog = false })
199 | }
200 | BottomNavGraph(navController = navController, context = context, authManager = auth)
201 | }
202 | }
203 | }
204 |
205 | fun initRemoteConfig() {
206 | mFirebaseRemoteConfig = Firebase.remoteConfig
207 | val configSettings: FirebaseRemoteConfigSettings = FirebaseRemoteConfigSettings.Builder()
208 | .setMinimumFetchIntervalInSeconds(3600)
209 | .build()
210 | mFirebaseRemoteConfig.setConfigSettingsAsync(configSettings)
211 | mFirebaseRemoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)
212 |
213 | mFirebaseRemoteConfig.addOnConfigUpdateListener(object: ConfigUpdateListener {
214 | override fun onUpdate(configUpdate: ConfigUpdate) {
215 | Log.d("HomeScreen", "Updated keys: " + configUpdate.updatedKeys)
216 | if(configUpdate.updatedKeys.contains(IS_BUTTON_VISIBLE_KEY) || configUpdate.updatedKeys.contains(WELCOME_MESSAGE_KEY)) {
217 | mFirebaseRemoteConfig.activate().addOnCompleteListener {
218 | displayWelcomeMessage()
219 | }
220 | }
221 | }
222 | override fun onError(error: FirebaseRemoteConfigException) {
223 | }
224 | })
225 |
226 | fetchWelcome()
227 | }
228 |
229 | fun fetchWelcome() {
230 | mFirebaseRemoteConfig.fetchAndActivate()
231 | .addOnCompleteListener { task ->
232 | if (task.isSuccessful) {
233 | val updated = task.result
234 | println("Parámetros actualizados: $updated")
235 | } else {
236 | println("Fetch failed")
237 | }
238 | }
239 | }
240 |
241 | fun displayWelcomeMessage() {
242 | welcomeMessage = mFirebaseRemoteConfig[WELCOME_MESSAGE_KEY].asString()
243 | isButtonVisible = mFirebaseRemoteConfig[IS_BUTTON_VISIBLE_KEY].asBoolean()
244 | }
245 |
246 | @Composable
247 | fun LogoutDialog(onConfirmLogout: () -> Unit, onDismiss: () -> Unit) {
248 | AlertDialog(
249 | onDismissRequest = onDismiss,
250 | title = { Text("Cerrar sesión") },
251 | text = { Text("¿Estás seguro que deseas cerrar sesión?") },
252 | confirmButton = {
253 | Button(
254 | onClick = onConfirmLogout
255 | ) {
256 | Text("Aceptar")
257 | }
258 | },
259 | dismissButton = {
260 | Button(
261 | onClick = onDismiss
262 | ) {
263 | Text("Cancelar")
264 | }
265 | }
266 | )
267 | }
268 |
269 | @Composable
270 | fun BottomBar(navController: NavHostController) {
271 | val screens = listOf(
272 | BottomNavScreen.Contact,
273 | BottomNavScreen.Note,
274 | BottomNavScreen.Photos
275 | )
276 | val navBackStackEntry by navController.currentBackStackEntryAsState()
277 | val currentDestination = navBackStackEntry?.destination
278 | NavigationBar {
279 | screens.forEach { screens ->
280 | if (currentDestination != null) {
281 | AddItem(
282 | screens = screens,
283 | currentDestination = currentDestination,
284 | navController = navController
285 | )
286 | }
287 | }
288 | }
289 | }
290 |
291 | @Composable
292 | fun RowScope.AddItem(screens: BottomNavScreen, currentDestination: NavDestination, navController: NavHostController) {
293 | NavigationBarItem(
294 | label = { Text(text = screens.title) },
295 | icon = { Icon(imageVector = screens.icon, contentDescription = "Icons") },
296 | selected = currentDestination.hierarchy?.any {
297 | it.route == screens.route
298 | } == true,
299 | onClick = {
300 | navController.navigate(screens.route) {
301 | popUpTo(navController.graph.id)
302 | launchSingleTop = true
303 | }
304 | }
305 | )
306 | }
307 |
308 | @Composable
309 | fun BottomNavGraph(navController: NavHostController, context: Context, authManager: AuthManager) {
310 | val realtime = RealtimeManager(context)
311 | val firestore = FirestoreManager(context)
312 | val storage = CloudStorageManager(context)
313 | NavHost(navController = navController, startDestination = BottomNavScreen.Contact.route) {
314 | composable(route = BottomNavScreen.Contact.route) {
315 | ContactsScreen(realtime = realtime, authManager = authManager)
316 | }
317 | composable(route = BottomNavScreen.Note.route) {
318 | NotesScreen(firestore = firestore)
319 | }
320 | composable(route = BottomNavScreen.Photos.route) {
321 | CloudStorageScreen(storage = storage)
322 | }
323 | }
324 | }
325 |
326 | sealed class BottomNavScreen(val route: String, val title: String, val icon: ImageVector) {
327 | object Contact : BottomNavScreen(
328 | route = "contact",
329 | title = "Contactos",
330 | icon = Icons.Default.Person
331 | )
332 | object Note : BottomNavScreen(
333 | route = "notes",
334 | title = "Notas",
335 | icon = Icons.Default.List
336 | )
337 | object Photos : BottomNavScreen(
338 | route = "photos",
339 | title = "Photos",
340 | icon = Icons.Default.Face
341 | )
342 | }
--------------------------------------------------------------------------------