├── .gitignore ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hackaprende │ │ └── dogedex │ │ ├── AuthScreenTest.kt │ │ ├── CustomTestRunner.kt │ │ ├── DogListScreenTest.kt │ │ ├── ExampleInstrumentedTest.kt │ │ ├── LoginActivityTest.kt │ │ └── MainActivityTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── labels.txt │ │ └── model.tflite │ ├── java │ │ └── com │ │ │ └── hackaprende │ │ │ └── dogedex │ │ │ └── DogedexApplication.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── detail_info_background.xml │ │ ├── dog_list_item_background.xml │ │ ├── dog_list_item_null_background.xml │ │ ├── ic_check_black.xml │ │ ├── ic_hearth_white.xml │ │ ├── ic_launcher_background.xml │ │ ├── progressbar_background.xml │ │ └── red_circle.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── com │ └── hackaprende │ └── dogedex │ ├── DogRepositoryTest.kt │ └── viewmodel │ ├── AuthViewModelTest.kt │ ├── DogListViewModelTest.kt │ └── DogedexCoroutineRule.kt ├── auth ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hackaprende │ │ └── dogedex │ │ └── auth │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── hackaprende │ │ └── dogedex │ │ └── auth │ │ ├── auth │ │ ├── AuthNavDestinations.kt │ │ ├── AuthRepository.kt │ │ ├── AuthScreen.kt │ │ ├── AuthViewModel.kt │ │ ├── LoginActivity.kt │ │ ├── LoginScreen.kt │ │ └── SignUpScreen.kt │ │ └── di │ │ └── AuthTasksModule.kt │ └── test │ └── java │ └── com │ └── hackaprende │ └── dogedex │ └── auth │ └── ExampleUnitTest.kt ├── build.gradle ├── camera ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hackaprende │ │ └── dogedex │ │ └── camera │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hackaprende │ │ │ └── dogedex │ │ │ └── camera │ │ │ ├── Constants.kt │ │ │ ├── di │ │ │ ├── ClassifierConstructorModule.kt │ │ │ └── ClassifierModule.kt │ │ │ ├── machinelearning │ │ │ ├── Classifier.kt │ │ │ ├── ClassifierRepository.kt │ │ │ └── DogRecognition.kt │ │ │ └── main │ │ │ ├── MainActivity.kt │ │ │ └── MainViewModel.kt │ └── res │ │ ├── drawable │ │ ├── ic_baseline_list.xml │ │ ├── ic_baseline_photo_camera.xml │ │ └── ic_baseline_settings.xml │ │ ├── layout │ │ └── activity_main.xml │ │ └── values │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── hackaprende │ └── dogedex │ └── camera │ └── ExampleUnitTest.kt ├── core ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hackaprende │ │ └── dogedex │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hackaprende │ │ │ └── dogedex │ │ │ └── core │ │ │ ├── Contants.kt │ │ │ ├── Utils.kt │ │ │ ├── WholeImageActivity.kt │ │ │ ├── api │ │ │ ├── ApiResponseStatus.kt │ │ │ ├── ApiService.kt │ │ │ ├── ApiServiceInterceptor.kt │ │ │ ├── dto │ │ │ │ ├── AddDogToUserDTO.kt │ │ │ │ ├── DogDTO.kt │ │ │ │ ├── DogDTOMapper.kt │ │ │ │ ├── LoginDTO.kt │ │ │ │ ├── SignUpDTO.kt │ │ │ │ ├── UserDTO.kt │ │ │ │ └── UserDTOMapper.kt │ │ │ ├── makeNetworkCall.kt │ │ │ └── responses │ │ │ │ ├── AuthApiResponse.kt │ │ │ │ ├── DefaultResponse.kt │ │ │ │ ├── DogApiResponse.kt │ │ │ │ ├── DogListApiResponse.kt │ │ │ │ ├── DogListResponse.kt │ │ │ │ ├── DogResponse.kt │ │ │ │ ├── LoginApiResponse.kt │ │ │ │ └── UserResponse.kt │ │ │ ├── composables │ │ │ ├── AuthField.kt │ │ │ ├── BackNavigationIcon.kt │ │ │ ├── ErrorDialog.kt │ │ │ └── LoadingWheel.kt │ │ │ ├── di │ │ │ ├── ApiServiceModule.kt │ │ │ ├── DispatchersModule.kt │ │ │ └── DogTasksModule.kt │ │ │ ├── dogdetail │ │ │ ├── DogDetailComposeActivity.kt │ │ │ ├── DogDetailScreen.kt │ │ │ ├── DogDetailViewModel.kt │ │ │ ├── MostProbableDogsDialog.kt │ │ │ └── ui │ │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── doglist │ │ │ ├── DogListActivity.kt │ │ │ ├── DogListScreen.kt │ │ │ ├── DogListViewModel.kt │ │ │ └── DogRepository.kt │ │ │ ├── model │ │ │ ├── Dog.kt │ │ │ └── User.kt │ │ │ ├── settings │ │ │ └── SettingsActivity.kt │ │ │ └── testutils │ │ │ └── EspressoIdlingResource.kt │ └── res │ │ ├── drawable │ │ ├── detail_info_background.xml │ │ ├── dog_list_item_background.xml │ │ ├── dog_list_item_null_background.xml │ │ ├── ic_check_black.xml │ │ ├── ic_hearth_white.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── progressbar_background.xml │ │ └── red_circle.xml │ │ ├── layout │ │ ├── activity_settings.xml │ │ └── activity_whole_image.xml │ │ └── values │ │ ├── colors.xml │ │ └── strings.xml │ └── test │ └── java │ └── com │ └── hackaprende │ └── dogedex │ └── core │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | #built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # files for the dex VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # generated files 13 | bin/ 14 | gen/ 15 | 16 | # Local configuration file (sdk path, etc) 17 | local.properties 18 | 19 | # Windows thumbnail db 20 | Thumbs.db 21 | 22 | # OSX files 23 | .DS_Store 24 | 25 | # Android Studio 26 | *.iml 27 | .idea 28 | #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. 29 | .gradle 30 | build/ 31 | .navigation 32 | captures/ 33 | output.json 34 | 35 | #NDK 36 | obj/ 37 | .externalNativeBuild -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'kotlin-parcelize' 6 | id 'dagger.hilt.android.plugin' 7 | } 8 | 9 | android { 10 | compileSdk 33 11 | 12 | defaultConfig { 13 | applicationId "com.hackaprende.dogedex" 14 | minSdk 21 15 | targetSdk 33 16 | versionCode 1 17 | versionName "1.0" 18 | 19 | testInstrumentationRunner "com.hackaprende.dogedex.CustomTestRunner" 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 | } 38 | buildFeatures { 39 | dataBinding true 40 | compose true 41 | } 42 | composeOptions { 43 | kotlinCompilerExtensionVersion compose_version 44 | } 45 | packagingOptions { 46 | resources { 47 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 48 | } 49 | } 50 | } 51 | 52 | dependencies { 53 | 54 | // Modules 55 | implementation project(path: ':core') 56 | implementation project(path: ':camera') 57 | implementation project(path: ':auth') 58 | 59 | implementation 'androidx.core:core-ktx:1.9.0' 60 | implementation 'androidx.appcompat:appcompat:1.5.1' 61 | implementation 'com.google.android.material:material:1.7.0' 62 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 63 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" 64 | implementation "androidx.activity:activity-ktx:1.6.1" 65 | implementation "io.coil-kt:coil:1.4.0" 66 | 67 | implementation "androidx.navigation:navigation-fragment-ktx:2.5.3" 68 | implementation "androidx.navigation:navigation-ui-ktx:2.5.3" 69 | 70 | //CameraX 71 | implementation "androidx.camera:camera-camera2:1.2.0-rc01" 72 | implementation "androidx.camera:camera-lifecycle:1.2.0-rc01" 73 | implementation "androidx.camera:camera-view:1.2.0-rc01" 74 | 75 | // TensorFlow Lite 76 | implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly-SNAPSHOT' 77 | implementation 'org.tensorflow:tensorflow-lite-support:0.1.0' 78 | 79 | // Jetpack Compose 80 | implementation 'androidx.compose.ui:ui:1.3.0' 81 | // Tooling support (Previews, etc.) 82 | implementation 'androidx.compose.ui:ui-tooling:1.3.0' 83 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 84 | implementation 'androidx.compose.foundation:foundation:1.3.0' 85 | // Material Design 86 | implementation 'androidx.compose.material:material:1.3.0' 87 | // Material design icons 88 | implementation 'androidx.compose.material:material-icons-core:1.3.0' 89 | implementation 'androidx.compose.material:material-icons-extended:1.3.0' 90 | // Integration with activities 91 | implementation 'androidx.activity:activity-compose:1.6.1' 92 | // Integration with ViewModels 93 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1' 94 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 95 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' 96 | implementation "androidx.navigation:navigation-compose:2.5.3" 97 | 98 | // Coil 99 | implementation 'io.coil-kt:coil-compose:1.3.1' 100 | 101 | // UI Tests 102 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.3.0' 103 | 104 | // Dependency Injection with Hilt 105 | implementation "com.google.dagger:hilt-android:2.42" 106 | kapt "com.google.dagger:hilt-compiler:2.42" 107 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 108 | 109 | // Espresso idling resources 110 | implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0' 111 | 112 | // Retrofit y Moshi 113 | implementation "com.squareup.retrofit2:retrofit:2.9.0" 114 | implementation "com.squareup.retrofit2:converter-moshi:2.9.0" 115 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 116 | testImplementation 'junit:junit:4.13.2' 117 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" 118 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 119 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 120 | debugImplementation "androidx.compose.ui:ui-test-manifest:1.3.0" 121 | androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42' 122 | kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42' 123 | } -------------------------------------------------------------------------------- /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/com/hackaprende/dogedex/AuthScreenTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import androidx.compose.ui.test.assertIsDisplayed 4 | import androidx.compose.ui.test.junit4.createComposeRule 5 | import androidx.compose.ui.test.onNodeWithTag 6 | import androidx.compose.ui.test.performClick 7 | import androidx.compose.ui.test.performTextInput 8 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 9 | import com.hackaprende.dogedex.camera.auth.AuthScreen 10 | import com.hackaprende.dogedex.camera.auth.AuthTasks 11 | import com.hackaprende.dogedex.camera.auth.AuthViewModel 12 | import com.hackaprende.dogedex.core.model.User 13 | import org.junit.Rule 14 | import org.junit.Test 15 | 16 | class AuthScreenTest { 17 | @get:Rule 18 | val composeTestRule = createComposeRule() 19 | 20 | @Test 21 | fun testTappingRegisterButtonOpenSignUpScreen() { 22 | class FakeAuthRepository: AuthTasks { 23 | override suspend fun login(email: String, password: String): ApiResponseStatus { 24 | TODO("Not yet implemented") 25 | } 26 | 27 | override suspend fun signUp( 28 | email: String, 29 | password: String, 30 | passwordConfirmation: String 31 | ): ApiResponseStatus { 32 | TODO("Not yet implemented") 33 | } 34 | 35 | } 36 | 37 | val viewModel = AuthViewModel( 38 | authRepository = FakeAuthRepository() 39 | ) 40 | 41 | composeTestRule.setContent { 42 | AuthScreen( 43 | onUserLoggedIn = { }, 44 | authViewModel = viewModel 45 | ) 46 | } 47 | 48 | composeTestRule.onNodeWithTag(testTag = "login-button").assertIsDisplayed() 49 | composeTestRule.onNodeWithTag(testTag = "login-screen-register-button").performClick() 50 | composeTestRule.onNodeWithTag(testTag = "sign-up-button").assertIsDisplayed() 51 | } 52 | 53 | @Test 54 | fun testEmailErrorShowsIfTappingLoginButtonAndNotEmail() { 55 | class FakeAuthRepository: AuthTasks { 56 | override suspend fun login(email: String, password: String): ApiResponseStatus { 57 | TODO("Not yet implemented") 58 | } 59 | 60 | override suspend fun signUp( 61 | email: String, 62 | password: String, 63 | passwordConfirmation: String 64 | ): ApiResponseStatus { 65 | TODO("Not yet implemented") 66 | } 67 | 68 | } 69 | 70 | val viewModel = AuthViewModel( 71 | authRepository = FakeAuthRepository() 72 | ) 73 | 74 | composeTestRule.setContent { 75 | AuthScreen( 76 | onUserLoggedIn = { }, 77 | authViewModel = viewModel 78 | ) 79 | } 80 | 81 | composeTestRule.onNodeWithTag(testTag = "login-button").performClick() 82 | composeTestRule.onNodeWithTag(useUnmergedTree = true, testTag = "email-field-error").assertIsDisplayed() 83 | composeTestRule.onNodeWithTag(useUnmergedTree = true, testTag = "email-field").performTextInput("hackaprende@gmail.com") 84 | composeTestRule.onNodeWithTag(useUnmergedTree = true, testTag = "login-button").performClick() 85 | composeTestRule.onNodeWithTag(useUnmergedTree = true, testTag = "password-field-error").assertIsDisplayed() 86 | } 87 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hackaprende/dogedex/CustomTestRunner.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import androidx.test.runner.AndroidJUnitRunner 6 | import dagger.hilt.android.testing.HiltTestApplication 7 | 8 | // A custom runner to set up the instrumented application class for tests. 9 | class CustomTestRunner : AndroidJUnitRunner() { 10 | 11 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application { 12 | return super.newApplication(cl, HiltTestApplication::class.java.name, context) 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hackaprende/dogedex/DogListScreenTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.material.ExperimentalMaterialApi 5 | import androidx.compose.ui.test.* 6 | import androidx.compose.ui.test.junit4.createComposeRule 7 | import coil.annotation.ExperimentalCoilApi 8 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 9 | import com.hackaprende.dogedex.core.doglist.DogListScreen 10 | import com.hackaprende.dogedex.core.doglist.DogListViewModel 11 | import com.hackaprende.dogedex.core.doglist.DogTasks 12 | import com.hackaprende.dogedex.core.model.Dog 13 | import org.junit.Rule 14 | import org.junit.Test 15 | 16 | @ExperimentalCoilApi 17 | @ExperimentalFoundationApi 18 | @ExperimentalMaterialApi 19 | class DogListScreenTest { 20 | @get:Rule 21 | val composeTestRule = createComposeRule() 22 | 23 | @Test 24 | fun progressBarShowsWhenLoadingState() { 25 | class FakeDogRepository : DogTasks { 26 | override suspend fun getDogCollection(): ApiResponseStatus> { 27 | return ApiResponseStatus.Loading() 28 | } 29 | 30 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 31 | TODO("Not yet implemented") 32 | } 33 | 34 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 35 | TODO("Not yet implemented") 36 | } 37 | } 38 | 39 | val viewModel = DogListViewModel( 40 | dogRepository = FakeDogRepository() 41 | ) 42 | 43 | composeTestRule.setContent { 44 | DogListScreen( 45 | onNavigationIconClick = { }, 46 | onDogClicked = { }, 47 | viewModel = viewModel, 48 | ) 49 | } 50 | 51 | composeTestRule.onNodeWithTag(testTag = "loading-wheel").assertIsDisplayed() 52 | } 53 | 54 | @Test 55 | fun errorDialogShowsIfErrorGettingDogs() { 56 | class FakeDogRepository : DogTasks { 57 | override suspend fun getDogCollection(): ApiResponseStatus> { 58 | return ApiResponseStatus.Error(messageId = R.string.there_was_an_error) 59 | } 60 | 61 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 62 | TODO("Not yet implemented") 63 | } 64 | 65 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 66 | TODO("Not yet implemented") 67 | } 68 | } 69 | 70 | val viewModel = DogListViewModel( 71 | dogRepository = FakeDogRepository() 72 | ) 73 | 74 | composeTestRule.setContent { 75 | DogListScreen( 76 | onNavigationIconClick = { }, 77 | onDogClicked = { }, 78 | viewModel = viewModel, 79 | ) 80 | } 81 | 82 | composeTestRule.onNodeWithTag(testTag = "error-dialog").assertIsDisplayed() 83 | } 84 | 85 | @Test 86 | fun dogListShowsIfSuccessGettingDogs() { 87 | val dog1Name = "Chihuahua" 88 | val dog2Name = "Guillermo" 89 | class FakeDogRepository : DogTasks { 90 | override suspend fun getDogCollection(): ApiResponseStatus> { 91 | return ApiResponseStatus.Success( 92 | listOf( 93 | Dog( 94 | 1, 1, dog1Name, "", "", "", 95 | "", "", "", "", "", 96 | inCollection = true 97 | ), 98 | Dog( 99 | 19, 23, dog2Name, "", "", "", 100 | "", "", "", "", "", 101 | inCollection = false 102 | ), 103 | ) 104 | ) 105 | } 106 | 107 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 108 | TODO("Not yet implemented") 109 | } 110 | 111 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 112 | TODO("Not yet implemented") 113 | } 114 | } 115 | 116 | val viewModel = DogListViewModel( 117 | dogRepository = FakeDogRepository() 118 | ) 119 | 120 | composeTestRule.setContent { 121 | DogListScreen( 122 | onNavigationIconClick = { }, 123 | onDogClicked = { }, 124 | viewModel = viewModel, 125 | ) 126 | } 127 | 128 | composeTestRule.onNodeWithTag(useUnmergedTree = true, testTag = "dog-${dog1Name}").assertIsDisplayed() 129 | composeTestRule.onNodeWithText("23").assertIsDisplayed() 130 | } 131 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hackaprende/dogedex/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 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("com.hackaprende.dogedex", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hackaprende/dogedex/LoginActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.material.ExperimentalMaterialApi 5 | import androidx.compose.ui.test.* 6 | import androidx.compose.ui.test.junit4.createAndroidComposeRule 7 | import androidx.test.espresso.Espresso.onView 8 | import androidx.test.espresso.assertion.ViewAssertions.matches 9 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 10 | import androidx.test.espresso.matcher.ViewMatchers.withId 11 | import coil.annotation.ExperimentalCoilApi 12 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 13 | import com.hackaprende.dogedex.camera.auth.AuthTasks 14 | import com.hackaprende.dogedex.camera.auth.LoginActivity 15 | import com.hackaprende.dogedex.camera.di.AuthTasksModule 16 | import com.hackaprende.dogedex.core.model.User 17 | import dagger.Binds 18 | import dagger.Module 19 | import dagger.hilt.InstallIn 20 | import dagger.hilt.android.testing.HiltAndroidRule 21 | import dagger.hilt.android.testing.HiltAndroidTest 22 | import dagger.hilt.android.testing.UninstallModules 23 | import dagger.hilt.components.SingletonComponent 24 | import org.junit.Rule 25 | import org.junit.Test 26 | import javax.inject.Inject 27 | 28 | @ExperimentalCoilApi 29 | @ExperimentalFoundationApi 30 | @ExperimentalMaterialApi 31 | @UninstallModules(com.hackaprende.dogedex.camera.di.AuthTasksModule::class) 32 | @HiltAndroidTest 33 | class LoginActivityTest { 34 | 35 | @get:Rule(order = 0) 36 | var hiltRule = HiltAndroidRule(this) 37 | 38 | @get:Rule(order = 1) 39 | val composeTestRule = createAndroidComposeRule() 40 | 41 | class FakeAuthRepository @Inject constructor(): AuthTasks { 42 | override suspend fun login(email: String, password: String): ApiResponseStatus { 43 | return ApiResponseStatus.Success( 44 | User(1L, "hackaprende@gmail.com", "ubycasb67878asd") 45 | ) 46 | } 47 | 48 | override suspend fun signUp( 49 | email: String, 50 | password: String, 51 | passwordConfirmation: String 52 | ): ApiResponseStatus { 53 | TODO("Not yet implemented") 54 | } 55 | } 56 | 57 | @Module 58 | @InstallIn(SingletonComponent::class) 59 | abstract class AuthTasksTestModule { 60 | 61 | @Binds 62 | abstract fun bindDogTasks( 63 | fakeAuthRepository: FakeAuthRepository 64 | ): AuthTasks 65 | } 66 | 67 | @Test 68 | fun mainActivityOpensAfterUserLogin() { 69 | val context = composeTestRule.activity 70 | 71 | composeTestRule 72 | .onNodeWithText(context.getString(R.string.login)) 73 | .assertIsDisplayed() 74 | 75 | composeTestRule 76 | .onNodeWithTag(useUnmergedTree = true, testTag = "email-field") 77 | .performTextInput("hackaprende@gmail.com") 78 | 79 | composeTestRule 80 | .onNodeWithTag(useUnmergedTree = true, testTag = "password-field") 81 | .performTextInput("test1234") 82 | 83 | composeTestRule 84 | .onNodeWithText(context.getString(R.string.login)) 85 | .performClick() 86 | 87 | onView(withId(R.id.take_photo_fab)).check(matches(isDisplayed())) 88 | } 89 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/hackaprende/dogedex/MainActivityTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import androidx.camera.core.ImageProxy 4 | import androidx.compose.foundation.ExperimentalFoundationApi 5 | import androidx.compose.material.ExperimentalMaterialApi 6 | import androidx.compose.ui.test.assertIsDisplayed 7 | import androidx.compose.ui.test.junit4.createComposeRule 8 | import androidx.compose.ui.test.onNodeWithTag 9 | import androidx.compose.ui.test.onNodeWithText 10 | import androidx.test.espresso.Espresso.onView 11 | import androidx.test.espresso.IdlingRegistry 12 | import androidx.test.espresso.action.ViewActions.click 13 | import androidx.test.espresso.assertion.ViewAssertions.matches 14 | import androidx.test.espresso.matcher.ViewMatchers.* 15 | import androidx.test.ext.junit.rules.ActivityScenarioRule 16 | import androidx.test.platform.app.InstrumentationRegistry 17 | import coil.annotation.ExperimentalCoilApi 18 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 19 | import com.hackaprende.dogedex.camera.di.ClassifierModule 20 | import com.hackaprende.dogedex.camera.di.DogTasksModule 21 | import com.hackaprende.dogedex.core.doglist.DogTasks 22 | import com.hackaprende.dogedex.camera.machinelearning.ClassifierTasks 23 | import com.hackaprende.dogedex.camera.machinelearning.DogRecognition 24 | import com.hackaprende.dogedex.camera.main.MainActivity 25 | import com.hackaprende.dogedex.core.model.Dog 26 | import com.hackaprende.dogedex.core.testutils.EspressoIdlingResource 27 | import dagger.Binds 28 | import dagger.Module 29 | import dagger.hilt.InstallIn 30 | import dagger.hilt.android.testing.HiltAndroidRule 31 | import dagger.hilt.android.testing.HiltAndroidTest 32 | import dagger.hilt.android.testing.UninstallModules 33 | import dagger.hilt.components.SingletonComponent 34 | import org.junit.After 35 | import org.junit.Before 36 | import org.junit.Rule 37 | import org.junit.Test 38 | import javax.inject.Inject 39 | 40 | @ExperimentalCoilApi 41 | @ExperimentalMaterialApi 42 | @ExperimentalFoundationApi 43 | @UninstallModules(com.hackaprende.dogedex.camera.di.DogTasksModule::class, com.hackaprende.dogedex.camera.di.ClassifierModule::class) 44 | @HiltAndroidTest 45 | class MainActivityTest { 46 | 47 | @get:Rule(order = 0) 48 | var hiltRule = HiltAndroidRule(this) 49 | 50 | @get:Rule(order = 1) 51 | val composeTestRule = createComposeRule() 52 | 53 | @get:Rule(order = 2) 54 | val activityScenarioRule = ActivityScenarioRule(MainActivity::class.java) 55 | 56 | class FakeDogRepository @Inject constructor(): DogTasks { 57 | override suspend fun getDogCollection(): ApiResponseStatus> { 58 | return ApiResponseStatus.Success( 59 | listOf( 60 | Dog( 61 | 1, 1, "", "", "", "", 62 | "", "", "", "", "", 63 | inCollection = true 64 | ), 65 | Dog( 66 | 19, 23, "", "", "", "", 67 | "", "", "", "", "", 68 | inCollection = false 69 | ), 70 | ) 71 | ) 72 | } 73 | 74 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 75 | TODO("Not yet implemented") 76 | } 77 | 78 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 79 | return ApiResponseStatus.Success( 80 | Dog( 81 | 89, 70, "Chow chow", "", "", "", 82 | "", "", "", "", "", 83 | inCollection = true 84 | ) 85 | ) 86 | } 87 | } 88 | 89 | @Module 90 | @InstallIn(SingletonComponent::class) 91 | abstract class DogTasksTestModule { 92 | 93 | @Binds 94 | abstract fun bindDogTasks( 95 | fakeDogRepository: FakeDogRepository 96 | ): DogTasks 97 | } 98 | 99 | class FakeClassifierRepository @Inject constructor(): ClassifierTasks { 100 | override suspend fun recognizeImage(imageProxy: ImageProxy): DogRecognition { 101 | return DogRecognition("ajsncuinasdc", 100.0f) 102 | } 103 | } 104 | 105 | @Module 106 | @InstallIn(SingletonComponent::class) 107 | abstract class ClassifierTestModule { 108 | @Binds 109 | abstract fun bindClassifierTasks( 110 | fakeClassifierRepository: FakeClassifierRepository 111 | ): ClassifierTasks 112 | } 113 | 114 | @Before 115 | fun registerIdlingResource() { 116 | IdlingRegistry.getInstance().register(EspressoIdlingResource.idlingResource) 117 | } 118 | 119 | @After 120 | fun unregisterIdlingResource() { 121 | IdlingRegistry.getInstance().unregister(EspressoIdlingResource.idlingResource) 122 | } 123 | 124 | @Test 125 | fun showAllFab() { 126 | onView(withId(R.id.take_photo_fab)).check(matches(isDisplayed())) 127 | onView(withId(R.id.dog_list_fab)).check(matches(isDisplayed())) 128 | onView(withId(R.id.settings_fab)).check(matches(isDisplayed())) 129 | } 130 | 131 | @Test 132 | fun dogListOpensWhenClickingButton() { 133 | onView(withId(R.id.dog_list_fab)).perform(click()) 134 | 135 | val context = InstrumentationRegistry.getInstrumentation().targetContext 136 | val string = context.getString(R.string.my_dog_collection) 137 | composeTestRule.onNodeWithText(string).assertIsDisplayed() 138 | } 139 | 140 | @Test 141 | fun whenRecognizingDogDetailsScreenOpens() { 142 | onView(withId(R.id.take_photo_fab)).perform(click()) 143 | composeTestRule.onNodeWithTag(testTag = "close-details-screen-fab").assertIsDisplayed() 144 | } 145 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 20 | 25 | 28 | 31 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/assets/labels.txt: -------------------------------------------------------------------------------- 1 | n02085620-chihuahua 2 | n02085782-japanese_spaniel 3 | n02085936-maltese_dog 4 | n02086079-pekinese 5 | n02086240-shih-tzu 6 | n02086646-blenheim_spaniel 7 | n02086910-papillon 8 | n02087046-toy_terrier 9 | n02087394-rhodesian_ridgeback 10 | n02088094-afghan_hound 11 | n02088238-basset 12 | n02088364-beagle 13 | n02088466-bloodhound 14 | n02088632-bluetick 15 | n02089078-black-and-tan_coonhound 16 | n02089867-walker_hound 17 | n02089973-english_foxhound 18 | n02090379-redbone 19 | n02090622-borzoi 20 | n02090721-irish_wolfhound 21 | n02091032-italian_greyhound 22 | n02091134-whippet 23 | n02091244-ibizan_hound 24 | n02091467-norwegian_elkhound 25 | n02091635-otterhound 26 | n02091831-saluki 27 | n02092002-scottish_deerhound 28 | n02092339-weimaraner 29 | n02093256-staffordshire_bullterrier 30 | n02093428-american_staffordshire_terrier 31 | n02093647-bedlington_terrier 32 | n02093754-border_terrier 33 | n02093859-kerry_blue_terrier 34 | n02093991-irish_terrier 35 | n02094114-norfolk_terrier 36 | n02094258-norwich_terrier 37 | n02094433-yorkshire_terrier 38 | n02095314-wire-haired_fox_terrier 39 | n02095570-lakeland_terrier 40 | n02095889-sealyham_terrier 41 | n02096051-airedale 42 | n02096177-cairn 43 | n02096294-australian_terrier 44 | n02096437-dandie_dinmont 45 | n02096585-boston_bull 46 | n02097047-miniature_schnauzer 47 | n02097130-giant_schnauzer 48 | n02097209-standard_schnauzer 49 | n02097298-scotch_terrier 50 | n02097474-tibetan_terrier 51 | n02097658-silky_terrier 52 | n02098105-soft-coated_wheaten_terrier 53 | n02098286-west_highland_white_terrier 54 | n02098413-lhasa 55 | n02099267-flat-coated_retriever 56 | n02099429-curly-coated_retriever 57 | n02099601-golden_retriever 58 | n02099712-labrador_retriever 59 | n02099849-chesapeake_bay_retriever 60 | n02100236-german_short-haired_pointer 61 | n02100583-vizsla 62 | n02100735-english_setter 63 | n02100877-irish_setter 64 | n02101006-gordon_setter 65 | n02101388-brittany_spaniel 66 | n02101556-clumber 67 | n02102040-english_springer 68 | n02102177-welsh_springer_spaniel 69 | n02102318-cocker_spaniel 70 | n02102480-sussex_spaniel 71 | n02102973-irish_water_spaniel 72 | n02104029-kuvasz 73 | n02104365-schipperke 74 | n02105056-groenendael 75 | n02105162-malinois 76 | n02105251-briard 77 | n02105412-kelpie 78 | n02105505-komondor 79 | n02105641-old_english_sheepdog 80 | n02105855-shetland_sheepdog 81 | n02106030-collie 82 | n02106166-border_collie 83 | n02106382-bouvier_des_flandres 84 | n02106550-rottweiler 85 | n02106662-german_shepherd 86 | n02107142-doberman 87 | n02107312-miniature_pinscher 88 | n02107574-greater_swiss_mountain_dog 89 | n02107683-bernese_mountain_dog 90 | n02107908-appenzeller 91 | n02108000-entlebucher 92 | n02108089-boxer 93 | n02108422-bull_mastiff 94 | n02108551-tibetan_mastiff 95 | n02108915-french_bulldog 96 | n02109047-great_dane 97 | n02109525-saint_bernard 98 | n02109961-eskimo_dog 99 | n02110063-malamute 100 | n02110185-siberian_husky 101 | n02110627-affenpinscher 102 | n02110806-basenji 103 | n02110958-pug 104 | n02111129-leonberg 105 | n02111277-newfoundland 106 | n02111500-great_pyrenees 107 | n02111889-samoyed 108 | n02112018-pomeranian 109 | n02112137-chow 110 | n02112350-keeshond 111 | n02112706-brabancon_griffon 112 | n02113023-pembroke 113 | n02113186-cardigan 114 | n02113624-toy_poodle 115 | n02113712-miniature_poodle 116 | n02113799-standard_poodle 117 | n02113978-mexican_hairless 118 | n02115641-dingo 119 | n02115913-dhole 120 | n02116738-african_hunting_dog -------------------------------------------------------------------------------- /app/src/main/assets/model.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/assets/model.tflite -------------------------------------------------------------------------------- /app/src/main/java/com/hackaprende/dogedex/DogedexApplication.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class DogedexApplication : Application() -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/detail_info_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dog_list_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/dog_list_item_null_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_hearth_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progressbar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/red_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #FF3700B3 11 | #212121 12 | #ABABAB 13 | #717171 14 | #F44336 15 | #D32F2F 16 | #FFFF00 17 | #142CC6 18 | #0C1D8A 19 | #BCBCBC 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Dogedex 3 | Female 4 | Weight 5 | Height 6 | Group 7 | Male 8 | Error showing dog, dog not found 9 | #%d 10 | %s years 11 | There is no internet connection 12 | There was an error 13 | Register 14 | Do not have an account? 15 | Password 16 | Email 17 | Login 18 | Sign up 19 | Confirm password 20 | Email is not valid 21 | Password must not be empty 22 | Passwords do not match 23 | Error signing up 24 | Error logging in 25 | User already exists 26 | There was an error 27 | Wrong user or password 28 | This is a content description 29 | Logout 30 | There was an error adding dog to collection 31 | Acepta la camara o me da amsiedad 32 | Aceptame por favor 33 | You need to accept camera permission to use camera 34 | DogDetailComposeActivity 35 | Oops, something happened! 36 | Try again 37 | My dog collection 38 | Not your dog? Tap to see other probable dogs 39 | Other probable dogs 40 | My dog is not here 41 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /app/src/test/java/com/hackaprende/dogedex/DogRepositoryTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 4 | import com.hackaprende.dogedex.core.api.ApiService 5 | import com.hackaprende.dogedex.core.api.dto.AddDogToUserDTO 6 | import com.hackaprende.dogedex.core.api.dto.DogDTO 7 | import com.hackaprende.dogedex.core.api.dto.LoginDTO 8 | import com.hackaprende.dogedex.core.api.dto.SignUpDTO 9 | import com.hackaprende.dogedex.core.api.responses.* 10 | import com.hackaprende.dogedex.core.doglist.DogRepository 11 | import kotlinx.coroutines.ExperimentalCoroutinesApi 12 | import kotlinx.coroutines.runBlocking 13 | import kotlinx.coroutines.test.TestCoroutineDispatcher 14 | import org.junit.Assert.assertEquals 15 | import org.junit.Test 16 | import java.net.UnknownHostException 17 | 18 | @ExperimentalCoroutinesApi 19 | class DogRepositoryTest { 20 | 21 | @Test 22 | fun testGetDogCollectionSuccess(): Unit = runBlocking { 23 | class FakeApiService : ApiService { 24 | override suspend fun getAllDogs(): DogListApiResponse { 25 | return DogListApiResponse( 26 | message = "", 27 | isSuccess = true, 28 | data = DogListResponse( 29 | dogs = listOf( 30 | DogDTO( 31 | 1, 1, "Wartoortle", "", "", "", 32 | "", "", "", "", "", 33 | ), 34 | DogDTO( 35 | 19, 2, "Charmeleon", "", "", "", 36 | "", "", "", "", "", 37 | ), 38 | ) 39 | ) 40 | ) 41 | } 42 | 43 | override suspend fun signUp(signUpDTO: SignUpDTO): AuthApiResponse { 44 | TODO("Not yet implemented") 45 | } 46 | 47 | override suspend fun login(loginDTO: LoginDTO): AuthApiResponse { 48 | TODO("Not yet implemented") 49 | } 50 | 51 | override suspend fun addDogToUser(addDogToUserDTO: AddDogToUserDTO): DefaultResponse { 52 | TODO("Not yet implemented") 53 | } 54 | 55 | override suspend fun getUserDogs(): DogListApiResponse { 56 | return DogListApiResponse( 57 | message = "", 58 | isSuccess = true, 59 | data = DogListResponse( 60 | dogs = listOf( 61 | DogDTO( 62 | 19, 2, "Charmeleon", "", "", "", 63 | "", "", "", "", "", 64 | ), 65 | ) 66 | ) 67 | ) 68 | } 69 | 70 | override suspend fun getDogByMlId(mlId: String): DogApiResponse { 71 | TODO("Not yet implemented") 72 | } 73 | 74 | } 75 | 76 | val dogRepository = DogRepository( 77 | apiService = FakeApiService(), 78 | dispatcher = TestCoroutineDispatcher() 79 | ) 80 | 81 | val apiResponseStatus = dogRepository.getDogCollection() 82 | assert(apiResponseStatus is ApiResponseStatus.Success) 83 | val dogCollection = (apiResponseStatus as ApiResponseStatus.Success).data 84 | assertEquals(2, dogCollection.size) 85 | assertEquals("Charmeleon", dogCollection[1].name) 86 | assertEquals("", dogCollection[0].name) 87 | } 88 | 89 | @Test 90 | fun testGetAllDogsError(): Unit = runBlocking { 91 | class FakeApiService : ApiService { 92 | override suspend fun getAllDogs(): DogListApiResponse { 93 | throw UnknownHostException() 94 | } 95 | 96 | override suspend fun signUp(signUpDTO: SignUpDTO): AuthApiResponse { 97 | TODO("Not yet implemented") 98 | } 99 | 100 | override suspend fun login(loginDTO: LoginDTO): AuthApiResponse { 101 | TODO("Not yet implemented") 102 | } 103 | 104 | override suspend fun addDogToUser(addDogToUserDTO: AddDogToUserDTO): DefaultResponse { 105 | TODO("Not yet implemented") 106 | } 107 | 108 | override suspend fun getUserDogs(): DogListApiResponse { 109 | return DogListApiResponse( 110 | message = "", 111 | isSuccess = true, 112 | data = DogListResponse( 113 | dogs = listOf( 114 | DogDTO( 115 | 19, 2, "Charmeleon", "", "", "", 116 | "", "", "", "", "", 117 | ), 118 | ) 119 | ) 120 | ) 121 | } 122 | 123 | override suspend fun getDogByMlId(mlId: String): DogApiResponse { 124 | TODO("Not yet implemented") 125 | } 126 | 127 | } 128 | 129 | val dogRepository = DogRepository( 130 | apiService = FakeApiService(), 131 | dispatcher = TestCoroutineDispatcher() 132 | ) 133 | 134 | val apiResponseStatus = dogRepository.getDogCollection() 135 | assert(apiResponseStatus is ApiResponseStatus.Error) 136 | assertEquals(R.string.unknown_host_exception_error, 137 | (apiResponseStatus as ApiResponseStatus.Error).messageId) 138 | } 139 | 140 | @Test 141 | fun getDogByMlSuccess() = runBlocking { 142 | val resultDogId = 19L 143 | 144 | class FakeApiService : ApiService { 145 | override suspend fun getAllDogs(): DogListApiResponse { 146 | TODO("Not yet implemented") 147 | } 148 | 149 | override suspend fun signUp(signUpDTO: SignUpDTO): AuthApiResponse { 150 | TODO("Not yet implemented") 151 | } 152 | 153 | override suspend fun login(loginDTO: LoginDTO): AuthApiResponse { 154 | TODO("Not yet implemented") 155 | } 156 | 157 | override suspend fun addDogToUser(addDogToUserDTO: AddDogToUserDTO): DefaultResponse { 158 | TODO("Not yet implemented") 159 | } 160 | 161 | override suspend fun getUserDogs(): DogListApiResponse { 162 | TODO("Not yet implemented") 163 | } 164 | 165 | override suspend fun getDogByMlId(mlId: String): DogApiResponse { 166 | return DogApiResponse( 167 | message = "", 168 | isSuccess = true, 169 | data = DogResponse( 170 | dog = DogDTO( 171 | resultDogId, 2, "Charmeleon", "", "", "", 172 | "", "", "", "", "", 173 | ) 174 | ) 175 | ) 176 | } 177 | } 178 | 179 | val dogRepository = DogRepository( 180 | apiService = FakeApiService(), 181 | dispatcher = TestCoroutineDispatcher() 182 | ) 183 | 184 | val apiResponseStatus = dogRepository.getDogByMlId("asijbhdcuhab") 185 | assert(apiResponseStatus is ApiResponseStatus.Success) 186 | assertEquals(resultDogId, (apiResponseStatus as ApiResponseStatus.Success).data.id) 187 | } 188 | 189 | @Test 190 | fun getDogByMlError() = runBlocking { 191 | val resultDogId = 19L 192 | 193 | class FakeApiService : ApiService { 194 | override suspend fun getAllDogs(): DogListApiResponse { 195 | TODO("Not yet implemented") 196 | } 197 | 198 | override suspend fun signUp(signUpDTO: SignUpDTO): AuthApiResponse { 199 | TODO("Not yet implemented") 200 | } 201 | 202 | override suspend fun login(loginDTO: LoginDTO): AuthApiResponse { 203 | TODO("Not yet implemented") 204 | } 205 | 206 | override suspend fun addDogToUser(addDogToUserDTO: AddDogToUserDTO): DefaultResponse { 207 | TODO("Not yet implemented") 208 | } 209 | 210 | override suspend fun getUserDogs(): DogListApiResponse { 211 | TODO("Not yet implemented") 212 | } 213 | 214 | override suspend fun getDogByMlId(mlId: String): DogApiResponse { 215 | return DogApiResponse( 216 | message = "error_getting_dog_by_ml_id", 217 | isSuccess = false, 218 | data = DogResponse( 219 | dog = DogDTO( 220 | resultDogId, 2, "Charmeleon", "", "", "", 221 | "", "", "", "", "", 222 | ) 223 | ) 224 | ) 225 | } 226 | } 227 | 228 | val dogRepository = DogRepository( 229 | apiService = FakeApiService(), 230 | dispatcher = TestCoroutineDispatcher() 231 | ) 232 | 233 | val apiResponseStatus = dogRepository.getDogByMlId("asijbhdcuhab") 234 | assert(apiResponseStatus is ApiResponseStatus.Error) 235 | assertEquals(R.string.unknown_error, (apiResponseStatus as ApiResponseStatus.Error).messageId) 236 | } 237 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hackaprende/dogedex/viewmodel/AuthViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.viewmodel 2 | 3 | import com.hackaprende.dogedex.R 4 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 5 | import com.hackaprende.dogedex.camera.auth.AuthTasks 6 | import com.hackaprende.dogedex.camera.auth.AuthViewModel 7 | import com.hackaprende.dogedex.core.model.User 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import org.junit.Rule 10 | import org.junit.Test 11 | import org.junit.Assert.* 12 | 13 | class AuthViewModelTest { 14 | 15 | @ExperimentalCoroutinesApi 16 | @get:Rule 17 | var dogedexCoroutineRule = DogedexCoroutineRule() 18 | 19 | @Test 20 | fun testLoginValidationsCorrect() { 21 | class FakeAuthRepository : AuthTasks { 22 | override suspend fun login(email: String, password: String): ApiResponseStatus { 23 | return ApiResponseStatus.Success( 24 | User(1, "hackaprende@gmail.com", "") 25 | ) 26 | } 27 | 28 | override suspend fun signUp( 29 | email: String, 30 | password: String, 31 | passwordConfirmation: String 32 | ): ApiResponseStatus { 33 | return ApiResponseStatus.Success( 34 | User(1, "", "") 35 | ) 36 | } 37 | 38 | } 39 | 40 | val viewModel = AuthViewModel( 41 | authRepository = FakeAuthRepository() 42 | ) 43 | 44 | viewModel.login("", "test1234") 45 | 46 | assertEquals( 47 | R.string.email_is_not_valid, 48 | viewModel.emailError.value) 49 | 50 | viewModel.login("hackaprende@gmail.com", "") 51 | 52 | assertEquals( 53 | R.string.password_must_not_be_empty, 54 | viewModel.passwordError.value) 55 | } 56 | 57 | @Test 58 | fun testLoginStatesCorrect() { 59 | val fakeUser = User( 60 | 1, "hackaprende@gmail.com", 61 | "" 62 | ) 63 | class FakeAuthRepository : AuthTasks { 64 | override suspend fun login(email: String, password: String): ApiResponseStatus { 65 | return ApiResponseStatus.Success(fakeUser) 66 | } 67 | 68 | override suspend fun signUp( 69 | email: String, 70 | password: String, 71 | passwordConfirmation: String 72 | ): ApiResponseStatus { 73 | return ApiResponseStatus.Success( 74 | User(1, "", "") 75 | ) 76 | } 77 | 78 | } 79 | 80 | val viewModel = AuthViewModel( 81 | authRepository = FakeAuthRepository() 82 | ) 83 | 84 | viewModel.login("hackaprende@gmail.com", "test1234") 85 | 86 | assertEquals(fakeUser.email, viewModel.user.value?.email) 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hackaprende/dogedex/viewmodel/DogListViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.viewmodel 2 | 3 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 4 | import com.hackaprende.dogedex.core.doglist.DogListViewModel 5 | import com.hackaprende.dogedex.core.doglist.DogTasks 6 | import com.hackaprende.dogedex.core.model.Dog 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import org.junit.Rule 9 | import org.junit.Test 10 | import org.junit.Assert.* 11 | 12 | class DogListViewModelTest { 13 | @ExperimentalCoroutinesApi 14 | @get:Rule 15 | var dogedexCoroutineRule = DogedexCoroutineRule() 16 | 17 | @Test 18 | fun downloadDogListStatusesCorrect() { 19 | class FakeDogRepository : DogTasks { 20 | override suspend fun getDogCollection(): ApiResponseStatus> { 21 | return ApiResponseStatus.Success( 22 | listOf( 23 | Dog( 24 | 1, 1, "", "", "", "", 25 | "", "", "", "", "", 26 | inCollection = false 27 | ), 28 | Dog( 29 | 19, 2, "", "", "", "", 30 | "", "", "", "", "", 31 | inCollection = false 32 | ), 33 | ) 34 | ) 35 | } 36 | 37 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 38 | return ApiResponseStatus.Success(Unit) 39 | } 40 | 41 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 42 | return ApiResponseStatus.Success(Dog( 43 | 1, 1, "", "", "", "", 44 | "", "", "", "", "", 45 | inCollection = false 46 | )) 47 | } 48 | 49 | } 50 | 51 | val viewModel = DogListViewModel( 52 | dogRepository = FakeDogRepository() 53 | ) 54 | 55 | assertEquals(2, viewModel.dogList.value.size) 56 | assertEquals(19, viewModel.dogList.value[1].id) 57 | assert(viewModel.status.value is ApiResponseStatus.Success) 58 | } 59 | 60 | @Test 61 | fun downloadDogListErrorStatusesCorrect() { 62 | class FakeDogRepository : DogTasks { 63 | override suspend fun getDogCollection(): ApiResponseStatus> { 64 | return ApiResponseStatus.Error(messageId = 12) 65 | } 66 | 67 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 68 | return ApiResponseStatus.Success(Unit) 69 | } 70 | 71 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 72 | return ApiResponseStatus.Success(Dog( 73 | 1, 1, "", "", "", "", 74 | "", "", "", "", "", 75 | inCollection = false 76 | )) 77 | } 78 | 79 | } 80 | 81 | val viewModel = DogListViewModel( 82 | dogRepository = FakeDogRepository() 83 | ) 84 | 85 | assertEquals(0, viewModel.dogList.value.size) 86 | assert(viewModel.status.value is ApiResponseStatus.Error) 87 | } 88 | 89 | @Test 90 | fun resetStatusCorrect() { 91 | class FakeDogRepository : DogTasks { 92 | override suspend fun getDogCollection(): ApiResponseStatus> { 93 | return ApiResponseStatus.Error(messageId = 12) 94 | } 95 | 96 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus { 97 | return ApiResponseStatus.Success(Unit) 98 | } 99 | 100 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus { 101 | return ApiResponseStatus.Success(Dog( 102 | 1, 1, "", "", "", "", 103 | "", "", "", "", "", 104 | inCollection = false 105 | )) 106 | } 107 | 108 | } 109 | 110 | val viewModel = DogListViewModel( 111 | dogRepository = FakeDogRepository() 112 | ) 113 | 114 | assert(viewModel.status.value is ApiResponseStatus.Error) 115 | 116 | viewModel.resetApiResponseStatus() 117 | 118 | assert(viewModel.status.value == null) 119 | } 120 | } -------------------------------------------------------------------------------- /app/src/test/java/com/hackaprende/dogedex/viewmodel/DogedexCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.viewmodel 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | @ExperimentalCoroutinesApi 13 | class DogedexCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()): 14 | TestWatcher(), 15 | TestCoroutineScope by TestCoroutineScope(dispatcher) { 16 | override fun starting(description: Description?) { 17 | super.starting(description) 18 | Dispatchers.setMain(dispatcher) 19 | } 20 | 21 | override fun finished(description: Description?) { 22 | super.finished(description) 23 | cleanupTestCoroutines() 24 | Dispatchers.resetMain() 25 | } 26 | } -------------------------------------------------------------------------------- /auth/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /auth/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdk 33 10 | 11 | defaultConfig { 12 | minSdk 21 13 | targetSdk 33 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | buildFeatures { 33 | compose true 34 | } 35 | composeOptions { 36 | kotlinCompilerExtensionVersion compose_version 37 | } 38 | } 39 | 40 | dependencies { 41 | implementation project(path: ':core') 42 | 43 | implementation 'androidx.core:core-ktx:1.9.0' 44 | implementation 'androidx.appcompat:appcompat:1.5.1' 45 | implementation 'com.google.android.material:material:1.7.0' 46 | 47 | // Dependency Injection with Hilt 48 | implementation "com.google.dagger:hilt-android:2.42" 49 | implementation 'androidx.compose.material:material:1.3.0' 50 | kapt "com.google.dagger:hilt-compiler:2.42" 51 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 52 | 53 | // coil 54 | implementation "io.coil-kt:coil:1.4.0" 55 | implementation 'io.coil-kt:coil-compose:1.3.1' 56 | 57 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 58 | testImplementation 'junit:junit:4.13.2' 59 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" 60 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 61 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 62 | debugImplementation "androidx.compose.ui:ui-test-manifest:1.3.0" 63 | androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42' 64 | kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42' 65 | } -------------------------------------------------------------------------------- /auth/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/auth/consumer-rules.pro -------------------------------------------------------------------------------- /auth/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 -------------------------------------------------------------------------------- /auth/src/androidTest/java/com/hackaprende/dogedex/auth/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth 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("com.hackaprende.dogedex.auth.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /auth/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/AuthNavDestinations.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | object AuthNavDestinations { 4 | const val LoginScreenDestination = "login_screen" 5 | const val SignUpScreenDestination = "sign_up_screen" 6 | } -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 4 | import com.hackaprende.dogedex.core.api.ApiService 5 | import com.hackaprende.dogedex.core.api.dto.LoginDTO 6 | import com.hackaprende.dogedex.core.api.dto.SignUpDTO 7 | import com.hackaprende.dogedex.core.api.dto.UserDTOMapper 8 | import com.hackaprende.dogedex.core.api.makeNetworkCall 9 | import com.hackaprende.dogedex.core.model.User 10 | import javax.inject.Inject 11 | 12 | interface AuthTasks { 13 | suspend fun login(email: String, password: String): ApiResponseStatus 14 | suspend fun signUp( 15 | email: String, password: String, 16 | passwordConfirmation: String 17 | ): ApiResponseStatus 18 | } 19 | 20 | class AuthRepository @Inject constructor( 21 | private val apiService: ApiService, 22 | ) : AuthTasks { 23 | 24 | override suspend fun login(email: String, password: String): ApiResponseStatus = makeNetworkCall { 25 | val loginDTO = LoginDTO(email, password) 26 | val loginResponse = apiService.login(loginDTO) 27 | 28 | if (!loginResponse.isSuccess) { 29 | throw Exception(loginResponse.message) 30 | } 31 | 32 | val userDTO = loginResponse.data.user 33 | val userDTOMapper = UserDTOMapper() 34 | userDTOMapper.fromUserDTOToUserDomain(userDTO) 35 | } 36 | 37 | override suspend fun signUp( 38 | email: String, password: String, 39 | passwordConfirmation: String 40 | ): ApiResponseStatus = makeNetworkCall { 41 | val signUpDTO = SignUpDTO(email, password, passwordConfirmation) 42 | val signUpResponse = apiService.signUp(signUpDTO) 43 | 44 | if (!signUpResponse.isSuccess) { 45 | throw Exception(signUpResponse.message) 46 | } 47 | 48 | val userDTO = signUpResponse.data.user 49 | val userDTOMapper = UserDTOMapper() 50 | userDTOMapper.fromUserDTOToUserDomain(userDTO) 51 | } 52 | } -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/AuthScreen.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.hilt.navigation.compose.hiltViewModel 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.hackaprende.dogedex.core.api.ApiResponseStatus 10 | import com.hackaprende.dogedex.auth.auth.AuthNavDestinations.LoginScreenDestination 11 | import com.hackaprende.dogedex.auth.auth.AuthNavDestinations.SignUpScreenDestination 12 | import com.hackaprende.dogedex.core.composables.ErrorDialog 13 | import com.hackaprende.dogedex.core.composables.LoadingWheel 14 | import com.hackaprende.dogedex.core.model.User 15 | 16 | @Composable 17 | fun AuthScreen( 18 | onUserLoggedIn: (User) -> Unit, 19 | authViewModel: AuthViewModel = hiltViewModel(), 20 | ) { 21 | val user = authViewModel.user 22 | 23 | val userValue = user.value 24 | if (userValue != null) { 25 | onUserLoggedIn(userValue) 26 | } 27 | 28 | val navController = rememberNavController() 29 | val status = authViewModel.status.value 30 | 31 | AuthNavHost( 32 | navController = navController, 33 | onLoginButtonClick = { email, password -> authViewModel.login(email, password) }, 34 | onSignUpButtonClick = { email, password, confirmPassword -> 35 | authViewModel.signUp(email, password, confirmPassword) }, 36 | authViewModel = authViewModel, 37 | ) 38 | 39 | if (status is ApiResponseStatus.Loading) { 40 | LoadingWheel() 41 | } else if (status is ApiResponseStatus.Error) { 42 | ErrorDialog(status.messageId) { authViewModel.resetApiResponseStatus() } 43 | } 44 | } 45 | 46 | @Composable 47 | private fun AuthNavHost( 48 | navController: NavHostController, 49 | onLoginButtonClick: (String, String) -> Unit, 50 | onSignUpButtonClick: (email: String, password: String, passwordConfirmation: String) -> Unit, 51 | authViewModel: AuthViewModel, 52 | ) { 53 | NavHost( 54 | navController = navController, 55 | startDestination = LoginScreenDestination 56 | ) { 57 | composable(route = LoginScreenDestination) { 58 | LoginScreen( 59 | onLoginButtonClick = onLoginButtonClick, 60 | onRegisterButtonClick = { 61 | navController.navigate(route = SignUpScreenDestination) 62 | }, 63 | authViewModel = authViewModel, 64 | ) 65 | } 66 | 67 | composable(route = SignUpScreenDestination) { 68 | SignUpScreen( 69 | onSignUpButtonClick = onSignUpButtonClick, 70 | onNavigationIconClick = { navController.navigateUp() }, 71 | authViewModel = authViewModel, 72 | ) 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/AuthViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.hackaprende.dogedex.auth.auth.AuthTasks 7 | import com.hackaprende.dogedex.core.R 8 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 9 | import com.hackaprende.dogedex.core.model.User 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class AuthViewModel @Inject constructor( 16 | private val authRepository: AuthTasks 17 | ) : ViewModel() { 18 | 19 | var user = mutableStateOf(null) 20 | private set 21 | 22 | var emailError = mutableStateOf(null) 23 | private set 24 | 25 | var passwordError = mutableStateOf(null) 26 | private set 27 | 28 | var confirmPasswordError = mutableStateOf(null) 29 | private set 30 | 31 | var status = mutableStateOf?>(null) 32 | private set 33 | 34 | fun login(email: String, password: String) { 35 | when { 36 | email.isEmpty() -> emailError.value = R.string.email_is_not_valid 37 | password.isEmpty() -> passwordError.value = R.string.password_must_not_be_empty 38 | else -> { 39 | viewModelScope.launch { 40 | status.value = ApiResponseStatus.Loading() 41 | handleResponseStatus( 42 | authRepository.login( 43 | email, 44 | password 45 | ) 46 | ) 47 | } 48 | } 49 | } 50 | } 51 | 52 | fun signUp( 53 | email: String, password: String, 54 | passwordConfirmation: String 55 | ) { 56 | when { 57 | email.isEmpty() -> emailError.value = R.string.email_is_not_valid 58 | password.isEmpty() -> passwordError.value = R.string.password_must_not_be_empty 59 | passwordConfirmation.isEmpty() -> confirmPasswordError.value = 60 | R.string.password_must_not_be_empty 61 | password != passwordConfirmation -> { 62 | passwordError.value = R.string.passwords_do_not_match 63 | confirmPasswordError.value = R.string.passwords_do_not_match 64 | } 65 | else -> { 66 | viewModelScope.launch { 67 | status.value = ApiResponseStatus.Loading() 68 | handleResponseStatus( 69 | authRepository.signUp( 70 | email, 71 | password, passwordConfirmation 72 | ) 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | 79 | fun resetErrors() { 80 | emailError.value = null 81 | passwordError.value = null 82 | confirmPasswordError.value = null 83 | } 84 | 85 | private fun handleResponseStatus(apiResponseStatus: ApiResponseStatus) { 86 | if (apiResponseStatus is ApiResponseStatus.Success) { 87 | user.value = apiResponseStatus.data!! 88 | } 89 | 90 | status.value = apiResponseStatus 91 | } 92 | 93 | fun resetApiResponseStatus() { 94 | status.value = null 95 | } 96 | } -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/LoginActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.widget.Toast 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.ExperimentalFoundationApi 9 | import androidx.compose.material.ExperimentalMaterialApi 10 | import coil.annotation.ExperimentalCoilApi 11 | import com.hackaprende.dogedex.core.dogdetail.ui.theme.DogedexTheme 12 | import com.hackaprende.dogedex.core.model.User 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | 16 | @ExperimentalCoilApi 17 | @ExperimentalMaterialApi 18 | @ExperimentalFoundationApi 19 | @AndroidEntryPoint 20 | class LoginActivity : ComponentActivity() { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | DogedexTheme { 26 | AuthScreen( 27 | onUserLoggedIn = ::startMainActivity, 28 | ) 29 | } 30 | } 31 | } 32 | 33 | private fun startMainActivity(userValue: User) { 34 | User.setLoggedInUser(this, userValue) 35 | try { 36 | startActivity( 37 | Intent( 38 | this, 39 | Class.forName("com.hackaprende.dogedex.camera.main.MainActivity") 40 | ) 41 | ) 42 | } catch (e: ClassNotFoundException) { 43 | Toast.makeText(this, "Camera Screen error", Toast.LENGTH_SHORT).show() 44 | } 45 | finish() 46 | } 47 | } -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/LoginScreen.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.Scaffold 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.TopAppBar 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.semantics.semantics 19 | import androidx.compose.ui.semantics.testTag 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.input.PasswordVisualTransformation 22 | import androidx.compose.ui.text.style.TextAlign 23 | import androidx.compose.ui.unit.dp 24 | import com.hackaprende.dogedex.core.R 25 | import com.hackaprende.dogedex.core.composables.AuthField 26 | 27 | @Composable 28 | fun LoginScreen( 29 | onLoginButtonClick: (String, String) -> Unit, 30 | onRegisterButtonClick: () -> Unit, 31 | authViewModel: AuthViewModel, 32 | ) { 33 | Scaffold( 34 | topBar = { LoginScreenToolbar() } 35 | ) { 36 | Content( 37 | onLoginButtonClick = onLoginButtonClick, 38 | onRegisterButtonClick = { 39 | onRegisterButtonClick() 40 | authViewModel.resetErrors() 41 | }, 42 | resetFieldErrors = { authViewModel.resetErrors() }, 43 | authViewModel = authViewModel 44 | ) 45 | } 46 | } 47 | 48 | @Composable 49 | private fun Content( 50 | onLoginButtonClick: (String, String) -> Unit, 51 | onRegisterButtonClick: () -> Unit, 52 | resetFieldErrors: () -> Unit, 53 | authViewModel: AuthViewModel, 54 | ) { 55 | val email = remember { mutableStateOf("") } 56 | val password = remember { mutableStateOf("") } 57 | 58 | Column( 59 | modifier = Modifier 60 | .fillMaxWidth() 61 | .padding( 62 | top = 32.dp, 63 | start = 16.dp, 64 | end = 16.dp, 65 | bottom = 16.dp 66 | ), 67 | horizontalAlignment = Alignment.CenterHorizontally 68 | ) { 69 | AuthField( 70 | label = stringResource(id = R.string.email), 71 | modifier = Modifier 72 | .fillMaxWidth(), 73 | email = email.value, 74 | onTextChanged = { 75 | email.value = it 76 | resetFieldErrors() 77 | }, 78 | errorMessageId = authViewModel.emailError.value, 79 | errorSemantic = "email-field-error", 80 | fieldSemantic = "email-field", 81 | ) 82 | 83 | AuthField( 84 | label = stringResource(id = R.string.password), 85 | modifier = Modifier 86 | .fillMaxWidth() 87 | .padding(top = 16.dp), 88 | email = password.value, 89 | onTextChanged = { 90 | password.value = it 91 | resetFieldErrors() 92 | }, 93 | visualTransformation = PasswordVisualTransformation(), 94 | errorMessageId = authViewModel.passwordError.value, 95 | errorSemantic = "password-field-error", 96 | fieldSemantic = "password-field", 97 | ) 98 | 99 | Button(modifier = Modifier 100 | .fillMaxWidth() 101 | .padding(top = 16.dp) 102 | .semantics { testTag = "login-button" }, 103 | onClick = { onLoginButtonClick(email.value, password.value) }) { 104 | Text( 105 | stringResource(R.string.login), 106 | textAlign = TextAlign.Center, 107 | fontWeight = FontWeight.Medium 108 | ) 109 | } 110 | 111 | Text( 112 | modifier = Modifier 113 | .fillMaxWidth() 114 | .padding(16.dp), 115 | textAlign = TextAlign.Center, 116 | text = stringResource(R.string.do_not_have_an_account) 117 | ) 118 | 119 | Text( 120 | modifier = Modifier 121 | .clickable(enabled = true, onClick = { onRegisterButtonClick() }) 122 | .fillMaxWidth() 123 | .padding(16.dp) 124 | .semantics { testTag = "login-screen-register-button" }, 125 | text = stringResource(R.string.register), 126 | textAlign = TextAlign.Center, 127 | fontWeight = FontWeight.Medium 128 | ) 129 | } 130 | } 131 | 132 | @Composable 133 | fun LoginScreenToolbar() { 134 | TopAppBar( 135 | title = { Text(stringResource(R.string.app_name)) }, 136 | backgroundColor = Color.Red, 137 | contentColor = Color.White, 138 | ) 139 | } -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/auth/SignUpScreen.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.auth 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.semantics.semantics 15 | import androidx.compose.ui.semantics.testTag 16 | import androidx.compose.ui.text.font.FontWeight 17 | import androidx.compose.ui.text.input.PasswordVisualTransformation 18 | import androidx.compose.ui.text.style.TextAlign 19 | import androidx.compose.ui.unit.dp 20 | import com.hackaprende.dogedex.core.composables.AuthField 21 | import com.hackaprende.dogedex.core.composables.BackNavigationIcon 22 | import com.hackaprende.dogedex.core.R 23 | 24 | @Composable 25 | fun SignUpScreen( 26 | onSignUpButtonClick: (email: String, password: String, passwordConfirmation: String) -> Unit, 27 | onNavigationIconClick: () -> Unit, 28 | authViewModel: AuthViewModel, 29 | ) { 30 | Scaffold( 31 | topBar = { 32 | SignUpScreenToolbar { 33 | onNavigationIconClick() 34 | authViewModel.resetErrors() 35 | } 36 | } 37 | ) { 38 | Content( 39 | resetFieldErrors = { authViewModel.resetErrors() }, 40 | onSignUpButtonClick = onSignUpButtonClick, 41 | authViewModel = authViewModel, 42 | ) 43 | } 44 | } 45 | 46 | @Composable 47 | private fun Content( 48 | resetFieldErrors: () -> Unit, 49 | onSignUpButtonClick: (email: String, password: String, passwordConfirmation: String) -> Unit, 50 | authViewModel: AuthViewModel, 51 | ) { 52 | val email = remember { mutableStateOf("") } 53 | val password = remember { mutableStateOf("") } 54 | val confirmPassword = remember { mutableStateOf("") } 55 | 56 | Column( 57 | modifier = Modifier 58 | .fillMaxWidth() 59 | .padding( 60 | top = 32.dp, 61 | start = 16.dp, 62 | end = 16.dp, 63 | bottom = 16.dp 64 | ), 65 | horizontalAlignment = Alignment.CenterHorizontally 66 | ) { 67 | AuthField( 68 | label = stringResource(id = R.string.email), 69 | modifier = Modifier 70 | .fillMaxWidth(), 71 | email = email.value, 72 | onTextChanged = { 73 | email.value = it 74 | resetFieldErrors() 75 | }, 76 | errorMessageId = authViewModel.emailError.value 77 | ) 78 | 79 | AuthField( 80 | label = stringResource(id = R.string.password), 81 | modifier = Modifier 82 | .fillMaxWidth() 83 | .padding(top = 16.dp), 84 | email = password.value, 85 | onTextChanged = { 86 | password.value = it 87 | resetFieldErrors() 88 | }, 89 | visualTransformation = PasswordVisualTransformation(), 90 | errorMessageId = authViewModel.passwordError.value, 91 | ) 92 | 93 | AuthField( 94 | label = stringResource(id = R.string.confirm_password), 95 | modifier = Modifier 96 | .fillMaxWidth() 97 | .padding(top = 16.dp), 98 | email = confirmPassword.value, 99 | onTextChanged = { 100 | confirmPassword.value = it 101 | resetFieldErrors() 102 | }, 103 | visualTransformation = PasswordVisualTransformation(), 104 | errorMessageId = authViewModel.confirmPasswordError.value, 105 | ) 106 | 107 | Button(modifier = Modifier 108 | .fillMaxWidth() 109 | .padding(top = 16.dp) 110 | .semantics { testTag = "sign-up-button" }, 111 | onClick = { 112 | onSignUpButtonClick(email.value, password.value, confirmPassword.value) 113 | }) { 114 | Text( 115 | stringResource(R.string.sign_up), 116 | textAlign = TextAlign.Center, 117 | fontWeight = FontWeight.Medium 118 | ) 119 | } 120 | } 121 | } 122 | 123 | @Composable 124 | private fun SignUpScreenToolbar( 125 | onNavigationIconClick: () -> Unit 126 | ) { 127 | TopAppBar( 128 | title = { Text(stringResource(R.string.app_name)) }, 129 | backgroundColor = Color.Red, 130 | contentColor = Color.White, 131 | navigationIcon = { BackNavigationIcon { onNavigationIconClick() } } 132 | ) 133 | } 134 | 135 | -------------------------------------------------------------------------------- /auth/src/main/java/com/hackaprende/dogedex/auth/di/AuthTasksModule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth.di 2 | 3 | import com.hackaprende.dogedex.auth.auth.AuthRepository 4 | import com.hackaprende.dogedex.auth.auth.AuthTasks 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | abstract class AuthTasksModule { 13 | 14 | @Binds 15 | abstract fun bindDogTasks( 16 | authRepository: AuthRepository 17 | ): AuthTasks 18 | } -------------------------------------------------------------------------------- /auth/src/test/java/com/hackaprende/dogedex/auth/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.auth 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 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | compose_version = '1.3.0' 5 | } 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.1.3' 12 | classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10' 13 | classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.5.3") 14 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42' 15 | 16 | // NOTE: Do not place your application dependencies here; they belong 17 | // in the individual module build.gradle files 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } -------------------------------------------------------------------------------- /camera/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /camera/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdk 33 10 | 11 | defaultConfig { 12 | minSdk 21 13 | targetSdk 33 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | consumerProguardFiles "consumer-rules.pro" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | buildFeatures { 30 | dataBinding true 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | } 35 | } 36 | 37 | dependencies { 38 | 39 | implementation 'androidx.core:core-ktx:1.9.0' 40 | implementation 'androidx.appcompat:appcompat:1.5.1' 41 | implementation 'com.google.android.material:material:1.7.0' 42 | implementation 'androidx.compose.foundation:foundation:1.3.0' 43 | implementation 'androidx.compose.material:material:1.3.0' 44 | testImplementation 'junit:junit:4.13.2' 45 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 46 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 47 | 48 | // Dependency Injection with Hilt 49 | implementation "com.google.dagger:hilt-android:2.42" 50 | kapt "com.google.dagger:hilt-compiler:2.42" 51 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 52 | 53 | //CameraX 54 | implementation "androidx.camera:camera-camera2:1.2.0-rc01" 55 | implementation "androidx.camera:camera-lifecycle:1.2.0-rc01" 56 | implementation "androidx.camera:camera-view:1.2.0-rc01" 57 | 58 | // TensorFlow Lite 59 | implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly-SNAPSHOT' 60 | implementation 'org.tensorflow:tensorflow-lite-support:0.1.0' 61 | 62 | // Coil 63 | implementation 'io.coil-kt:coil-compose:1.3.1' 64 | 65 | implementation project(path: ':core') 66 | } -------------------------------------------------------------------------------- /camera/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/camera/consumer-rules.pro -------------------------------------------------------------------------------- /camera/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 -------------------------------------------------------------------------------- /camera/src/androidTest/java/com/hackaprende/dogedex/camera/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera 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("com.hackaprende.dogedex.camera.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /camera/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera 2 | 3 | const val MAX_RECOGNITION_DOG_RESULTS = 5 4 | const val MODEL_PATH = "model.tflite" 5 | const val LABEL_PATH = "labels.txt" -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/di/ClassifierConstructorModule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.di 2 | 3 | import android.content.Context 4 | import com.hackaprende.dogedex.camera.LABEL_PATH 5 | import com.hackaprende.dogedex.camera.MODEL_PATH 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.qualifiers.ApplicationContext 10 | import dagger.hilt.components.SingletonComponent 11 | import org.tensorflow.lite.support.common.FileUtil 12 | import java.nio.MappedByteBuffer 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object ClassifierConstructorModule { 17 | 18 | @Provides 19 | fun providesClassifierModel(@ApplicationContext context: Context): MappedByteBuffer = 20 | FileUtil.loadMappedFile(context, MODEL_PATH) 21 | 22 | @Provides 23 | fun providesClassifierLabels(@ApplicationContext context: Context): List = 24 | FileUtil.loadLabels(context, LABEL_PATH) 25 | } -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/di/ClassifierModule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.di 2 | 3 | import com.hackaprende.dogedex.camera.machinelearning.ClassifierRepository 4 | import com.hackaprende.dogedex.camera.machinelearning.ClassifierTasks 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | abstract class ClassifierModule { 13 | @Binds 14 | abstract fun bindClassifierTasks( 15 | classifierRepository: ClassifierRepository 16 | ): ClassifierTasks 17 | } 18 | -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/machinelearning/Classifier.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.machinelearning 2 | 3 | import android.graphics.Bitmap 4 | import com.hackaprende.dogedex.camera.MAX_RECOGNITION_DOG_RESULTS 5 | import org.tensorflow.lite.Interpreter 6 | import org.tensorflow.lite.support.common.TensorProcessor 7 | import org.tensorflow.lite.support.common.ops.DequantizeOp 8 | import org.tensorflow.lite.support.image.ImageProcessor 9 | import org.tensorflow.lite.support.image.TensorImage 10 | import org.tensorflow.lite.support.image.ops.ResizeOp 11 | import org.tensorflow.lite.support.label.TensorLabel 12 | import org.tensorflow.lite.support.tensorbuffer.TensorBuffer 13 | import java.nio.MappedByteBuffer 14 | import java.util.* 15 | import javax.inject.Inject 16 | 17 | class Classifier @Inject constructor(tfLiteModel: MappedByteBuffer, 18 | private val labels: List) { 19 | /** 20 | * Image size along the x axis. 21 | */ 22 | private val imageSizeX: Int 23 | 24 | /** 25 | * Image size along the y axis. 26 | */ 27 | private val imageSizeY: Int 28 | 29 | /** Optional GPU delegate for acceleration. */ 30 | /** 31 | * An instance of the driver class to run model inference with Tensorflow Lite. 32 | */ 33 | private var tfLite: Interpreter 34 | 35 | /** 36 | * Input image TensorBuffer. 37 | */ 38 | private var inputImageBuffer: TensorImage 39 | 40 | /** 41 | * Output probability TensorBuffer. 42 | */ 43 | private val outputProbabilityBuffer: TensorBuffer 44 | 45 | /** 46 | * Processor to apply post processing of the output probability. 47 | */ 48 | private val tensorProcessor: TensorProcessor 49 | 50 | init { 51 | val tfLiteOptions = Interpreter.Options() 52 | tfLite = Interpreter(tfLiteModel, tfLiteOptions) 53 | 54 | // Reads type and shape of input and output tensors, respectively. 55 | val imageTensorIndex = 0 56 | val imageShape = tfLite.getInputTensor(imageTensorIndex).shape() // [1 224 224 3] 57 | imageSizeY = imageShape[1] 58 | imageSizeX = imageShape[2] 59 | val imageDataType = tfLite.getInputTensor(imageTensorIndex).dataType() 60 | val probabilityTensorIndex = 0 61 | val probabilityShape = 62 | tfLite.getOutputTensor(probabilityTensorIndex).shape() // {1, NUM_CLASSES} 63 | val probabilityDataType = tfLite.getOutputTensor(probabilityTensorIndex).dataType() 64 | 65 | // Creates the input tensor. 66 | inputImageBuffer = TensorImage(imageDataType) 67 | 68 | // Creates the output tensor and its processor. 69 | outputProbabilityBuffer = TensorBuffer.createFixedSize( 70 | probabilityShape, 71 | probabilityDataType 72 | ) 73 | 74 | // Creates the post processor for the output probability. 75 | tensorProcessor = TensorProcessor.Builder().add(DequantizeOp(0f, 1 / 255.0f)).build() 76 | } 77 | 78 | /** 79 | * Runs inference and returns the classification results. 80 | */ 81 | fun recognizeImage(bitmap: Bitmap): List { 82 | inputImageBuffer = loadImage(bitmap) 83 | val rewoundOutputBuffer = outputProbabilityBuffer.buffer.rewind() 84 | tfLite.run(inputImageBuffer.buffer, rewoundOutputBuffer) 85 | // Gets the map of label and probability. 86 | val labeledProbability = 87 | TensorLabel(labels, tensorProcessor.process(outputProbabilityBuffer)).mapWithFloatValue 88 | 89 | // Gets top-k results. 90 | return getTopKProbability(labeledProbability) 91 | } 92 | 93 | /** 94 | * Loads input image, and applies pre processing. 95 | */ 96 | private fun loadImage(bitmap: Bitmap): TensorImage { 97 | // Loads bitmap into a TensorImage. 98 | inputImageBuffer.load(bitmap) 99 | 100 | // Creates processor for the TensorImage. 101 | val imageProcessor = ImageProcessor.Builder() 102 | .add(ResizeOp(imageSizeX, imageSizeY, ResizeOp.ResizeMethod.NEAREST_NEIGHBOR)) 103 | .build() 104 | return imageProcessor.process(inputImageBuffer) 105 | } 106 | 107 | /** 108 | * Gets the top-k results. 109 | */ 110 | private fun getTopKProbability(labelProb: Map): List { 111 | // Find the best classifications. 112 | val priorityQueue = 113 | PriorityQueue(MAX_RECOGNITION_DOG_RESULTS) { 114 | lhs: DogRecognition, rhs: DogRecognition -> 115 | (rhs.confidence).compareTo(lhs.confidence) 116 | } 117 | 118 | for ((key, value) in labelProb) { 119 | priorityQueue.add(DogRecognition(key, value * 100.0f)) 120 | } 121 | 122 | val recognitions = mutableListOf() 123 | val recognitionsSize = minOf(priorityQueue.size, MAX_RECOGNITION_DOG_RESULTS) 124 | for (i in 0 until recognitionsSize) { 125 | recognitions.add(priorityQueue.poll()!!) 126 | } 127 | 128 | return recognitions 129 | } 130 | } -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/machinelearning/ClassifierRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.machinelearning 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.* 5 | import androidx.camera.core.ImageProxy 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.withContext 8 | import java.io.ByteArrayOutputStream 9 | import javax.inject.Inject 10 | 11 | interface ClassifierTasks { 12 | suspend fun recognizeImage(imageProxy: ImageProxy): List 13 | } 14 | 15 | class ClassifierRepository @Inject constructor(private val classifier: Classifier) : 16 | ClassifierTasks { 17 | 18 | override suspend fun recognizeImage(imageProxy: ImageProxy): List = 19 | withContext(Dispatchers.IO) { 20 | val bitmap = convertImageProxyToBitmap(imageProxy) 21 | if (bitmap == null) { 22 | listOf(DogRecognition("", 0f)) 23 | } else { 24 | classifier.recognizeImage(bitmap).subList(0, 5) 25 | } 26 | } 27 | 28 | @SuppressLint("UnsafeOptInUsageError") 29 | private fun convertImageProxyToBitmap(imageProxy: ImageProxy): Bitmap? { 30 | val image = imageProxy.image ?: return null 31 | 32 | val yBuffer = image.planes[0].buffer // Y 33 | val uBuffer = image.planes[1].buffer // U 34 | val vBuffer = image.planes[2].buffer // V 35 | 36 | val ySize = yBuffer.remaining() 37 | val uSize = uBuffer.remaining() 38 | val vSize = vBuffer.remaining() 39 | 40 | val nv21 = ByteArray(ySize + uSize + vSize) 41 | 42 | //U and V are swapped 43 | yBuffer.get(nv21, 0, ySize) 44 | vBuffer.get(nv21, ySize, vSize) 45 | uBuffer.get(nv21, ySize + vSize, uSize) 46 | 47 | val yuvImage = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null) 48 | val out = ByteArrayOutputStream() 49 | yuvImage.compressToJpeg( 50 | Rect(0, 0, yuvImage.width, yuvImage.height), 100, 51 | out 52 | ) 53 | val imageBytes = out.toByteArray() 54 | 55 | return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) 56 | } 57 | } -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/machinelearning/DogRecognition.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.machinelearning 2 | 3 | data class DogRecognition(val id: String, 4 | val confidence: Float) -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/main/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.main 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.os.Build 7 | import android.os.Bundle 8 | import android.view.View 9 | import android.widget.Toast 10 | import androidx.activity.result.contract.ActivityResultContracts 11 | import androidx.activity.viewModels 12 | import androidx.appcompat.app.AlertDialog 13 | import androidx.appcompat.app.AppCompatActivity 14 | import androidx.camera.core.CameraSelector 15 | import androidx.camera.core.ImageAnalysis 16 | import androidx.camera.core.ImageCapture 17 | import androidx.camera.core.Preview 18 | import androidx.camera.lifecycle.ProcessCameraProvider 19 | import androidx.compose.foundation.ExperimentalFoundationApi 20 | import androidx.compose.material.ExperimentalMaterialApi 21 | import androidx.core.content.ContextCompat 22 | import coil.annotation.ExperimentalCoilApi 23 | import com.hackaprende.dogedex.camera.R 24 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 25 | import com.hackaprende.dogedex.core.api.ApiServiceInterceptor 26 | import com.hackaprende.dogedex.camera.databinding.ActivityMainBinding 27 | import com.hackaprende.dogedex.core.dogdetail.DogDetailComposeActivity 28 | import com.hackaprende.dogedex.core.doglist.DogListActivity 29 | import com.hackaprende.dogedex.camera.machinelearning.DogRecognition 30 | import com.hackaprende.dogedex.core.model.Dog 31 | import com.hackaprende.dogedex.core.model.User 32 | import com.hackaprende.dogedex.core.testutils.EspressoIdlingResource 33 | import com.hackaprende.dogedex.core.settings.SettingsActivity 34 | import dagger.hilt.android.AndroidEntryPoint 35 | import java.util.concurrent.ExecutorService 36 | import java.util.concurrent.Executors 37 | 38 | @ExperimentalFoundationApi 39 | @ExperimentalMaterialApi 40 | @ExperimentalCoilApi 41 | @AndroidEntryPoint 42 | class MainActivity : AppCompatActivity() { 43 | private val requestPermissionLauncher = 44 | registerForActivityResult( 45 | ActivityResultContracts.RequestPermission() 46 | ) { isGranted: Boolean -> 47 | if (isGranted) { 48 | setupCamera() 49 | } else { 50 | Toast.makeText(this, R.string.camera_permission_rejected_message, 51 | Toast.LENGTH_SHORT).show() 52 | } 53 | } 54 | 55 | private lateinit var binding: ActivityMainBinding 56 | private lateinit var imageCapture: ImageCapture 57 | private lateinit var cameraExecutor: ExecutorService 58 | private var isCameraReady = false 59 | private val viewModel: MainViewModel by viewModels() 60 | 61 | override fun onCreate(savedInstanceState: Bundle?) { 62 | super.onCreate(savedInstanceState) 63 | binding = ActivityMainBinding.inflate(layoutInflater) 64 | setContentView(binding.root) 65 | 66 | val user = User.getLoggedInUser(this) 67 | if (user == null) { 68 | openLoginActivity() 69 | return 70 | } else { 71 | ApiServiceInterceptor.setSessionToken(user.authenticationToken) 72 | } 73 | 74 | binding.settingsFab.setOnClickListener { 75 | openSettingsActivity() 76 | } 77 | 78 | binding.dogListFab.setOnClickListener { 79 | openDogListActivity() 80 | } 81 | 82 | viewModel.status.observe(this) { 83 | status -> 84 | 85 | when(status) { 86 | is ApiResponseStatus.Error -> { 87 | binding.loadingWheel.visibility = View.GONE 88 | Toast.makeText(this, status.messageId, Toast.LENGTH_SHORT).show() 89 | } 90 | is ApiResponseStatus.Loading -> binding.loadingWheel.visibility = View.VISIBLE 91 | is ApiResponseStatus.Success -> binding.loadingWheel.visibility = View.GONE 92 | } 93 | } 94 | 95 | viewModel.dog.observe(this) { 96 | dog -> 97 | if (dog != null) { 98 | openDogDetailActivity(dog) 99 | } 100 | } 101 | 102 | viewModel.dogRecognition.observe(this) { 103 | enabledTakePhotoButton(it) 104 | } 105 | 106 | requestCameraPermission() 107 | } 108 | 109 | private fun openDogDetailActivity(dog: Dog) { 110 | val intent = Intent(this, DogDetailComposeActivity::class.java) 111 | intent.putExtra(DogDetailComposeActivity.DOG_KEY, dog) 112 | intent.putExtra(DogDetailComposeActivity.MOST_PROBABLE_DOGS_IDS, 113 | ArrayList(viewModel.probableDogIds)) 114 | intent.putExtra(DogDetailComposeActivity.IS_RECOGNITION_KEY, true) 115 | startActivity(intent) 116 | } 117 | 118 | override fun onDestroy() { 119 | super.onDestroy() 120 | if (::cameraExecutor.isInitialized) { 121 | cameraExecutor.shutdown() 122 | } 123 | } 124 | 125 | private fun setupCamera() { 126 | binding.cameraPreview.post { 127 | imageCapture = ImageCapture.Builder() 128 | .setTargetRotation(binding.cameraPreview.display.rotation) 129 | .build() 130 | cameraExecutor = Executors.newSingleThreadExecutor() 131 | startCamera() 132 | isCameraReady = true 133 | } 134 | } 135 | 136 | private fun requestCameraPermission() { 137 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 138 | when { 139 | ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA 140 | ) == PackageManager.PERMISSION_GRANTED -> { 141 | setupCamera() 142 | } 143 | shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { 144 | AlertDialog.Builder(this) 145 | .setTitle(R.string.camera_permission_rationale_dialog_title) 146 | .setMessage(R.string.camera_permission_rationale_dialog_message) 147 | .setPositiveButton(android.R.string.ok) { _, _ -> 148 | requestPermissionLauncher.launch( 149 | Manifest.permission.CAMERA 150 | ) 151 | } 152 | .setNegativeButton(android.R.string.cancel) { _, _ -> 153 | }.show() 154 | } 155 | else -> { 156 | requestPermissionLauncher.launch( 157 | Manifest.permission.CAMERA 158 | ) 159 | } 160 | } 161 | } else { 162 | setupCamera() 163 | } 164 | } 165 | 166 | private fun startCamera() { 167 | val cameraProviderFuture = 168 | ProcessCameraProvider.getInstance(this) 169 | 170 | EspressoIdlingResource.increment() 171 | cameraProviderFuture.addListener({ 172 | // Used to bind the lifecycle of cameras to the lifecycle owner 173 | val cameraProvider = cameraProviderFuture.get() 174 | // Preview 175 | val preview = Preview.Builder().build() 176 | preview.setSurfaceProvider(binding.cameraPreview.surfaceProvider) 177 | 178 | // Select back camera as a default 179 | val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA 180 | 181 | val imageAnalysis = ImageAnalysis.Builder() 182 | .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) 183 | .build() 184 | imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy -> 185 | EspressoIdlingResource.decrement() 186 | viewModel.recognizeImage(imageProxy) 187 | } 188 | 189 | // Bind use cases to camera 190 | cameraProvider.bindToLifecycle( 191 | this, cameraSelector, 192 | preview, imageCapture, imageAnalysis 193 | ) 194 | }, ContextCompat.getMainExecutor(this)) 195 | } 196 | 197 | private fun enabledTakePhotoButton(dogRecognition: DogRecognition) { 198 | if (dogRecognition.confidence > 70.0) { 199 | binding.takePhotoFab.alpha = 1f 200 | binding.takePhotoFab.setOnClickListener { 201 | viewModel.getDogByMlId(dogRecognition.id) 202 | } 203 | } else { 204 | binding.takePhotoFab.alpha = 0.2f 205 | binding.takePhotoFab.setOnClickListener(null) 206 | } 207 | } 208 | 209 | private fun openDogListActivity() { 210 | startActivity(Intent(this, DogListActivity::class.java)) 211 | } 212 | 213 | private fun openSettingsActivity() { 214 | startActivity(Intent(this, SettingsActivity::class.java)) 215 | } 216 | 217 | private fun openLoginActivity() { 218 | try { 219 | startActivity( 220 | Intent( 221 | this, 222 | Class.forName("com.hackaprende.dogedex.auth.auth.LoginActivity") 223 | ) 224 | ) 225 | } catch (e: ClassNotFoundException) { 226 | Toast.makeText(this, 227 | getString(com.hackaprende.dogedex.core.R.string.login_screen_error), Toast.LENGTH_SHORT).show() 228 | } 229 | finish() 230 | } 231 | } -------------------------------------------------------------------------------- /camera/src/main/java/com/hackaprende/dogedex/camera/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera.main 2 | 3 | import androidx.camera.core.ImageProxy 4 | import androidx.lifecycle.LiveData 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 9 | import com.hackaprende.dogedex.camera.machinelearning.ClassifierTasks 10 | import com.hackaprende.dogedex.camera.machinelearning.DogRecognition 11 | import com.hackaprende.dogedex.core.doglist.DogTasks 12 | import com.hackaprende.dogedex.core.model.Dog 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.launch 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class MainViewModel @Inject constructor( 19 | private val dogRepository: DogTasks, 20 | private val classifierRepository: ClassifierTasks, 21 | ): ViewModel() { 22 | 23 | private val _dog = MutableLiveData() 24 | val dog: LiveData 25 | get() = _dog 26 | 27 | private val _status = MutableLiveData>() 28 | val status: LiveData> 29 | get() = _status 30 | 31 | private val _dogRecognition = MutableLiveData() 32 | val dogRecognition: LiveData 33 | get() = _dogRecognition 34 | 35 | val probableDogIds = mutableListOf() 36 | 37 | fun recognizeImage(imageProxy: ImageProxy) { 38 | viewModelScope.launch { 39 | val dogRecognitionList = classifierRepository.recognizeImage(imageProxy) 40 | updateDogRecognition(dogRecognitionList) 41 | updateProbableDogIds(dogRecognitionList) 42 | imageProxy.close() 43 | } 44 | } 45 | 46 | private fun updateProbableDogIds(dogRecognitionList: List) { 47 | probableDogIds.clear() 48 | if (dogRecognitionList.size >= 5) { 49 | probableDogIds 50 | .addAll(dogRecognitionList.subList(1, 4) 51 | .map { 52 | it.id 53 | }) 54 | } 55 | } 56 | 57 | private fun updateDogRecognition(dogRecognitionList: List) { 58 | _dogRecognition.value = dogRecognitionList.first() 59 | } 60 | 61 | fun getDogByMlId(mlDogId: String) { 62 | viewModelScope.launch { 63 | _status.value = ApiResponseStatus.Loading() 64 | handleResponseStatus(dogRepository.getDogByMlId(mlDogId)) 65 | } 66 | } 67 | 68 | private fun handleResponseStatus(apiResponseStatus: ApiResponseStatus) { 69 | if (apiResponseStatus is ApiResponseStatus.Success) { 70 | _dog.value = apiResponseStatus.data!! 71 | } 72 | 73 | _status.value = apiResponseStatus 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /camera/src/main/res/drawable/ic_baseline_list.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /camera/src/main/res/drawable/ic_baseline_photo_camera.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /camera/src/main/res/drawable/ic_baseline_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /camera/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 28 | 29 | 38 | 39 | 48 | 49 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /camera/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Acepta la camara o me da amsiedad 4 | You need to accept camera permission to use camera 5 | Aceptame por favor 6 | This is a content description 7 | -------------------------------------------------------------------------------- /camera/src/test/java/com/hackaprende/dogedex/camera/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.camera 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 | } -------------------------------------------------------------------------------- /core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'kotlin-parcelize' 6 | id 'dagger.hilt.android.plugin' 7 | } 8 | 9 | android { 10 | compileSdk 33 11 | 12 | defaultConfig { 13 | minSdk 21 14 | targetSdk 33 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | buildFeatures { 34 | dataBinding true 35 | compose true 36 | } 37 | composeOptions { 38 | kotlinCompilerExtensionVersion compose_version 39 | } 40 | } 41 | 42 | dependencies { 43 | 44 | implementation 'androidx.core:core-ktx:1.9.0' 45 | implementation 'androidx.appcompat:appcompat:1.5.1' 46 | implementation 'com.google.android.material:material:1.7.0' 47 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4' 48 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" 49 | implementation "androidx.activity:activity-ktx:1.6.1" 50 | implementation "io.coil-kt:coil:1.4.0" 51 | 52 | implementation "androidx.navigation:navigation-fragment-ktx:2.5.3" 53 | implementation "androidx.navigation:navigation-ui-ktx:2.5.3" 54 | 55 | //CameraX 56 | implementation "androidx.camera:camera-camera2:1.2.0-rc01" 57 | implementation "androidx.camera:camera-lifecycle:1.2.0-rc01" 58 | implementation "androidx.camera:camera-view:1.2.0-rc01" 59 | 60 | // TensorFlow Lite 61 | implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly-SNAPSHOT' 62 | implementation 'org.tensorflow:tensorflow-lite-support:0.1.0' 63 | 64 | // Jetpack Compose 65 | implementation 'androidx.compose.ui:ui:1.3.0' 66 | // Tooling support (Previews, etc.) 67 | implementation 'androidx.compose.ui:ui-tooling:1.3.0' 68 | // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.) 69 | implementation 'androidx.compose.foundation:foundation:1.3.0' 70 | // Material Design 71 | implementation 'androidx.compose.material:material:1.3.0' 72 | // Material design icons 73 | implementation 'androidx.compose.material:material-icons-core:1.3.0' 74 | implementation 'androidx.compose.material:material-icons-extended:1.3.0' 75 | // Integration with activities 76 | implementation 'androidx.activity:activity-compose:1.6.1' 77 | // Integration with ViewModels 78 | implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1' 79 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 80 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1' 81 | implementation "androidx.navigation:navigation-compose:2.5.3" 82 | 83 | // Coil 84 | implementation 'io.coil-kt:coil-compose:1.3.1' 85 | 86 | // UI Tests 87 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.3.0' 88 | 89 | // Dependency Injection with Hilt 90 | implementation "com.google.dagger:hilt-android:2.42" 91 | kapt "com.google.dagger:hilt-compiler:2.42" 92 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 93 | 94 | // Espresso idling resources 95 | implementation 'androidx.test.espresso:espresso-idling-resource:3.4.0' 96 | 97 | // Retrofit y Moshi 98 | implementation "com.squareup.retrofit2:retrofit:2.9.0" 99 | implementation "com.squareup.retrofit2:converter-moshi:2.9.0" 100 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 101 | testImplementation 'junit:junit:4.13.2' 102 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4" 103 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 104 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 105 | debugImplementation "androidx.compose.ui:ui-test-manifest:1.3.0" 106 | androidTestImplementation 'com.google.dagger:hilt-android-testing:2.42' 107 | kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.42' 108 | } -------------------------------------------------------------------------------- /core/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hackaprende/dogedex-kotlin/97a4d7fcff9aeaafe85b4f708c5fef984c8fa30f/core/consumer-rules.pro -------------------------------------------------------------------------------- /core/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 -------------------------------------------------------------------------------- /core/src/androidTest/java/com/hackaprende/dogedex/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core 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("com.hackaprende.dogedex.core.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/Contants.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core 2 | 3 | const val BASE_URL = "https://todogs.herokuapp.com/api/v1/" 4 | const val GET_ALL_DOGS_URL = "dogs" 5 | const val SIGN_UP_URL = "sign_up" 6 | const val SIGN_IN_URL = "sign_in" 7 | const val ADD_DOG_TO_USER_URL = "add_dog_to_user" 8 | const val GET_USER_DOGS_URL = "get_user_dogs" 9 | const val GET_DOG_BY_ML_ID = "find_dog_by_ml_id" -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex 2 | 3 | import android.util.Patterns 4 | 5 | fun isValidEmail(email: String?): Boolean { 6 | return !email.isNullOrEmpty() && 7 | Patterns.EMAIL_ADDRESS.matcher(email).matches() 8 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/WholeImageActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core 2 | 3 | import android.net.Uri 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.widget.Toast 7 | import coil.load 8 | import com.hackaprende.dogedex.core.databinding.ActivityWholeImageBinding 9 | import java.io.File 10 | 11 | class WholeImageActivity : AppCompatActivity() { 12 | companion object { 13 | const val PHOTO_URI_KEY = "photo_uri" 14 | } 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | val binding = ActivityWholeImageBinding.inflate(layoutInflater) 19 | setContentView(binding.root) 20 | 21 | val photoUri = intent.extras?.getString(PHOTO_URI_KEY) 22 | val uri = Uri.parse(photoUri) 23 | 24 | val path = uri.path 25 | if (path == null) { 26 | Toast.makeText(this, "Error showing image no photo uri", 27 | Toast.LENGTH_SHORT).show() 28 | finish() 29 | return 30 | } 31 | 32 | binding.wholeImage.load(File(path)) 33 | } 34 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/ApiResponseStatus.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api 2 | 3 | sealed class ApiResponseStatus { 4 | class Success(val data: T): ApiResponseStatus() 5 | class Loading: ApiResponseStatus() 6 | class Error(val messageId: Int): ApiResponseStatus() 7 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/ApiService.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api 2 | 3 | import com.hackaprende.dogedex.core.* 4 | import com.hackaprende.dogedex.core.api.dto.AddDogToUserDTO 5 | import com.hackaprende.dogedex.core.api.dto.LoginDTO 6 | import com.hackaprende.dogedex.core.api.dto.SignUpDTO 7 | import com.hackaprende.dogedex.core.api.responses.AuthApiResponse 8 | import com.hackaprende.dogedex.core.api.responses.DefaultResponse 9 | import com.hackaprende.dogedex.core.api.responses.DogApiResponse 10 | import com.hackaprende.dogedex.core.api.responses.DogListApiResponse 11 | import retrofit2.http.* 12 | 13 | interface ApiService { 14 | @GET(GET_ALL_DOGS_URL) 15 | suspend fun getAllDogs(): DogListApiResponse 16 | 17 | @POST(SIGN_UP_URL) 18 | suspend fun signUp(@Body signUpDTO: SignUpDTO): AuthApiResponse 19 | 20 | @POST(SIGN_IN_URL) 21 | suspend fun login(@Body loginDTO: LoginDTO): AuthApiResponse 22 | 23 | @Headers("${ApiServiceInterceptor.NEEDS_AUTH_HEADER_KEY}: true") 24 | @POST(ADD_DOG_TO_USER_URL) 25 | suspend fun addDogToUser(@Body addDogToUserDTO: AddDogToUserDTO): DefaultResponse 26 | 27 | @Headers("${ApiServiceInterceptor.NEEDS_AUTH_HEADER_KEY}: true") 28 | @GET(GET_USER_DOGS_URL) 29 | suspend fun getUserDogs(): DogListApiResponse 30 | 31 | @GET(GET_DOG_BY_ML_ID) 32 | suspend fun getDogByMlId(@Query("ml_id") mlId: String): DogApiResponse 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/ApiServiceInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api 2 | 3 | import okhttp3.Interceptor 4 | import okhttp3.Response 5 | 6 | object ApiServiceInterceptor : Interceptor { 7 | const val NEEDS_AUTH_HEADER_KEY = "needs_authentication" 8 | 9 | private var sessionToken: String? = null 10 | 11 | fun setSessionToken(sessionToken: String) { 12 | this.sessionToken = sessionToken 13 | } 14 | 15 | override fun intercept(chain: Interceptor.Chain): Response { 16 | val request = chain.request() 17 | val requestBuilder = request.newBuilder() 18 | if (request.header(NEEDS_AUTH_HEADER_KEY) != null) { 19 | // needs credentials 20 | if (sessionToken == null) { 21 | throw RuntimeException("Need to be authenticated to perform this request") 22 | } else { 23 | requestBuilder.addHeader("AUTH-TOKEN", sessionToken!!) 24 | } 25 | } 26 | return chain.proceed(requestBuilder.build()) 27 | } 28 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/AddDogToUserDTO.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class AddDogToUserDTO(@field:Json(name = "dog_id") val dogId: Long) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/DogDTO.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class DogDTO( 6 | val id: Long, 7 | val index: Int, 8 | @field:Json(name = "name_en") val name: String, 9 | @field:Json(name = "dog_type") val type: String, 10 | @field:Json(name = "height_female") val heightFemale: String, 11 | @field:Json(name = "height_male") val heightMale: String, 12 | @field:Json(name = "image_url") val imageUrl: String, 13 | @field:Json(name = "life_expectancy") val lifeExpectancy: String, 14 | val temperament: String, 15 | @field:Json(name = "weight_female") val weightFemale: String, 16 | @field:Json(name = "weight_male") val weightMale: String 17 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/DogDTOMapper.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | import com.hackaprende.dogedex.core.model.Dog 4 | 5 | class DogDTOMapper { 6 | 7 | fun fromDogDTOToDogDomain(dogDTO: DogDTO): Dog { 8 | return Dog( 9 | dogDTO.id, dogDTO.index, dogDTO.name, dogDTO.type, 10 | dogDTO.heightFemale, dogDTO.heightMale, dogDTO.imageUrl, dogDTO.lifeExpectancy, 11 | dogDTO.temperament, dogDTO.weightFemale, dogDTO.weightMale 12 | ) 13 | } 14 | 15 | fun fromDogDTOListToDogDomainList(dogDTOList: List): List { 16 | return dogDTOList.map { fromDogDTOToDogDomain(it) } 17 | } 18 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/LoginDTO.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | class LoginDTO( 4 | val email: String, 5 | val password: String, 6 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/SignUpDTO.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class SignUpDTO( 6 | val email: String, 7 | val password: String, 8 | @field:Json(name = "password_confirmation") val passwordConfirmation: String 9 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/UserDTO.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class UserDTO( 6 | val id: Long, 7 | val email: String, 8 | @field:Json(name = "authentication_token") val authenticationToken: String, 9 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/dto/UserDTOMapper.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.dto 2 | 3 | import com.hackaprende.dogedex.core.model.User 4 | 5 | class UserDTOMapper { 6 | fun fromUserDTOToUserDomain(userDTO: UserDTO) = 7 | User(userDTO.id, userDTO.email, userDTO.authenticationToken) 8 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/makeNetworkCall.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api 2 | 3 | import com.hackaprende.dogedex.core.R 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.withContext 6 | import retrofit2.HttpException 7 | import java.lang.Exception 8 | import java.net.UnknownHostException 9 | 10 | private const val UNAUTHORIZED_ERROR_CODE = 401 11 | 12 | suspend fun makeNetworkCall( 13 | call: suspend () -> T 14 | ): ApiResponseStatus = withContext(Dispatchers.IO) { 15 | try { 16 | ApiResponseStatus.Success(call()) 17 | } catch (e: UnknownHostException) { 18 | ApiResponseStatus.Error(R.string.unknown_host_exception_error) 19 | } catch (e: HttpException) { 20 | val errorMessage = if (e.code() == UNAUTHORIZED_ERROR_CODE) { 21 | R.string.wrong_user_or_password 22 | } else { 23 | R.string.unknown_error 24 | } 25 | ApiResponseStatus.Error(errorMessage) 26 | } catch (e: Exception) { 27 | val errorMessage = when(e.message) { 28 | "sign_up_error" -> R.string.error_sign_up 29 | "sign_in_error" -> R.string.error_sign_in 30 | "user_already_exists" -> R.string.user_already_exists 31 | "error_adding_dog" -> R.string.error_adding_dog 32 | else -> R.string.unknown_error 33 | } 34 | ApiResponseStatus.Error(errorMessage) 35 | } 36 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/AuthApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class AuthApiResponse( 6 | val message: String, 7 | @field:Json(name = "is_success") val isSuccess: Boolean, 8 | val data: UserResponse, 9 | ) 10 | -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/DefaultResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class DefaultResponse( 6 | val message: String, 7 | @field:Json(name = "is_success") val isSuccess: Boolean, 8 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/DogApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class DogApiResponse( 6 | val message: String, 7 | @field:Json(name = "is_success") val isSuccess: Boolean, 8 | val data: DogResponse, 9 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/DogListApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.squareup.moshi.Json 4 | 5 | class DogListApiResponse( 6 | val message: String, 7 | @field:Json(name = "is_success") val isSuccess: Boolean, 8 | val data: DogListResponse, 9 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/DogListResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.hackaprende.dogedex.core.api.dto.DogDTO 4 | 5 | class DogListResponse(val dogs: List) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/DogResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.hackaprende.dogedex.core.api.dto.DogDTO 4 | 5 | class DogResponse(val dog: DogDTO) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/LoginApiResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | class LoginApiResponse { 4 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/api/responses/UserResponse.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.api.responses 2 | 3 | import com.hackaprende.dogedex.core.api.dto.UserDTO 4 | 5 | class UserResponse(val user: UserDTO) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/composables/AuthField.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.composables 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material.OutlinedTextField 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.res.stringResource 10 | import androidx.compose.ui.semantics.semantics 11 | import androidx.compose.ui.semantics.testTag 12 | import androidx.compose.ui.text.input.VisualTransformation 13 | 14 | @Composable 15 | fun AuthField( 16 | label: String, 17 | email: String, 18 | onTextChanged: (String) -> Unit, 19 | modifier: Modifier = Modifier, 20 | errorSemantic: String = "", 21 | fieldSemantic: String = "", 22 | errorMessageId: Int? = null, 23 | visualTransformation: VisualTransformation = VisualTransformation.None, 24 | ) { 25 | Column(modifier = modifier,) { 26 | if (errorMessageId != null) { 27 | Text( 28 | modifier = Modifier 29 | .fillMaxWidth() 30 | .semantics { testTag = errorSemantic }, 31 | text = stringResource(id = errorMessageId) 32 | ) 33 | } 34 | OutlinedTextField( 35 | modifier = Modifier.fillMaxWidth() 36 | .semantics { testTag = fieldSemantic }, 37 | label = { Text(label) }, 38 | value = email, 39 | onValueChange = { onTextChanged(it) }, 40 | visualTransformation = visualTransformation, 41 | isError = errorMessageId != null 42 | ) 43 | } 44 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/composables/BackNavigationIcon.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.composables 2 | 3 | import androidx.compose.material.Icon 4 | import androidx.compose.material.IconButton 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.sharp.ArrowBack 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 9 | 10 | @Composable 11 | fun BackNavigationIcon( 12 | onClick: () -> Unit 13 | ) { 14 | IconButton(onClick = onClick) { 15 | Icon( 16 | painter = rememberVectorPainter(image = Icons.Sharp.ArrowBack), 17 | contentDescription = null 18 | ) 19 | } 20 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/composables/ErrorDialog.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.composables 2 | 3 | import androidx.compose.material.AlertDialog 4 | import androidx.compose.material.Button 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.res.stringResource 9 | import androidx.compose.ui.semantics.semantics 10 | import androidx.compose.ui.semantics.testTag 11 | import com.hackaprende.dogedex.core.R 12 | 13 | @Composable 14 | fun ErrorDialog( 15 | messageId: Int, 16 | onDialogDismiss: () -> Unit, 17 | ) { 18 | AlertDialog( 19 | modifier = Modifier 20 | .semantics { testTag = "error-dialog" }, 21 | onDismissRequest = { }, 22 | title = { 23 | Text(stringResource(R.string.error_dialog_title)) 24 | }, 25 | text = { 26 | Text(stringResource(id = messageId)) 27 | }, 28 | confirmButton = { 29 | Button(onClick = { onDialogDismiss() }) { 30 | Text(stringResource(R.string.try_again)) 31 | } 32 | } 33 | ) 34 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/composables/LoadingWheel.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.composables 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.semantics.semantics 11 | import androidx.compose.ui.semantics.testTag 12 | 13 | @Composable 14 | fun LoadingWheel() { 15 | Box( 16 | modifier = Modifier 17 | .fillMaxSize() 18 | .semantics { testTag = "loading-wheel" }, 19 | contentAlignment = Alignment.Center 20 | ) { 21 | CircularProgressIndicator( 22 | color = Color.Red 23 | ) 24 | } 25 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/di/ApiServiceModule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.di 2 | 3 | import com.hackaprende.dogedex.core.BASE_URL 4 | import com.hackaprende.dogedex.core.api.ApiService 5 | import com.hackaprende.dogedex.core.api.ApiServiceInterceptor 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import okhttp3.OkHttpClient 11 | import retrofit2.Retrofit 12 | import retrofit2.converter.moshi.MoshiConverterFactory 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object ApiServiceModule { 17 | 18 | @Provides 19 | fun provideApiService(retrofit: Retrofit) = retrofit.create(ApiService::class.java) 20 | 21 | @Provides 22 | fun provideRetrofit( 23 | okHttpClient: OkHttpClient, 24 | ) = Retrofit.Builder() 25 | .client(okHttpClient) 26 | .baseUrl(BASE_URL) 27 | .addConverterFactory(MoshiConverterFactory.create()) 28 | .build() 29 | 30 | @Provides 31 | fun provideHttpClient() = OkHttpClient 32 | .Builder() 33 | .addInterceptor(ApiServiceInterceptor) 34 | .build() 35 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/di/DispatchersModule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.di 2 | 3 | import dagger.Module 4 | import dagger.Provides 5 | import dagger.hilt.InstallIn 6 | import dagger.hilt.components.SingletonComponent 7 | import kotlinx.coroutines.Dispatchers 8 | 9 | @Module 10 | @InstallIn(SingletonComponent::class) 11 | object DispatchersModule { 12 | @Provides 13 | fun provideIoDispatcher() = Dispatchers.IO 14 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/di/DogTasksModule.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.di 2 | 3 | import com.hackaprende.dogedex.core.doglist.DogRepository 4 | import com.hackaprende.dogedex.core.doglist.DogTasks 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | 10 | @Module 11 | @InstallIn(SingletonComponent::class) 12 | abstract class DogTasksModule { 13 | 14 | @Binds 15 | abstract fun bindDogTasks( 16 | dogRepository: DogRepository 17 | ): DogTasks 18 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/DogDetailComposeActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import coil.annotation.ExperimentalCoilApi 7 | import com.hackaprende.dogedex.core.dogdetail.ui.theme.DogedexTheme 8 | import dagger.hilt.android.AndroidEntryPoint 9 | 10 | @ExperimentalCoilApi 11 | @AndroidEntryPoint 12 | class DogDetailComposeActivity : ComponentActivity() { 13 | companion object { 14 | const val DOG_KEY = "dog" 15 | const val MOST_PROBABLE_DOGS_IDS = "most_probable_dogs_ids" 16 | const val IS_RECOGNITION_KEY = "is_recognition" 17 | } 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContent { 22 | DogedexTheme { 23 | DogDetailScreen( 24 | finishActivity = { finish() } 25 | ) 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/DogDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.SavedStateHandle 5 | import androidx.lifecycle.ViewModel 6 | import androidx.lifecycle.viewModelScope 7 | import coil.annotation.ExperimentalCoilApi 8 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 9 | import com.hackaprende.dogedex.core.doglist.DogTasks 10 | import com.hackaprende.dogedex.core.model.Dog 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.MutableStateFlow 13 | import kotlinx.coroutines.flow.StateFlow 14 | import kotlinx.coroutines.flow.collect 15 | import kotlinx.coroutines.launch 16 | import javax.inject.Inject 17 | 18 | @ExperimentalCoilApi 19 | @HiltViewModel 20 | class DogDetailViewModel @Inject constructor( 21 | private val dogRepository: DogTasks, 22 | savedStateHandle: SavedStateHandle, 23 | ): ViewModel() { 24 | 25 | var dog = mutableStateOf( 26 | savedStateHandle.get(DogDetailComposeActivity.DOG_KEY) 27 | ) 28 | private set 29 | 30 | private var probableDogsIds = mutableStateOf( 31 | savedStateHandle.get>(DogDetailComposeActivity.MOST_PROBABLE_DOGS_IDS) ?: arrayListOf() 32 | ) 33 | 34 | var isRecognition = mutableStateOf( 35 | savedStateHandle.get(DogDetailComposeActivity.IS_RECOGNITION_KEY) ?: false 36 | ) 37 | private set 38 | 39 | var status = mutableStateOf?>(null) 40 | private set 41 | 42 | private var _probableDogList = MutableStateFlow>(mutableListOf()) 43 | val probableDogList: StateFlow> 44 | get() = _probableDogList 45 | 46 | fun getProbableDogs() { 47 | _probableDogList.value.clear() 48 | viewModelScope.launch { 49 | dogRepository.getProbableDogs(probableDogsIds.value) 50 | .collect { apiResponseStatus -> 51 | if (apiResponseStatus is ApiResponseStatus.Success) { 52 | val probableDogMutableList = _probableDogList.value.toMutableList() 53 | probableDogMutableList.add(apiResponseStatus.data) 54 | _probableDogList.value = probableDogMutableList 55 | } 56 | } 57 | } 58 | } 59 | 60 | fun updateDog(newDog: Dog) { 61 | dog.value = newDog 62 | } 63 | 64 | fun addDogToUser() { 65 | viewModelScope.launch { 66 | status.value = ApiResponseStatus.Loading() 67 | handleAddDogToUserResponseStatus(dogRepository.addDogToUser(dog.value!!.id)) 68 | } 69 | } 70 | 71 | private fun handleAddDogToUserResponseStatus(apiResponseStatus: ApiResponseStatus) { 72 | status.value = apiResponseStatus 73 | } 74 | 75 | fun resetApiResponseStatus() { 76 | status.value = null 77 | } 78 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/MostProbableDogsDialog.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.material.AlertDialog 8 | import androidx.compose.material.Button 9 | import androidx.compose.material.Surface 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.colorResource 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import coil.annotation.ExperimentalCoilApi 20 | import com.hackaprende.dogedex.core.dogdetail.ui.theme.DogedexTheme 21 | import com.hackaprende.dogedex.core.model.Dog 22 | import com.hackaprende.dogedex.core.R 23 | 24 | @ExperimentalCoilApi 25 | @Composable 26 | fun MostProbableDogsDialog( 27 | mostProbableDogs: MutableList, 28 | onShowMostProbableDogsDialogDismiss: () -> Unit, 29 | onItemClicked: (Dog) -> Unit 30 | ) { 31 | AlertDialog( 32 | onDismissRequest = { 33 | onShowMostProbableDogsDialogDismiss() 34 | }, 35 | title = { 36 | Text( 37 | text = stringResource(id = R.string.other_probable_dogs), 38 | color = colorResource(id = R.color.text_black), 39 | fontSize = 20.sp, 40 | fontWeight = FontWeight.Medium 41 | ) 42 | }, 43 | text = { 44 | MostProbableDogsList(mostProbableDogs) { 45 | onItemClicked(it) 46 | onShowMostProbableDogsDialogDismiss() 47 | } 48 | }, 49 | buttons = { 50 | Row( 51 | modifier = Modifier.padding(all = 8.dp), 52 | horizontalArrangement = Arrangement.Center 53 | ) { 54 | Button( 55 | modifier = Modifier.fillMaxWidth(), 56 | onClick = { onShowMostProbableDogsDialogDismiss() } 57 | ) { 58 | Text(stringResource(id = R.string.dismiss)) 59 | } 60 | } 61 | } 62 | ) 63 | } 64 | 65 | @ExperimentalCoilApi 66 | @Composable 67 | fun MostProbableDogsList(dogs: MutableList, onItemClicked: (Dog) -> Unit) { 68 | Box( 69 | Modifier.height(250.dp) 70 | ) { 71 | LazyColumn( 72 | content = { 73 | items(dogs) { 74 | MostProbableDogItem(dog = it, onItemClicked) 75 | } 76 | } 77 | ) 78 | } 79 | } 80 | 81 | @ExperimentalCoilApi 82 | @Composable 83 | fun MostProbableDogItem(dog: Dog, onItemClicked: (Dog) -> Unit) { 84 | Row( 85 | modifier = Modifier 86 | .fillMaxWidth() 87 | .padding(16.dp) 88 | .clickable( 89 | enabled = true, 90 | onClick = { onItemClicked(dog) } 91 | ), 92 | ) { 93 | Text( 94 | dog.name, 95 | modifier = Modifier.padding(8.dp), 96 | color = colorResource(id = R.color.text_black), 97 | ) 98 | } 99 | } 100 | 101 | @ExperimentalCoilApi 102 | @Composable 103 | @Preview(showBackground = true) 104 | fun MostProbableDogsDialogPreview() { 105 | DogedexTheme { 106 | Surface { 107 | MostProbableDogsDialog(getFakeDogs(), {}) {} 108 | } 109 | } 110 | } 111 | 112 | fun getFakeDogs(): MutableList { 113 | val dogList = mutableListOf() 114 | dogList.add( 115 | Dog( 116 | 1, 1, "Chihuahua", "Chihuahua", "Toy", 117 | "19", "Brave", "12 - 15", 118 | "2.0", "2.5", "12.0", false 119 | ) 120 | ) 121 | dogList.add( 122 | Dog( 123 | 2, 2, "Pug", "Pug", "Toy", 124 | "12", "Friendly", "www.pug.com", 125 | "10 - 12", "4.5", "12.0", true 126 | ) 127 | ) 128 | dogList.add( 129 | Dog( 130 | 3, 3, "Husky", "Husky", "Sporting", 131 | "15", "www.husky.com", "8 - 12 ", 132 | "5.0", "4.5", "12.0", false 133 | ) 134 | ) 135 | return dogList 136 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = Purple200, 11 | primaryVariant = Purple700, 12 | secondary = Teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = Purple500, 17 | primaryVariant = Purple700, 18 | secondary = Teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun DogedexTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = Typography, 41 | shapes = Shapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/dogdetail/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.dogdetail.ui.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/doglist/DogListActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.doglist 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.compose.foundation.ExperimentalFoundationApi 8 | import androidx.compose.material.ExperimentalMaterialApi 9 | import coil.annotation.ExperimentalCoilApi 10 | import com.hackaprende.dogedex.core.dogdetail.DogDetailComposeActivity 11 | import com.hackaprende.dogedex.core.dogdetail.ui.theme.DogedexTheme 12 | import com.hackaprende.dogedex.core.model.Dog 13 | import dagger.hilt.android.AndroidEntryPoint 14 | 15 | @ExperimentalMaterialApi 16 | @ExperimentalFoundationApi 17 | @ExperimentalCoilApi 18 | @AndroidEntryPoint 19 | class DogListActivity : ComponentActivity() { 20 | 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | setContent { 24 | DogedexTheme { 25 | DogListScreen( 26 | onNavigationIconClick = ::onNavigationIconClick, 27 | onDogClicked = ::openDogDetailActivity, 28 | ) 29 | } 30 | } 31 | } 32 | 33 | private fun openDogDetailActivity(dog: Dog) { 34 | val intent = Intent(this, DogDetailComposeActivity::class.java) 35 | intent.putExtra(DogDetailComposeActivity.DOG_KEY, dog) 36 | startActivity(intent) 37 | } 38 | 39 | private fun onNavigationIconClick() { 40 | finish() 41 | } 42 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/doglist/DogListScreen.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.doglist 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.grid.GridCells 8 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 9 | import androidx.compose.foundation.lazy.grid.items 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.semantics.semantics 17 | import androidx.compose.ui.semantics.testTag 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import androidx.hilt.navigation.compose.hiltViewModel 23 | import coil.annotation.ExperimentalCoilApi 24 | import coil.compose.rememberImagePainter 25 | import com.hackaprende.dogedex.core.R 26 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 27 | import com.hackaprende.dogedex.core.composables.BackNavigationIcon 28 | import com.hackaprende.dogedex.core.composables.ErrorDialog 29 | import com.hackaprende.dogedex.core.composables.LoadingWheel 30 | import com.hackaprende.dogedex.core.model.Dog 31 | 32 | private const val GRID_SPAN_COUNT = 3 33 | 34 | @ExperimentalCoilApi 35 | @ExperimentalMaterialApi 36 | @ExperimentalFoundationApi 37 | @Composable 38 | fun DogListScreen( 39 | onNavigationIconClick: () -> Unit, 40 | onDogClicked: (Dog) -> Unit, 41 | viewModel: DogListViewModel = hiltViewModel() 42 | ) { 43 | val status = viewModel.status.value 44 | val dogList = viewModel.dogList.value 45 | Scaffold( 46 | topBar = { DogListScreenTopBar(onNavigationIconClick) } 47 | ) { 48 | LazyVerticalGrid( 49 | contentPadding = it, 50 | columns = GridCells.Fixed(GRID_SPAN_COUNT), 51 | content = { 52 | items(dogList) { 53 | dog -> 54 | DogGridItem(dog = dog, onDogClicked) 55 | } 56 | }, 57 | ) 58 | } 59 | 60 | if (status is ApiResponseStatus.Loading) { 61 | LoadingWheel() 62 | } else if (status is ApiResponseStatus.Error) { 63 | ErrorDialog(status.messageId) { viewModel.resetApiResponseStatus() } 64 | } 65 | } 66 | 67 | @Composable 68 | fun DogListScreenTopBar( 69 | onClick: () -> Unit 70 | ) { 71 | TopAppBar( 72 | title = { Text(stringResource(R.string.my_dog_collection)) }, 73 | backgroundColor = Color.White, 74 | contentColor = Color.Black, 75 | navigationIcon = { BackNavigationIcon(onClick) } 76 | ) 77 | } 78 | 79 | @ExperimentalCoilApi 80 | @ExperimentalMaterialApi 81 | @Composable 82 | fun DogGridItem(dog: Dog, onDogClicked: (Dog) -> Unit) { 83 | if (dog.inCollection) { 84 | Surface( 85 | modifier = Modifier 86 | .padding(8.dp) 87 | .height(100.dp) 88 | .width(100.dp), 89 | onClick = { onDogClicked(dog) }, 90 | shape = RoundedCornerShape(4.dp) 91 | ) { 92 | Image( 93 | painter = rememberImagePainter(dog.imageUrl), 94 | contentDescription = null, 95 | modifier = Modifier 96 | .background(Color.White) 97 | .semantics { testTag = "dog-${dog.name}" } 98 | ) 99 | } 100 | } else { 101 | Surface( 102 | modifier = Modifier 103 | .padding(8.dp) 104 | .height(100.dp) 105 | .width(100.dp), 106 | color = Color.Red, 107 | shape = RoundedCornerShape(4.dp) 108 | ) { 109 | Text( 110 | text = dog.index.toString(), 111 | color = Color.White, 112 | modifier = Modifier 113 | .fillMaxSize() 114 | .padding(16.dp), 115 | textAlign = TextAlign.Center, 116 | fontSize = 42.sp, 117 | fontWeight = FontWeight.Black 118 | ) 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/doglist/DogListViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.doglist 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 7 | import com.hackaprende.dogedex.core.model.Dog 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import kotlinx.coroutines.launch 10 | import javax.inject.Inject 11 | 12 | @HiltViewModel 13 | class DogListViewModel @Inject constructor( 14 | private val dogRepository: DogTasks, 15 | ): ViewModel() { 16 | 17 | var dogList = mutableStateOf>(listOf()) 18 | private set 19 | 20 | var status = mutableStateOf?>(null) 21 | private set 22 | 23 | init { 24 | getDogCollection() 25 | } 26 | 27 | private fun getDogCollection() { 28 | viewModelScope.launch { 29 | status.value = ApiResponseStatus.Loading() 30 | handleResponseStatus(dogRepository.getDogCollection()) 31 | } 32 | } 33 | 34 | @Suppress("UNCHECKED_CAST") 35 | private fun handleResponseStatus(apiResponseStatus: ApiResponseStatus>) { 36 | if (apiResponseStatus is ApiResponseStatus.Success) { 37 | dogList.value = apiResponseStatus.data!! 38 | } 39 | 40 | status.value = apiResponseStatus as ApiResponseStatus 41 | } 42 | 43 | fun resetApiResponseStatus() { 44 | status.value = null 45 | } 46 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/doglist/DogRepository.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.doglist 2 | 3 | import com.hackaprende.dogedex.core.R 4 | import com.hackaprende.dogedex.core.api.ApiResponseStatus 5 | import com.hackaprende.dogedex.core.api.ApiService 6 | import com.hackaprende.dogedex.core.api.dto.AddDogToUserDTO 7 | import com.hackaprende.dogedex.core.api.dto.DogDTOMapper 8 | import com.hackaprende.dogedex.core.api.makeNetworkCall 9 | import com.hackaprende.dogedex.core.model.Dog 10 | import kotlinx.coroutines.CoroutineDispatcher 11 | import kotlinx.coroutines.async 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.flow 14 | import kotlinx.coroutines.flow.flowOn 15 | import kotlinx.coroutines.withContext 16 | import javax.inject.Inject 17 | 18 | interface DogTasks { 19 | suspend fun getDogCollection(): ApiResponseStatus> 20 | suspend fun addDogToUser(dogId: Long): ApiResponseStatus 21 | suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus 22 | suspend fun getProbableDogs(probableDogsIds: ArrayList): Flow> 23 | } 24 | 25 | class DogRepository @Inject constructor( 26 | private val apiService: ApiService, 27 | private val dispatcher: CoroutineDispatcher, 28 | ) : DogTasks { 29 | 30 | override suspend fun getDogCollection(): ApiResponseStatus> { 31 | return withContext(dispatcher) { 32 | val allDogsListDeferred = async { downloadDogs() } 33 | val userDogsListDeferred = async { getUserDogs() } 34 | 35 | val allDogsListResponse = allDogsListDeferred.await() 36 | val userDogsListResponse = userDogsListDeferred.await() 37 | 38 | if (allDogsListResponse is ApiResponseStatus.Error) { 39 | allDogsListResponse 40 | } else if (userDogsListResponse is ApiResponseStatus.Error) { 41 | userDogsListResponse 42 | } else if (allDogsListResponse is ApiResponseStatus.Success && 43 | userDogsListResponse is ApiResponseStatus.Success) { 44 | ApiResponseStatus.Success(getCollectionList(allDogsListResponse.data, 45 | userDogsListResponse.data)) 46 | } else { 47 | ApiResponseStatus.Error(R.string.unknown_error) 48 | } 49 | } 50 | } 51 | 52 | private fun getCollectionList(allDogList: List, userDogList: List) = 53 | allDogList.map { 54 | if (userDogList.contains(it)) { 55 | it 56 | } else { 57 | Dog( 58 | it.id, it.index, "", "", "", "", 59 | "", "", "", "", "", 60 | inCollection = false 61 | ) 62 | } 63 | }.sorted() 64 | 65 | private suspend fun downloadDogs(): ApiResponseStatus> = makeNetworkCall { 66 | val dogListApiResponse = apiService.getAllDogs() 67 | val dogDTOList = dogListApiResponse.data.dogs 68 | val dogDTOMapper = DogDTOMapper() 69 | dogDTOMapper.fromDogDTOListToDogDomainList(dogDTOList) 70 | } 71 | 72 | private suspend fun getUserDogs(): ApiResponseStatus> = makeNetworkCall { 73 | val dogListApiResponse = apiService.getUserDogs() 74 | val dogDTOList = dogListApiResponse.data.dogs 75 | val dogDTOMapper = DogDTOMapper() 76 | dogDTOMapper.fromDogDTOListToDogDomainList(dogDTOList) 77 | } 78 | 79 | override suspend fun addDogToUser(dogId: Long): ApiResponseStatus = makeNetworkCall { 80 | val addDogToUserDTO = AddDogToUserDTO(dogId) 81 | val defaultResponse = apiService.addDogToUser(addDogToUserDTO) 82 | 83 | if (!defaultResponse.isSuccess) { 84 | throw Exception(defaultResponse.message) 85 | } 86 | } 87 | 88 | override suspend fun getDogByMlId(mlDogId: String): ApiResponseStatus = makeNetworkCall { 89 | val response = apiService.getDogByMlId(mlDogId) 90 | 91 | if (!response.isSuccess) { 92 | throw Exception(response.message) 93 | } 94 | 95 | val dogDTOMapper = DogDTOMapper() 96 | dogDTOMapper.fromDogDTOToDogDomain(response.data.dog) 97 | } 98 | 99 | override suspend fun getProbableDogs(probableDogsIds: ArrayList): 100 | Flow> = flow { 101 | for (mlDogId in probableDogsIds) { 102 | val dog = getDogByMlId(mlDogId) 103 | emit(dog) 104 | } 105 | }.flowOn(dispatcher) 106 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/model/Dog.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | 6 | @Parcelize 7 | data class Dog( 8 | val id: Long, 9 | val index: Int, 10 | val name: String, 11 | val type: String, 12 | val heightFemale: String, 13 | val heightMale: String, 14 | val imageUrl: String, 15 | val lifeExpectancy: String, 16 | val temperament: String, 17 | val weightFemale: String, 18 | val weightMale: String, 19 | var inCollection: Boolean = true 20 | ) : Parcelable, Comparable { 21 | override fun compareTo(other: Dog) = 22 | if (this.index > other.index) { 23 | 1 24 | } else { 25 | -1 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.model 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | 6 | class User( 7 | val id: Long, 8 | val email: String, 9 | val authenticationToken: String 10 | ) { 11 | 12 | companion object { 13 | private const val AUTH_PREFS = "auth_prefs" 14 | private const val ID_KEY = "id" 15 | private const val EMAIL_KEY = "email" 16 | private const val AUTH_TOKEN_KEY = "auth_token" 17 | 18 | fun setLoggedInUser(activity: Activity, user: User) { 19 | activity.getSharedPreferences(AUTH_PREFS, 20 | Context.MODE_PRIVATE).also { 21 | it.edit() 22 | .putLong(ID_KEY, user.id) 23 | .putString(EMAIL_KEY, user.email) 24 | .putString(AUTH_TOKEN_KEY, user.authenticationToken) 25 | .apply() 26 | } 27 | } 28 | 29 | fun getLoggedInUser(activity: Activity): User? { 30 | val prefs = activity.getSharedPreferences( 31 | AUTH_PREFS, 32 | Context.MODE_PRIVATE 33 | ) ?: return null 34 | 35 | val userId = prefs.getLong(ID_KEY, 0) 36 | if (userId == 0L) { 37 | return null 38 | } 39 | 40 | return User( 41 | userId, 42 | prefs.getString(EMAIL_KEY, "") ?: "", 43 | prefs.getString(AUTH_TOKEN_KEY, "") ?: "", 44 | ) 45 | } 46 | 47 | fun logout(activity: Activity) { 48 | activity.getSharedPreferences( 49 | AUTH_PREFS, 50 | Context.MODE_PRIVATE 51 | ).also { 52 | it.edit().clear().apply() 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/settings/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.settings 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import android.widget.Toast 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.compose.foundation.ExperimentalFoundationApi 8 | import androidx.compose.material.ExperimentalMaterialApi 9 | import coil.annotation.ExperimentalCoilApi 10 | import com.hackaprende.dogedex.core.R 11 | import com.hackaprende.dogedex.core.databinding.ActivitySettingsBinding 12 | import com.hackaprende.dogedex.core.model.User 13 | 14 | @ExperimentalCoilApi 15 | @ExperimentalFoundationApi 16 | @ExperimentalMaterialApi 17 | class SettingsActivity : AppCompatActivity() { 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | val binding = ActivitySettingsBinding.inflate(layoutInflater) 21 | setContentView(binding.root) 22 | 23 | binding.logoutButton.setOnClickListener { 24 | logout() 25 | } 26 | } 27 | 28 | private fun logout() { 29 | User.logout(this) 30 | try { 31 | val intent = Intent( 32 | this, 33 | Class.forName("com.hackaprende.dogedex.auth.auth.LoginActivity") 34 | ) 35 | intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK 36 | startActivity(intent) 37 | } catch (e: ClassNotFoundException) { 38 | Toast.makeText(this, getString(R.string.login_screen_error), Toast.LENGTH_SHORT).show() 39 | } 40 | startActivity(intent) 41 | } 42 | } -------------------------------------------------------------------------------- /core/src/main/java/com/hackaprende/dogedex/core/testutils/EspressoIdlingResource.kt: -------------------------------------------------------------------------------- 1 | package com.hackaprende.dogedex.core.testutils 2 | 3 | import androidx.test.espresso.IdlingResource 4 | import androidx.test.espresso.idling.CountingIdlingResource 5 | 6 | object EspressoIdlingResource { 7 | private const val RESOURCE = "GLOBAL" 8 | 9 | private val countingIdlingResource = CountingIdlingResource(RESOURCE) 10 | 11 | val idlingResource: IdlingResource 12 | get() = countingIdlingResource 13 | 14 | fun increment() { 15 | countingIdlingResource.increment() 16 | } 17 | 18 | fun decrement() { 19 | if (!countingIdlingResource.isIdleNow) { 20 | countingIdlingResource.decrement() 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /core/src/main/res/drawable/detail_info_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/dog_list_item_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/dog_list_item_null_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_check_black.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_hearth_white.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | -------------------------------------------------------------------------------- /core/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 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/progressbar_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/src/main/res/drawable/red_circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 |