diff --git "a/CPR2U-Android/.github/ISSUE_TEMPLATE/\352\270\260\353\263\270-\354\235\264\354\212\210\355\205\234\355\224\214\353\246\277.md" "b/CPR2U-Android/.github/ISSUE_TEMPLATE/\352\270\260\353\263\270-\354\235\264\354\212\210\355\205\234\355\224\214\353\246\277.md" new file mode 100644 index 0000000..e43bba3 --- /dev/null +++ "b/CPR2U-Android/.github/ISSUE_TEMPLATE/\352\270\260\353\263\270-\354\235\264\354\212\210\355\205\234\355\224\214\353\246\277.md" @@ -0,0 +1,17 @@ +--- +name: 기본 이슈템플릿 +about: 이거쓰세용 +title: '' +labels: '' +assignees: '' + +--- + +## 🔥 Issue + +CalendarFragment 기초 세팅 완료하기! + +## 📌 Todo + +- [ ] 뷰 세팅(캘린더 제외) +- [ ] 로직 짜기(캘린더 제외) diff --git a/CPR2U-Android/.github/PULL_REQUEST_TEMPLATE.md b/CPR2U-Android/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c3086a7 --- /dev/null +++ b/CPR2U-Android/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## ❤️‍🔥 관련 이슈 + +close #1 + +## ✨ PR Point + +- DI +- Base Activity, Base Fragment +- 라이브러리 세팅 +- 바텀 네비게이션 세팅 +- 기타 뷰 세팅 + + diff --git a/CPR2U-Android/.gitignore b/CPR2U-Android/.gitignore new file mode 100644 index 0000000..c1b47ad --- /dev/null +++ b/CPR2U-Android/.gitignore @@ -0,0 +1,166 @@ +### Kotlin ### +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iml +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml +.idea/discord.xml +.idea/git_toolbox_prj.xml +.idea/deploymentTargetDropDown.xml + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Uncomment the following line in case you need and you don't have the release build type files in your app + release/ + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar diff --git a/CPR2U-Android/README.md b/CPR2U-Android/README.md index de6db1c..9cdce4a 100644 --- a/CPR2U-Android/README.md +++ b/CPR2U-Android/README.md @@ -1,2 +1 @@ -# CPR2U-Android - +# CPR2U-Android \ No newline at end of file diff --git a/CPR2U-Android/app/.gitignore b/CPR2U-Android/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/CPR2U-Android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/CPR2U-Android/app/build.gradle b/CPR2U-Android/app/build.gradle new file mode 100644 index 0000000..9bb9585 --- /dev/null +++ b/CPR2U-Android/app/build.gradle @@ -0,0 +1,135 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' + id 'com.google.gms.google-services' + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' +} + +Properties properties = new Properties() +properties.load(project.rootProject.file('local.properties').newDataInputStream()) +def map_api_key_in_manifest = properties.getProperty("MAPS_API_KEY") + +android { + namespace 'com.example.cpr2u_android' + compileSdk 33 + + defaultConfig { + buildConfigField( + "String", + "BASE_URL", + properties.getProperty("BASE_URL") + ) + applicationId "com.example.cpr2u_android" + minSdk 26 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + manifestPlaceholders = [MAPS_API_KEY: map_api_key_in_manifest] + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + dataBinding = true + viewBinding true + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.6.0' + + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'com.google.android.libraries.places:places:3.0.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + + //FragmentContainerView + implementation 'androidx.fragment:fragment-ktx:1.5.5' + + //Navigation Component + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + //Gson + implementation "com.google.code.gson:gson:2.8.9" + + // Gson Converter + implementation "com.squareup.retrofit2:converter-gson:2.9.0" + + //ViewModel + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + + //LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + + //LifeCycle + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + androidTestImplementation "androidx.arch.core:core-testing:$arch_version" + implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1' + implementation 'androidx.fragment:fragment-ktx:1.5.5' + + //ViewPager2 + implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" + + /* Third Party Library */ + + //Glide + implementation 'com.github.bumptech.glide:glide:4.12.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' + + //Retrofit2 + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + + //okHttp + implementation "com.squareup.okhttp3:logging-interceptor:$okHttp_version" + implementation "com.squareup.okhttp3:okhttp:$okHttp_version" + + implementation "com.airbnb.android:lottie:3.6.1" + + //Firebase SDK 추가 + implementation platform('com.google.firebase:firebase-bom:29.1.0') + implementation 'com.google.firebase:firebase-analytics-ktx' + + //Firebase Cloud Messaging(FCM) + implementation "com.google.firebase:firebase-messaging-ktx:23.1.2" + + //Timber + implementation 'com.jakewharton.timber:timber:5.0.1' + + // koin + implementation "io.insert-koin:koin-androidx-scope:$koin_version" + implementation "io.insert-koin:koin-androidx-viewmodel:$koin_version" + testImplementation "io.insert-koin:koin-test:$koin_version" + + implementation 'org.tensorflow:tensorflow-lite:2.5.0' + implementation 'org.tensorflow:tensorflow-lite-gpu:2.5.0' + implementation 'org.tensorflow:tensorflow-lite-support:0.3.0' + + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation "com.google.truth:truth:1.1.3" + + implementation 'com.google.android.gms:play-services-maps:18.1.0' + implementation 'com.google.android.gms:play-services-location:21.0.1' + implementation 'com.google.maps.android:android-maps-utils:0.5' +} \ No newline at end of file diff --git a/CPR2U-Android/app/google-services.json b/CPR2U-Android/app/google-services.json new file mode 100644 index 0000000..9fc31c2 --- /dev/null +++ b/CPR2U-Android/app/google-services.json @@ -0,0 +1,82 @@ +{ + "project_info": { + "project_number": "260439878640", + "project_id": "cpr2u-b1b0d", + "storage_bucket": "cpr2u-b1b0d.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:260439878640:android:268aefa00850161d108b35", + "android_client_info": { + "package_name": "com.example.cpr2u_android" + } + }, + "oauth_client": [ + { + "client_id": "260439878640-q9914et25h3p7q3m94jc4fh252gnstbl.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyA9jGR0v8QfuXvpMVSLcb1YTl6DHIyiDuQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "260439878640-q9914et25h3p7q3m94jc4fh252gnstbl.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "260439878640-vr6q4c66afobdoo5jc6i80juulmmr9s2.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.jhHwang.CPRtoU" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:260439878640:android:b4f8deda94cb724b108b35", + "android_client_info": { + "package_name": "com.example.test" + } + }, + "oauth_client": [ + { + "client_id": "260439878640-q9914et25h3p7q3m94jc4fh252gnstbl.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyA9jGR0v8QfuXvpMVSLcb1YTl6DHIyiDuQ" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "260439878640-q9914et25h3p7q3m94jc4fh252gnstbl.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "260439878640-vr6q4c66afobdoo5jc6i80juulmmr9s2.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.jhHwang.CPRtoU" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/CPR2U-Android/app/proguard-rules.pro b/CPR2U-Android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/CPR2U-Android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/CPR2U-Android/app/src/androidTest/java/com/example/cpr2u_android/ExampleInstrumentedTest.kt b/CPR2U-Android/app/src/androidTest/java/com/example/cpr2u_android/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..ddc79e3 --- /dev/null +++ b/CPR2U-Android/app/src/androidTest/java/com/example/cpr2u_android/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.cpr2u_android + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.cpr2u_android", appContext.packageName) + } +} \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/AndroidManifest.xml b/CPR2U-Android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..eb2bc1a --- /dev/null +++ b/CPR2U-Android/app/src/main/AndroidManifest.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/assets/classifier.tflite b/CPR2U-Android/app/src/main/assets/classifier.tflite new file mode 100644 index 0000000..8c0f598 Binary files /dev/null and b/CPR2U-Android/app/src/main/assets/classifier.tflite differ diff --git a/CPR2U-Android/app/src/main/assets/movenet_lightning.tflite b/CPR2U-Android/app/src/main/assets/movenet_lightning.tflite new file mode 100644 index 0000000..7e90817 Binary files /dev/null and b/CPR2U-Android/app/src/main/assets/movenet_lightning.tflite differ diff --git a/CPR2U-Android/app/src/main/assets/movenet_multipose_fp16.tflite b/CPR2U-Android/app/src/main/assets/movenet_multipose_fp16.tflite new file mode 100644 index 0000000..13f58ef Binary files /dev/null and b/CPR2U-Android/app/src/main/assets/movenet_multipose_fp16.tflite differ diff --git a/CPR2U-Android/app/src/main/assets/movenet_thunder.tflite b/CPR2U-Android/app/src/main/assets/movenet_thunder.tflite new file mode 100644 index 0000000..1582dc7 Binary files /dev/null and b/CPR2U-Android/app/src/main/assets/movenet_thunder.tflite differ diff --git a/CPR2U-Android/app/src/main/assets/posenet.tflite b/CPR2U-Android/app/src/main/assets/posenet.tflite new file mode 100644 index 0000000..d8b8b32 Binary files /dev/null and b/CPR2U-Android/app/src/main/assets/posenet.tflite differ diff --git a/CPR2U-Android/app/src/main/ic_launcher-playstore.png b/CPR2U-Android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..09b83c6 Binary files /dev/null and b/CPR2U-Android/app/src/main/ic_launcher-playstore.png differ diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/AuthService.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/AuthService.kt new file mode 100644 index 0000000..d722493 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/AuthService.kt @@ -0,0 +1,40 @@ +package com.example.cpr2u_android.data.api + +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.model.request.auth.RequestSignUp +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.auth.ResponseAutoLogin +import com.example.cpr2u_android.data.model.response.auth.ResponseLogin +import com.example.cpr2u_android.data.model.response.auth.ResponsePhoneVerification +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface AuthService { + + @POST("auth/auto-login") + suspend fun postAutoLogin( + @Body refreshToken: String, + ): ResponseAutoLogin + + @POST("auth/verification") + suspend fun postVerification( + @Body phone_number: String, + ): ResponsePhoneVerification + + @POST("auth/login") + suspend fun postLogin( + @Body body: RequestLogin, + ): ResponseLogin + + @GET("auth/nickname") + suspend fun getNickname( + @Query("nickname") nickName: String, + ): GeneralResponse + + @POST("auth/signup") + suspend fun postSignUp( + @Body body: RequestSignUp, + ): ResponseAutoLogin +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/CallService.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/CallService.kt new file mode 100644 index 0000000..20307b4 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/CallService.kt @@ -0,0 +1,44 @@ +package com.example.cpr2u_android.data.api + +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.request.education.RequestCall +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.call.ResponseCall +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.data.model.response.call.ResponseDispatch +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface CallService { + + @POST("/call") + suspend fun postCall( + @Body body: RequestCall, + ): ResponseCall + + @POST("/call/end/{call_id}") + suspend fun postCallEnd( + @Path("call_id") call_id: Int, + ): GeneralResponse + + @GET("/call") + suspend fun getCallList(): ResponseCallList + + @POST("/dispatch") + suspend fun postDispatch( + @Body cpr_call_id: Int, + ): ResponseDispatch + + @POST("/dispatch/arrive/{dispatch_id}") + suspend fun postDispatchArrive( + @Path("dispatch_id") dispatch_id: Int, + ): GeneralResponse + + @POST("/dispatch/report") + suspend fun postDispatchReport( + @Body data: RequestDispatchReport, + ): GeneralResponse +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/EducationService.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/EducationService.kt new file mode 100644 index 0000000..827a7cc --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/api/EducationService.kt @@ -0,0 +1,33 @@ +package com.example.cpr2u_android.data.api + +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.education.ResponseQuizzesList +import com.example.cpr2u_android.data.model.response.education.ResponseUserInfo +import retrofit2.http.* + +interface EducationService { + @POST("education/lectures/progress/{lectureId}") + suspend fun postLectureProgress( + @Path("lectureId") lectureId: Int, + ): GeneralResponse + + @GET("education/quizzes") + suspend fun getQuizzes(): ResponseQuizzesList + + @POST("quizzes/progress") + suspend fun postQuizProgress( + @Body score: Int, + ): GeneralResponse + + @POST("education/exercises/progress") + suspend fun postExercisesProgress( + @Body score: Int, + ): GeneralResponse + + @GET("education") + suspend fun getUserInfo(): ResponseUserInfo + + @GET("/users/address") + suspend fun getAddress(): ResponseAddress +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/auth/AuthDataSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/auth/AuthDataSource.kt new file mode 100644 index 0000000..d8b569b --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/auth/AuthDataSource.kt @@ -0,0 +1,16 @@ +package com.example.cpr2u_android.data.datasource.auth + +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.model.request.auth.RequestSignUp +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.auth.ResponseAutoLogin +import com.example.cpr2u_android.data.model.response.auth.ResponseLogin +import com.example.cpr2u_android.data.model.response.auth.ResponsePhoneVerification + +interface AuthDataSource { + suspend fun postAutoLogin(refreshToken: String): ResponseAutoLogin + suspend fun postVerification(phoneNumber: String): ResponsePhoneVerification + suspend fun postLogin(loginData: RequestLogin): ResponseLogin + suspend fun getNickName(nickname: String): GeneralResponse + suspend fun postSignUp(signUpData: RequestSignUp): ResponseAutoLogin +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/auth/AuthRemoteDataSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/auth/AuthRemoteDataSource.kt new file mode 100644 index 0000000..532df20 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/auth/AuthRemoteDataSource.kt @@ -0,0 +1,31 @@ +package com.example.cpr2u_android.data.datasource.auth + +import com.example.cpr2u_android.data.api.AuthService +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.model.request.auth.RequestSignUp +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.auth.ResponseAutoLogin +import com.example.cpr2u_android.data.model.response.auth.ResponseLogin +import com.example.cpr2u_android.data.model.response.auth.ResponsePhoneVerification + +class AuthRemoteDataSource(private val authService: AuthService) : AuthDataSource { + override suspend fun postAutoLogin(refreshToken: String): ResponseAutoLogin { + return authService.postAutoLogin(refreshToken) + } + + override suspend fun postVerification(phoneNumber: String): ResponsePhoneVerification { + return authService.postVerification(phoneNumber) + } + + override suspend fun postLogin(loginData: RequestLogin): ResponseLogin { + return authService.postLogin(loginData) + } + + override suspend fun getNickName(nickname: String): GeneralResponse { + return authService.getNickname(nickname) + } + + override suspend fun postSignUp(signUpData: RequestSignUp): ResponseAutoLogin { + return authService.postSignUp(signUpData) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/call/CallDataSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/call/CallDataSource.kt new file mode 100644 index 0000000..50e1638 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/call/CallDataSource.kt @@ -0,0 +1,18 @@ +package com.example.cpr2u_android.data.datasource.call + +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.request.education.RequestCall +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.call.ResponseCall +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.data.model.response.call.ResponseDispatch + +interface CallDataSource { + suspend fun postCall(data: RequestCall): ResponseCall + suspend fun postCallEnd(callId: Int): GeneralResponse + suspend fun getCallList(): ResponseCallList + suspend fun postDispatch(callId: Int): ResponseDispatch + suspend fun postDispatchArrived(dispatchId: Int): GeneralResponse + suspend fun postDispatchReport(data: RequestDispatchReport): GeneralResponse +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/call/CallRemoteDataSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/call/CallRemoteDataSource.kt new file mode 100644 index 0000000..2a49c43 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/call/CallRemoteDataSource.kt @@ -0,0 +1,38 @@ +package com.example.cpr2u_android.data.datasource.call + +import com.example.cpr2u_android.data.api.CallService +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.request.education.RequestCall +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.call.ResponseCall +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.data.model.response.call.ResponseDispatch +import timber.log.Timber + +class CallRemoteDataSource(private val callService: CallService) : CallDataSource { + override suspend fun postCall(data: RequestCall): ResponseCall { + return callService.postCall(data) + } + + override suspend fun postCallEnd(callId: Int): GeneralResponse { + Timber.d("data call Id -> $callId") + return callService.postCallEnd(callId) + } + + override suspend fun getCallList(): ResponseCallList { + return callService.getCallList() + } + + override suspend fun postDispatch(callId: Int): ResponseDispatch { + return callService.postDispatch(callId) + } + + override suspend fun postDispatchArrived(dispatchId: Int): GeneralResponse { + return callService.postDispatchArrive(dispatchId) + } + + override suspend fun postDispatchReport(data: RequestDispatchReport): GeneralResponse { + return callService.postDispatchReport(data) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/education/EducationDataSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/education/EducationDataSource.kt new file mode 100644 index 0000000..34352bc --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/education/EducationDataSource.kt @@ -0,0 +1,15 @@ +package com.example.cpr2u_android.data.datasource.education + +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.education.ResponseQuizzesList +import com.example.cpr2u_android.data.model.response.education.ResponseUserInfo + +interface EducationDataSource { + suspend fun postLectureId(lectureId: Int): GeneralResponse + suspend fun getQuizzes(): ResponseQuizzesList + suspend fun postQuizProgress(score: Int): GeneralResponse + suspend fun postExercisesProgress(score: Int): GeneralResponse + suspend fun getUserInfo(): ResponseUserInfo + suspend fun getAddress(): ResponseAddress +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/education/EducationRemoteDataSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/education/EducationRemoteDataSource.kt new file mode 100644 index 0000000..ba95542 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/datasource/education/EducationRemoteDataSource.kt @@ -0,0 +1,35 @@ +package com.example.cpr2u_android.data.datasource.education + +import com.example.cpr2u_android.data.api.EducationService +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.education.ResponseQuizzesList +import com.example.cpr2u_android.data.model.response.education.ResponseUserInfo +import timber.log.Timber + +class EducationRemoteDataSource(private val educationService: EducationService) : EducationDataSource { + override suspend fun postLectureId(lectureId: Int): GeneralResponse { + Timber.d("Datasource lectureId -> $lectureId") + return educationService.postLectureProgress(lectureId) + } + + override suspend fun getQuizzes(): ResponseQuizzesList { + return educationService.getQuizzes() + } + + override suspend fun postQuizProgress(score: Int): GeneralResponse { + return educationService.postQuizProgress(score) + } + + override suspend fun postExercisesProgress(score: Int): GeneralResponse { + return educationService.postExercisesProgress(score) + } + + override suspend fun getUserInfo(): ResponseUserInfo { + return educationService.getUserInfo() + } + + override suspend fun getAddress(): ResponseAddress { + return educationService.getAddress() + } +} \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/RequestDispatchReport.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/RequestDispatchReport.kt new file mode 100644 index 0000000..26b7953 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/RequestDispatchReport.kt @@ -0,0 +1,11 @@ +package com.example.cpr2u_android.data.model.request + + +import com.google.gson.annotations.SerializedName + +data class RequestDispatchReport( + @SerializedName("content") + val content: String, + @SerializedName("dispatch_id") + val dispatchId: Int +) \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/auth/RequestLogin.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/auth/RequestLogin.kt new file mode 100644 index 0000000..0d7da82 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/auth/RequestLogin.kt @@ -0,0 +1,10 @@ +package com.example.cpr2u_android.data.model.request.auth + +import com.google.gson.annotations.SerializedName + +data class RequestLogin( + @SerializedName("device_token") + val deviceToken: String, + @SerializedName("phone_number") + val phoneNumber: String, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/auth/RequestSignUp.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/auth/RequestSignUp.kt new file mode 100644 index 0000000..3a11ed0 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/auth/RequestSignUp.kt @@ -0,0 +1,13 @@ +package com.example.cpr2u_android.data.model.request.auth + + +import com.google.gson.annotations.SerializedName + +data class RequestSignUp( + @SerializedName("device_token") + val deviceToken: String, + @SerializedName("nickname") + val nickname: String, + @SerializedName("phone_number") + val phoneNumber: String +) \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/education/RequestCall.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/education/RequestCall.kt new file mode 100644 index 0000000..e479dc8 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/request/education/RequestCall.kt @@ -0,0 +1,13 @@ +package com.example.cpr2u_android.data.model.request.education + + +import com.google.gson.annotations.SerializedName + +data class RequestCall( + @SerializedName("full_address") + val fullAddress: String, + @SerializedName("latitude") + val latitude: Double, + @SerializedName("longitude") + val longitude: Double +) \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/GeneralResponse.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/GeneralResponse.kt new file mode 100644 index 0000000..a2d3400 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/GeneralResponse.kt @@ -0,0 +1,11 @@ +package com.example.cpr2u_android.data.model.response.auth + + +import com.google.gson.annotations.SerializedName + +data class GeneralResponse( + @SerializedName("status") + val status: Int, + @SerializedName("message") + val message: String, +) \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponseAutoLogin.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponseAutoLogin.kt new file mode 100644 index 0000000..221748d --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponseAutoLogin.kt @@ -0,0 +1,19 @@ +package com.example.cpr2u_android.data.model.response.auth + +import com.google.gson.annotations.SerializedName + +data class ResponseAutoLogin( + @SerializedName("data") + val data: AutoLoginData, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) + +data class AutoLoginData( + @SerializedName("access_token") + val accessToken: String, + @SerializedName("refresh_token") + val refreshToken: String, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponseLogin.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponseLogin.kt new file mode 100644 index 0000000..6d357ff --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponseLogin.kt @@ -0,0 +1,19 @@ +package com.example.cpr2u_android.data.model.response.auth + +import com.google.gson.annotations.SerializedName + +data class ResponseLogin( + @SerializedName("data") + val data: LoginData, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) { + data class LoginData( + @SerializedName("access_token") + val accessToken: String, + @SerializedName("refresh_token") + val refreshToken: String, + ) +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponsePhoneVerification.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponsePhoneVerification.kt new file mode 100644 index 0000000..e1e3502 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/auth/ResponsePhoneVerification.kt @@ -0,0 +1,17 @@ +package com.example.cpr2u_android.data.model.response.auth + +import com.google.gson.annotations.SerializedName + +data class ResponsePhoneVerification( + @SerializedName("data") + val data: ValidationCodeData, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) + +data class ValidationCodeData( + @SerializedName("validation_code") + val validationCode: String, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseAddress.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseAddress.kt new file mode 100644 index 0000000..b5f27e5 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseAddress.kt @@ -0,0 +1,26 @@ +package com.example.cpr2u_android.data.model.response.call + +import com.google.gson.annotations.SerializedName + +data class ResponseAddress( + @SerializedName("data") + val data: List, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) { + data class Data( + @SerializedName("gugun_list") + val gugunList: List, + @SerializedName("sido") + val sido: String, + ) { + data class Gugun( + @SerializedName("gugun") + val gugun: String, + @SerializedName("id") + val id: Int, + ) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseCall.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseCall.kt new file mode 100644 index 0000000..e6329cd --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseCall.kt @@ -0,0 +1,17 @@ +package com.example.cpr2u_android.data.model.response.call + +import com.google.gson.annotations.SerializedName + +data class ResponseCall( + @SerializedName("data") + val data: Data, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) { + data class Data( + @SerializedName("call_id") + val callId: Int, + ) +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseCallList.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseCallList.kt new file mode 100644 index 0000000..64ac2fd --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseCallList.kt @@ -0,0 +1,34 @@ +package com.example.cpr2u_android.data.model.response.call + +import com.google.gson.annotations.SerializedName + +data class ResponseCallList( + @SerializedName("data") + val data: Data, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) { + data class Data( + @SerializedName("angel_status") + val angelStatus: String, + @SerializedName("call_list") + val callList: List, + @SerializedName("is_patient") + val isPatient: Boolean, + ) { + data class Call( + @SerializedName("called_at") + val calledAt: String, + @SerializedName("cpr_call_id") + val cprCallId: Int, + @SerializedName("full_address") + val fullAddress: String, + @SerializedName("latitude") + val latitude: Double, + @SerializedName("longitude") + val longitude: Double, + ) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseDispatch.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseDispatch.kt new file mode 100644 index 0000000..eb92327 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/call/ResponseDispatch.kt @@ -0,0 +1,25 @@ +package com.example.cpr2u_android.data.model.response.call + +import com.google.gson.annotations.SerializedName + +data class ResponseDispatch( + @SerializedName("data") + val data: Data, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) { + data class Data( + @SerializedName("called_at") + val calledAt: String, + @SerializedName("dispatch_id") + val dispatchId: Int, + @SerializedName("full_address") + val fullAddress: String, + @SerializedName("latitude") + val latitude: Double, + @SerializedName("longitude") + val longitude: Double, + ) +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/QuizzesListData.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/QuizzesListData.kt new file mode 100644 index 0000000..d59e96c --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/QuizzesListData.kt @@ -0,0 +1,25 @@ +package com.example.cpr2u_android.data.model.response.education + +import com.google.gson.annotations.SerializedName + +data class QuizzesListData( + @SerializedName("index") + val index: Int, + @SerializedName("question") + val question: String, + @SerializedName("type") + val type: Int, + @SerializedName("answer") + val answer: Int, + @SerializedName("reason") + val reason: String, + @SerializedName("answer_list") + val answerList: List, +) { + data class Answer( + @SerializedName("content") + val content: String, + @SerializedName("id") + val id: Int, + ) +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/ResponseQuizzesList.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/ResponseQuizzesList.kt new file mode 100644 index 0000000..1269417 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/ResponseQuizzesList.kt @@ -0,0 +1,12 @@ +package com.example.cpr2u_android.data.model.response.education + +import com.google.gson.annotations.SerializedName + +data class ResponseQuizzesList( + @SerializedName("data") + val data: List, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/ResponseUserInfo.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/ResponseUserInfo.kt new file mode 100644 index 0000000..e347419 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/model/response/education/ResponseUserInfo.kt @@ -0,0 +1,31 @@ +package com.example.cpr2u_android.data.model.response.education + +import com.google.gson.annotations.SerializedName + +data class ResponseUserInfo( + @SerializedName("data") + val data: Data, + @SerializedName("message") + val message: String, + @SerializedName("status") + val status: Int, +) { + data class Data( + @SerializedName("angel_status") + val angelStatus: Int, + @SerializedName("days_left_until_expiration") + val daysLeftUntilExpiration: Int, + @SerializedName("is_lecture_completed") + val isLectureCompleted: Int, + @SerializedName("is_posture_completed") + val isPostureCompleted: Int, + @SerializedName("is_quiz_completed") + val isQuizCompleted: Int, + @SerializedName("last_lecture_title") + val lastLectureTitle: String, + @SerializedName("nickname") + val nickname: String, + @SerializedName("progress_percent") + val progressPercent: Double, + ) +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/auth/AuthRepositoryImpl.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/auth/AuthRepositoryImpl.kt new file mode 100644 index 0000000..ca5f0b0 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/auth/AuthRepositoryImpl.kt @@ -0,0 +1,34 @@ +package com.example.cpr2u_android.data.repository.auth + +import com.example.cpr2u_android.data.datasource.auth.AuthDataSource +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.model.request.auth.RequestSignUp +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.auth.ResponseAutoLogin +import com.example.cpr2u_android.data.model.response.auth.ResponseLogin +import com.example.cpr2u_android.data.model.response.auth.ResponsePhoneVerification +import com.example.cpr2u_android.domain.repository.auth.AuthRepository +import timber.log.Timber + +class AuthRepositoryImpl(private val authDataSource: AuthDataSource) : AuthRepository { + override suspend fun postAuthLogin(refreshToken: String): ResponseAutoLogin? { + return authDataSource.postAutoLogin(refreshToken) + } + + override suspend fun postVerification(phoneNumber: String): ResponsePhoneVerification { + Timber.d("구현체 phoneNumber -> $phoneNumber") + return authDataSource.postVerification(phoneNumber) + } + + override suspend fun postLogin(loginData: RequestLogin): ResponseLogin { + return authDataSource.postLogin(loginData) + } + + override suspend fun getNickname(nickname: String): GeneralResponse { + return authDataSource.getNickName(nickname) + } + + override suspend fun postSignUp(signUpData: RequestSignUp): ResponseAutoLogin { + return authDataSource.postSignUp(signUpData) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/call/CallRepositoryImpl.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/call/CallRepositoryImpl.kt new file mode 100644 index 0000000..bb0551b --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/call/CallRepositoryImpl.kt @@ -0,0 +1,36 @@ +package com.example.cpr2u_android.data.repository.call + +import com.example.cpr2u_android.data.datasource.call.CallDataSource +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.request.education.RequestCall +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseCall +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.data.model.response.call.ResponseDispatch +import com.example.cpr2u_android.domain.repository.call.CallRepository + +class CallRepositoryImpl(private val callDataSource: CallDataSource): CallRepository { + override suspend fun postCall(data: RequestCall): ResponseCall { + return callDataSource.postCall(data) + } + + override suspend fun postCallEnd(callId: Int): GeneralResponse { + return callDataSource.postCallEnd(callId) + } + + override suspend fun getCallList(): ResponseCallList { + return callDataSource.getCallList() + } + + override suspend fun postDispatch(callID: Int): ResponseDispatch { + return callDataSource.postDispatch(callID) + } + + override suspend fun postDispatchArrive(dispatchId: Int): GeneralResponse { + return callDataSource.postDispatchArrived(dispatchId) + } + + override suspend fun postDispatchReport(data: RequestDispatchReport): GeneralResponse { + return callDataSource.postDispatchReport(data) + } +} \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/education/EducationRepositoryImpl.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/education/EducationRepositoryImpl.kt new file mode 100644 index 0000000..d918f90 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/repository/education/EducationRepositoryImpl.kt @@ -0,0 +1,37 @@ +package com.example.cpr2u_android.data.repository.education + +import com.example.cpr2u_android.data.datasource.education.EducationDataSource +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.education.ResponseQuizzesList +import com.example.cpr2u_android.data.model.response.education.ResponseUserInfo +import com.example.cpr2u_android.domain.repository.education.EducationRepository +import timber.log.Timber + +class EducationRepositoryImpl(private val educationDataSource: EducationDataSource) : + EducationRepository { + override suspend fun postLectureId(lectureId: Int): GeneralResponse { + Timber.d("repository Impl ID -> $lectureId") + return educationDataSource.postLectureId(lectureId) + } + + override suspend fun getQuizzes(): ResponseQuizzesList { + return educationDataSource.getQuizzes() + } + + override suspend fun postQuizProgress(score: Int): GeneralResponse { + return educationDataSource.postQuizProgress(score) + } + + override suspend fun postExercisesProgress(score: Int): GeneralResponse { + return educationDataSource.postExercisesProgress(score) + } + + override suspend fun getUserInfo(): ResponseUserInfo { + return educationDataSource.getUserInfo() + } + + override suspend fun getAddress(): ResponseAddress { + return educationDataSource.getAddress() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/sharedpref/CPR2USharedPreference.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/sharedpref/CPR2USharedPreference.kt new file mode 100644 index 0000000..2467675 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/data/sharedpref/CPR2USharedPreference.kt @@ -0,0 +1,67 @@ +package com.example.cpr2u_android.data.sharedpref + +import android.content.Context +import android.content.SharedPreferences +import timber.log.Timber + +object CPR2USharedPreference { + private const val ACCESS_TOKEN = "ACCESS_TOKEN" + private const val REFRESH_TOKEN = "REFRESH_TOKEN" + private const val DEVICE_TOKEN = "DEVICE_TOKEN" + private const val IS_LOGIN = "IS_LOGIN" + private const val USER_NAME = "USER_NAME" + private const val LOCATION = "LOCATION" + lateinit var preferences: SharedPreferences + fun init(context: Context) { + Timber.d("shared-preference-init") + preferences = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE) + } + + fun getIsLogin(): Boolean { + return preferences.getBoolean(IS_LOGIN, false) + } + + fun setIsLogin(value: Boolean) { + preferences.edit().putBoolean(IS_LOGIN, value).apply() + } + + fun getAccessToken(): String { + return preferences.getString(ACCESS_TOKEN, "") ?: "" + } + + fun setAccessToken(value: String) { + preferences.edit().putString(ACCESS_TOKEN, value).apply() + } + + fun getRefreshToken(): String { + return preferences.getString(REFRESH_TOKEN, "") ?: "" + } + + fun setRefreshToken(value: String) { + preferences.edit().putString(REFRESH_TOKEN, value).apply() + } + + fun getDeviceToken(): String { + return preferences.getString(DEVICE_TOKEN, "") ?: "" + } + + fun setDeviceToken(value: String) { + preferences.edit().putString(DEVICE_TOKEN, value).apply() + } + + fun getUserName(): String { + return preferences.getString(USER_NAME, "") ?: "" + } + + fun setUserName(value: String) { + preferences.edit().putString(USER_NAME, value).apply() + } + + fun getLocation(): String { + return preferences.getString(LOCATION, "") ?: "" + } + + fun setLocation(value: String) { + preferences.edit().putString(LOCATION, value).apply() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/CPR2UApplication.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/CPR2UApplication.kt new file mode 100644 index 0000000..d3706f7 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/CPR2UApplication.kt @@ -0,0 +1,30 @@ +package com.example.cpr2u_android.di + +import android.app.Application +import com.example.cpr2u_android.BuildConfig +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import timber.log.Timber + +class CPR2UApplication : Application() { + override fun onCreate() { + super.onCreate() + + startKoin { + androidContext(this@CPR2UApplication) + modules( + netWorkModule, + dataSourceModule, + repositoryModule, + viewModelModule, + ) + } + + CPR2USharedPreference.init(applicationContext) + + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/DataSourceModule.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/DataSourceModule.kt new file mode 100644 index 0000000..e9ff0a6 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/DataSourceModule.kt @@ -0,0 +1,16 @@ +package com.example.cpr2u_android.di + +import com.example.cpr2u_android.data.api.EducationService +import com.example.cpr2u_android.data.datasource.auth.AuthDataSource +import com.example.cpr2u_android.data.datasource.auth.AuthRemoteDataSource +import com.example.cpr2u_android.data.datasource.call.CallDataSource +import com.example.cpr2u_android.data.datasource.call.CallRemoteDataSource +import com.example.cpr2u_android.data.datasource.education.EducationDataSource +import com.example.cpr2u_android.data.datasource.education.EducationRemoteDataSource +import org.koin.dsl.module + +val dataSourceModule = module { + single { AuthRemoteDataSource(get()) } + single { EducationRemoteDataSource(get()) } + single { CallRemoteDataSource(get()) } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/NetworkModule.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/NetworkModule.kt new file mode 100644 index 0000000..fbc5f16 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/NetworkModule.kt @@ -0,0 +1,46 @@ +package com.example.cpr2u_android.di + +import com.example.cpr2u_android.BuildConfig +import com.example.cpr2u_android.data.api.AuthService +import com.example.cpr2u_android.data.api.CallService +import com.example.cpr2u_android.data.api.EducationService +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import com.google.gson.GsonBuilder +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import org.koin.dsl.module +import retrofit2.Call +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +val netWorkModule = module { + single { + OkHttpClient.Builder() + .addInterceptor( + Interceptor { chain -> + chain.proceed( + chain.request().newBuilder() + .addHeader( + "Authorization", + CPR2USharedPreference.getAccessToken(), + ) + .build(), + ) + }, + ) + .build() + } + + single { + Retrofit.Builder() + .client(get()) + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().setLenient().create())) + .baseUrl(BuildConfig.BASE_URL) + .build() + } + single { + get().create(AuthService::class.java) + } + single { get().create(EducationService::class.java) } + single { get().create(CallService::class.java)} +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/RepositoryModule.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/RepositoryModule.kt new file mode 100644 index 0000000..96d04cf --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/RepositoryModule.kt @@ -0,0 +1,15 @@ +package com.example.cpr2u_android.di + +import com.example.cpr2u_android.data.repository.auth.AuthRepositoryImpl +import com.example.cpr2u_android.data.repository.call.CallRepositoryImpl +import com.example.cpr2u_android.data.repository.education.EducationRepositoryImpl +import com.example.cpr2u_android.domain.repository.auth.AuthRepository +import com.example.cpr2u_android.domain.repository.call.CallRepository +import com.example.cpr2u_android.domain.repository.education.EducationRepository +import org.koin.dsl.module + +val repositoryModule = module { + single { AuthRepositoryImpl(get()) } + single { EducationRepositoryImpl(get()) } + single { CallRepositoryImpl(get()) } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/ViewModelModule.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/ViewModelModule.kt new file mode 100644 index 0000000..35f1745 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/di/ViewModelModule.kt @@ -0,0 +1,15 @@ +package com.example.cpr2u_android.di + +import com.example.cpr2u_android.presentation.auth.AuthViewModel +import com.example.cpr2u_android.presentation.call.CallViewModel +import com.example.cpr2u_android.presentation.education.EducationViewModel +import com.example.cpr2u_android.presentation.splash.SplashViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val viewModelModule = module { + viewModel { SplashViewModel(get()) } + viewModel { AuthViewModel(get()) } + viewModel { EducationViewModel(get()) } + viewModel { CallViewModel(get()) } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/CallInfoBottomSheet.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/CallInfoBottomSheet.kt new file mode 100644 index 0000000..194776a --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/CallInfoBottomSheet.kt @@ -0,0 +1,8 @@ +package com.example.cpr2u_android.domain.model + +data class CallInfoBottomSheet( + var callId: Int, + var distance: String, + var duration: String, + var fullAddress: String, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/CallListInfo.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/CallListInfo.kt new file mode 100644 index 0000000..31076c7 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/CallListInfo.kt @@ -0,0 +1,16 @@ +package com.example.cpr2u_android.domain.model + +import com.google.gson.annotations.SerializedName + +data class CallListInfo( + @SerializedName("called_at") + val calledAt: String, + @SerializedName("cpr_call_id") + val cprCallId: Int, + @SerializedName("full_address") + val fullAddress: String, + @SerializedName("latitude") + val latitude: Double, + @SerializedName("longitude") + val longitude: Double, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/QuizzesListData.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/QuizzesListData.kt new file mode 100644 index 0000000..b224afe --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/QuizzesListData.kt @@ -0,0 +1,25 @@ +package com.example.cpr2u_android.domain.model + +import com.google.gson.annotations.SerializedName + +data class QuizzesListData( + @SerializedName("index") + val index: Int, + @SerializedName("question") + val question: String, + @SerializedName("type") + val type: Int, + @SerializedName("answer") + val answer: Int, + @SerializedName("reason") + val reason: String, + @SerializedName("answer_list") + val answerList: List, +) { + data class Answer( + @SerializedName("content") + val content: String, + @SerializedName("id") + val id: Int, + ) +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/UserInfo.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/UserInfo.kt new file mode 100644 index 0000000..96b2c65 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/model/UserInfo.kt @@ -0,0 +1,22 @@ +package com.example.cpr2u_android.domain.model + +import com.google.gson.annotations.SerializedName + +data class UserInfo( + @SerializedName("angel_status") + var angelStatus: Int, + @SerializedName("days_left_until_expiration") + var daysLeftUntilExpiration: Int, + @SerializedName("is_lecture_completed") + var isLectureCompleted: Int, + @SerializedName("is_posture_completed") + var isPostureCompleted: Int, + @SerializedName("is_quiz_completed") + var isQuizCompleted: Int, + @SerializedName("last_lecture_title") + var lastLectureTitle: String, + @SerializedName("nickname") + var nickname: String, + @SerializedName("progress_percent") + var progressPercent: Double, +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/auth/AuthRepository.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/auth/AuthRepository.kt new file mode 100644 index 0000000..9bad8cd --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/auth/AuthRepository.kt @@ -0,0 +1,19 @@ +package com.example.cpr2u_android.domain.repository.auth + +import android.provider.ContactsContract.CommonDataKinds.Nickname +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.model.request.auth.RequestSignUp +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.auth.ResponseAutoLogin +import com.example.cpr2u_android.data.model.response.auth.ResponseLogin +import com.example.cpr2u_android.data.model.response.auth.ResponsePhoneVerification + +interface AuthRepository { + + suspend fun postAuthLogin(refreshToken: String): ResponseAutoLogin? + suspend fun postVerification(phoneNumber: String): ResponsePhoneVerification + + suspend fun postLogin(loginData: RequestLogin): ResponseLogin + suspend fun getNickname(nickname: String): GeneralResponse + suspend fun postSignUp(signUpData: RequestSignUp): ResponseAutoLogin +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/call/CallRepository.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/call/CallRepository.kt new file mode 100644 index 0000000..2c3fc88 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/call/CallRepository.kt @@ -0,0 +1,20 @@ +package com.example.cpr2u_android.domain.repository.call + +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.request.education.RequestCall +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.call.ResponseCall +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.data.model.response.call.ResponseDispatch + +interface CallRepository { + suspend fun postCall(data: RequestCall): ResponseCall + suspend fun postCallEnd(callId: Int): GeneralResponse + suspend fun getCallList(): ResponseCallList + suspend fun postDispatch(callID: Int): ResponseDispatch + suspend fun postDispatchArrive(dispatchId: Int): GeneralResponse + + suspend fun postDispatchReport(data: RequestDispatchReport): GeneralResponse + +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/education/EducationRepository.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/education/EducationRepository.kt new file mode 100644 index 0000000..1ab088c --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/domain/repository/education/EducationRepository.kt @@ -0,0 +1,17 @@ +package com.example.cpr2u_android.domain.repository.education + +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.response.auth.GeneralResponse +import com.example.cpr2u_android.data.model.response.call.ResponseAddress +import com.example.cpr2u_android.data.model.response.education.ResponseQuizzesList +import com.example.cpr2u_android.data.model.response.education.ResponseUserInfo + +interface EducationRepository { + suspend fun postLectureId(lectureId: Int): GeneralResponse + suspend fun getQuizzes(): ResponseQuizzesList + suspend fun postQuizProgress(score: Int): GeneralResponse + suspend fun postExercisesProgress(score: Int): GeneralResponse + suspend fun getUserInfo(): ResponseUserInfo + + suspend fun getAddress(): ResponseAddress +} \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/VisualizationUtils.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/VisualizationUtils.kt new file mode 100644 index 0000000..af4ca67 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/VisualizationUtils.kt @@ -0,0 +1,123 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package org.tensorflow.lite.examples.poseestimation + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import com.example.cpr2u_android.ml.data.BodyPart +import com.example.cpr2u_android.ml.data.Person +import kotlin.math.max + +/** + * 이미지 또는 비디오 위에 점과 선으로 관절 데이터를 그린다. + */ +object VisualizationUtils { + /** Radius of circle used to draw keypoints. */ + private const val CIRCLE_RADIUS = 6f + + /** Width of line used to connected two keypoints. */ + private const val LINE_WIDTH = 4f + + /** The text size of the person id that will be displayed when the tracker is available. */ + private const val PERSON_ID_TEXT_SIZE = 30f + + /** Distance from person id to the nose keypoint. */ + private const val PERSON_ID_MARGIN = 6f + + /** Pair of keypoints to draw lines between. */ + private val bodyJoints = listOf( + Pair(BodyPart.NOSE, BodyPart.LEFT_EYE), + Pair(BodyPart.NOSE, BodyPart.RIGHT_EYE), + Pair(BodyPart.LEFT_EYE, BodyPart.LEFT_EAR), + Pair(BodyPart.RIGHT_EYE, BodyPart.RIGHT_EAR), + Pair(BodyPart.NOSE, BodyPart.LEFT_SHOULDER), + Pair(BodyPart.NOSE, BodyPart.RIGHT_SHOULDER), + Pair(BodyPart.LEFT_SHOULDER, BodyPart.LEFT_ELBOW), + Pair(BodyPart.LEFT_ELBOW, BodyPart.LEFT_WRIST), + Pair(BodyPart.RIGHT_SHOULDER, BodyPart.RIGHT_ELBOW), + Pair(BodyPart.RIGHT_ELBOW, BodyPart.RIGHT_WRIST), + Pair(BodyPart.LEFT_SHOULDER, BodyPart.RIGHT_SHOULDER), + Pair(BodyPart.LEFT_SHOULDER, BodyPart.LEFT_HIP), + Pair(BodyPart.RIGHT_SHOULDER, BodyPart.RIGHT_HIP), + Pair(BodyPart.LEFT_HIP, BodyPart.RIGHT_HIP), + Pair(BodyPart.LEFT_HIP, BodyPart.LEFT_KNEE), + Pair(BodyPart.LEFT_KNEE, BodyPart.LEFT_ANKLE), + Pair(BodyPart.RIGHT_HIP, BodyPart.RIGHT_KNEE), + Pair(BodyPart.RIGHT_KNEE, BodyPart.RIGHT_ANKLE), + ) + + // Draw line and point indicate body pose + fun drawBodyKeypoints( + input: Bitmap, + persons: List, + isTrackerEnabled: Boolean = false, + ): Bitmap { + val paintCircle = Paint().apply { + strokeWidth = CIRCLE_RADIUS + color = Color.RED + style = Paint.Style.FILL + } + val paintLine = Paint().apply { + strokeWidth = LINE_WIDTH + color = Color.RED + style = Paint.Style.STROKE + } + + val paintText = Paint().apply { + textSize = PERSON_ID_TEXT_SIZE + color = Color.BLUE + textAlign = Paint.Align.LEFT + } + + val output = input.copy(Bitmap.Config.ARGB_8888, true) + val originalSizeCanvas = Canvas(output) + persons.forEach { person -> + // draw person id if tracker is enable + if (isTrackerEnabled) { + person.boundingBox?.let { + val personIdX = max(0f, it.left) + val personIdY = max(0f, it.top) + + originalSizeCanvas.drawText( + person.id.toString(), + personIdX, + personIdY - PERSON_ID_MARGIN, + paintText, + ) + originalSizeCanvas.drawRect(it, paintLine) + } + } + bodyJoints.forEach { + val pointA = person.keyPoints[it.first.position].coordinate + val pointB = person.keyPoints[it.second.position].coordinate + originalSizeCanvas.drawLine(pointA.x, pointA.y, pointB.x, pointB.y, paintLine) + } + + person.keyPoints.forEach { point -> + originalSizeCanvas.drawCircle( + point.coordinate.x, + point.coordinate.y, + CIRCLE_RADIUS, + paintCircle, + ) + } + } + return output + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/YuvToRgbConverter.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/YuvToRgbConverter.kt new file mode 100644 index 0000000..a6a8a25 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/YuvToRgbConverter.kt @@ -0,0 +1,151 @@ +package org.tensorflow.lite.examples.poseestimation + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Rect +import android.media.Image +import android.renderscript.Allocation +import android.renderscript.Element +import android.renderscript.RenderScript +import android.renderscript.ScriptIntrinsicYuvToRGB +import java.nio.ByteBuffer + +class YuvToRgbConverter(context: Context) { + private val rs = RenderScript.create(context) + private val scriptYuvToRgb = ScriptIntrinsicYuvToRGB.create(rs, Element.U8_4(rs)) + + private var pixelCount: Int = -1 + private lateinit var yuvBuffer: ByteBuffer + private lateinit var inputAllocation: Allocation + private lateinit var outputAllocation: Allocation + + @Synchronized + fun yuvToRgb(image: Image, output: Bitmap) { + + // Ensure that the intermediate output byte buffer is allocated + if (!::yuvBuffer.isInitialized) { + pixelCount = image.cropRect.width() * image.cropRect.height() + yuvBuffer = ByteBuffer.allocateDirect( + pixelCount * ImageFormat.getBitsPerPixel(ImageFormat.YUV_420_888) / 8) + } + + // Get the YUV data in byte array form + imageToByteBuffer(image, yuvBuffer) + + // Ensure that the RenderScript inputs and outputs are allocated + if (!::inputAllocation.isInitialized) { + inputAllocation = Allocation.createSized(rs, Element.U8(rs), yuvBuffer.array().size) + } + if (!::outputAllocation.isInitialized) { + outputAllocation = Allocation.createFromBitmap(rs, output) + } + + // Convert YUV to RGB + inputAllocation.copyFrom(yuvBuffer.array()) + scriptYuvToRgb.setInput(inputAllocation) + scriptYuvToRgb.forEach(outputAllocation) + outputAllocation.copyTo(output) + } + + private fun imageToByteBuffer(image: Image, outputBuffer: ByteBuffer) { + assert(image.format == ImageFormat.YUV_420_888) + + val imageCrop = image.cropRect + val imagePlanes = image.planes + val rowData = ByteArray(imagePlanes.first().rowStride) + + imagePlanes.forEachIndexed { planeIndex, plane -> + + // How many values are read in input for each output value written + // Only the Y plane has a value for every pixel, U and V have half the resolution i.e. + // + // Y Plane U Plane V Plane + // =============== ======= ======= + // Y Y Y Y Y Y Y Y U U U U V V V V + // Y Y Y Y Y Y Y Y U U U U V V V V + // Y Y Y Y Y Y Y Y U U U U V V V V + // Y Y Y Y Y Y Y Y U U U U V V V V + // Y Y Y Y Y Y Y Y + // Y Y Y Y Y Y Y Y + // Y Y Y Y Y Y Y Y + val outputStride: Int + + // The index in the output buffer the next value will be written at + // For Y it's zero, for U and V we start at the end of Y and interleave them i.e. + // + // First chunk Second chunk + // =============== =============== + // Y Y Y Y Y Y Y Y U V U V U V U V + // Y Y Y Y Y Y Y Y U V U V U V U V + // Y Y Y Y Y Y Y Y U V U V U V U V + // Y Y Y Y Y Y Y Y U V U V U V U V + // Y Y Y Y Y Y Y Y + // Y Y Y Y Y Y Y Y + // Y Y Y Y Y Y Y Y + var outputOffset: Int + + when (planeIndex) { + 0 -> { + outputStride = 1 + outputOffset = 0 + } + 1 -> { + outputStride = 2 + outputOffset = pixelCount + 1 + } + 2 -> { + outputStride = 2 + outputOffset = pixelCount + } + else -> { + // Image contains more than 3 planes, something strange is going on + return@forEachIndexed + } + } + + val buffer = plane.buffer + val rowStride = plane.rowStride + val pixelStride = plane.pixelStride + + // We have to divide the width and height by two if it's not the Y plane + val planeCrop = if (planeIndex == 0) { + imageCrop + } else { + Rect( + imageCrop.left / 2, + imageCrop.top / 2, + imageCrop.right / 2, + imageCrop.bottom / 2 + ) + } + + val planeWidth = planeCrop.width() + val planeHeight = planeCrop.height() + + buffer.position(rowStride * planeCrop.top + pixelStride * planeCrop.left) + for (row in 0 until planeHeight) { + val length: Int + if (pixelStride == 1 && outputStride == 1) { + // When there is a single stride value for pixel and output, we can just copy + // the entire row in a single step + length = planeWidth + buffer.get(outputBuffer.array(), outputOffset, length) + outputOffset += length + } else { + // When either pixel or output have a stride > 1 we must copy pixel by pixel + length = (planeWidth - 1) * pixelStride + 1 + buffer.get(rowData, 0, length) + for (col in 0 until planeWidth) { + outputBuffer.array()[outputOffset] = rowData[col * pixelStride] + outputOffset += outputStride + } + } + + if (row < planeHeight - 1) { + buffer.position(buffer.position() + rowStride - length) + } + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/camera/CameraSource.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/camera/CameraSource.kt new file mode 100644 index 0000000..79cfa31 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/camera/CameraSource.kt @@ -0,0 +1,349 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.camera + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.graphics.ImageFormat +import android.graphics.Matrix +import android.graphics.Rect +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager +import android.media.ImageReader +import android.os.Handler +import android.os.HandlerThread +import android.util.Log +import android.view.Surface +import android.view.SurfaceView +import com.example.cpr2u_android.ml.data.Person +import com.example.cpr2u_android.ml.ml.MoveNetMultiPose +import com.example.cpr2u_android.ml.ml.PoseClassifier +import com.example.cpr2u_android.ml.ml.PoseDetector +import com.example.cpr2u_android.ml.ml.TrackerType +import kotlinx.coroutines.suspendCancellableCoroutine +import org.tensorflow.lite.examples.poseestimation.VisualizationUtils +import org.tensorflow.lite.examples.poseestimation.YuvToRgbConverter +import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class CameraSource( + private val surfaceView: SurfaceView, + private val listener: CameraSourceListener? = null, +) { + + companion object { + private const val PREVIEW_WIDTH = 640 + private const val PREVIEW_HEIGHT = 480 + + /** Threshold for confidence score. */ + private const val MIN_CONFIDENCE = .2f + private const val TAG = "Camera Source" + } + + private val lock = Any() + private var detector: PoseDetector? = null + private var classifier: PoseClassifier? = null + private var isTrackerEnabled = false + private var yuvConverter: YuvToRgbConverter = YuvToRgbConverter(surfaceView.context) + private lateinit var imageBitmap: Bitmap + + /** Frame count that have been processed so far in an one second interval to calculate FPS. */ + private var fpsTimer: Timer? = null + private var frameProcessedInOneSecondInterval = 0 + private var framesPerSecond = 0 + + /** Detects, characterizes, and connects to a CameraDevice (used for all camera operations) */ + private val cameraManager: CameraManager by lazy { + val context = surfaceView.context + context.getSystemService(Context.CAMERA_SERVICE) as CameraManager + } + + /** Readers used as buffers for camera still shots */ + private var imageReader: ImageReader? = null + + /** The [CameraDevice] that will be opened in this fragment */ + private var camera: CameraDevice? = null + + /** Internal reference to the ongoing [CameraCaptureSession] configured with our parameters */ + private var session: CameraCaptureSession? = null + + /** [HandlerThread] where all buffer reading operations run */ + private var imageReaderThread: HandlerThread? = null + + /** [Handler] corresponding to [imageReaderThread] */ + private var imageReaderHandler: Handler? = null + private var cameraId: String = "" + + suspend fun initCamera() { + camera = openCamera(cameraManager, cameraId) + imageReader = + ImageReader.newInstance(PREVIEW_WIDTH, PREVIEW_HEIGHT, ImageFormat.YUV_420_888, 3) + imageReader?.setOnImageAvailableListener({ reader -> + val image = reader.acquireLatestImage() + if (image != null) { + if (!::imageBitmap.isInitialized) { + imageBitmap = + Bitmap.createBitmap( + PREVIEW_WIDTH, + PREVIEW_HEIGHT, + Bitmap.Config.ARGB_8888, + ) + } + yuvConverter.yuvToRgb(image, imageBitmap) + // Create rotated version for portrait display + val rotateMatrix = Matrix() + rotateMatrix.postRotate(0.0f) + + val rotatedBitmap = Bitmap.createBitmap( + imageBitmap, + 0, + 0, + PREVIEW_WIDTH, + PREVIEW_HEIGHT, + rotateMatrix, + false, + ) + processImage(rotatedBitmap) + image.close() + } + }, imageReaderHandler) + + imageReader?.surface?.let { surface -> + session = createSession(listOf(surface)) + val cameraRequest = camera?.createCaptureRequest( + CameraDevice.TEMPLATE_PREVIEW, + )?.apply { + addTarget(surface) + } + cameraRequest?.build()?.let { + session?.setRepeatingRequest(it, null, null) + } + } + } + + private suspend fun createSession(targets: List): CameraCaptureSession = + suspendCancellableCoroutine { cont -> + camera?.createCaptureSession( + targets, + object : CameraCaptureSession.StateCallback() { + override fun onConfigured(captureSession: CameraCaptureSession) = + cont.resume(captureSession) + + override fun onConfigureFailed(session: CameraCaptureSession) { + cont.resumeWithException(Exception("Session error")) + } + }, + null, + ) + } + + @SuppressLint("MissingPermission") + private suspend fun openCamera(manager: CameraManager, cameraId: String): CameraDevice = + suspendCancellableCoroutine { cont -> + manager.openCamera( + cameraId, + object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) = cont.resume(camera) + + override fun onDisconnected(camera: CameraDevice) { + camera.close() + } + + override fun onError(camera: CameraDevice, error: Int) { + if (cont.isActive) cont.resumeWithException(Exception("Camera error")) + } + }, + imageReaderHandler, + ) + } + + fun prepareCamera() { + for (cameraId in cameraManager.cameraIdList) { + val characteristics = cameraManager.getCameraCharacteristics(cameraId) + + // We don't use a front facing camera in this sample. + val cameraDirection = characteristics.get(CameraCharacteristics.LENS_FACING) + if (cameraDirection != null && + cameraDirection == CameraCharacteristics.LENS_FACING_FRONT + ) { + continue + } + this.cameraId = cameraId + } + } + + fun setDetector(detector: PoseDetector) { + synchronized(lock) { + if (this.detector != null) { + this.detector?.close() + this.detector = null + } + this.detector = detector + } + } + + fun setClassifier(classifier: PoseClassifier?) { + synchronized(lock) { + if (this.classifier != null) { + this.classifier?.close() + this.classifier = null + } + this.classifier = classifier + } + } + + /** + * Set Tracker for Movenet MuiltiPose model. + */ + fun setTracker(trackerType: TrackerType) { + isTrackerEnabled = trackerType != TrackerType.OFF + (this.detector as? MoveNetMultiPose)?.setTracker(trackerType) + } + + fun resume() { + imageReaderThread = HandlerThread("imageReaderThread").apply { start() } + imageReaderHandler = Handler(imageReaderThread!!.looper) + fpsTimer = Timer() + fpsTimer?.scheduleAtFixedRate( + object : TimerTask() { + override fun run() { + framesPerSecond = frameProcessedInOneSecondInterval + frameProcessedInOneSecondInterval = 0 + } + }, + 0, + 1000, + ) + } + + fun close() { + session?.close() + session = null + camera?.close() + camera = null + imageReader?.close() + imageReader = null + stopImageReaderThread() + detector?.close() + detector = null + classifier?.close() + classifier = null + fpsTimer?.cancel() + fpsTimer = null + frameProcessedInOneSecondInterval = 0 + framesPerSecond = 0 + } + + // process image + private fun processImage(bitmap: Bitmap) { + val persons = mutableListOf() + var classificationResult: List>? = null + + synchronized(lock) { + detector?.estimatePoses(bitmap)?.let { + persons.addAll(it) + + // if the model only returns one item, allow running the Pose classifier. + if (persons.isNotEmpty()) { + classifier?.run { + classificationResult = classify(persons[0]) + } + } + } + } + frameProcessedInOneSecondInterval++ + if (frameProcessedInOneSecondInterval == 1) { + // send fps to view + listener?.onFPSListener(framesPerSecond) + } + + // if the model returns only one item, show that item's score. + if (persons.isNotEmpty()) { + listener?.onDetectedInfo(persons[0].score, classificationResult, persons) + } + visualize(persons, bitmap) + } + + /** + * 이미지 위에 사람의 관절 포인트를 시각화한다. + */ + private fun visualize(persons: List, bitmap: Bitmap) { + val outputBitmap = VisualizationUtils.drawBodyKeypoints( + bitmap, + persons.filter { it.score > MIN_CONFIDENCE }, + isTrackerEnabled, + ) + + val holder = surfaceView.holder + val surfaceCanvas = holder.lockCanvas() + surfaceCanvas?.let { canvas -> + val screenWidth: Int + val screenHeight: Int + val left: Int + val top: Int + + if (canvas.height > canvas.width) { + val ratio = outputBitmap.height.toFloat() / outputBitmap.width + screenWidth = canvas.width + left = 0 + screenHeight = (canvas.width * ratio).toInt() + top = (canvas.height - screenHeight) / 2 + } else { + val ratio = outputBitmap.width.toFloat() / outputBitmap.height + screenHeight = canvas.height + top = 0 + screenWidth = (canvas.height * ratio).toInt() + left = (canvas.width - screenWidth) / 2 + } + val right: Int = left + screenWidth + val bottom: Int = top + screenHeight + + canvas.drawBitmap( + outputBitmap, + Rect(0, 0, outputBitmap.width, outputBitmap.height), + Rect(left, top, right, bottom), + null, + ) + surfaceView.holder.unlockCanvasAndPost(canvas) + } + } + + private fun stopImageReaderThread() { + imageReaderThread?.quitSafely() + try { + imageReaderThread?.join() + imageReaderThread = null + imageReaderHandler = null + } catch (e: InterruptedException) { + Log.d(TAG, e.message.toString()) + } + } + + interface CameraSourceListener { + fun onFPSListener(fps: Int) + + fun onDetectedInfo( + personScore: Float?, + poseLabels: List>?, + persons: List, + ) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/BodyPart.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/BodyPart.kt new file mode 100644 index 0000000..096ef8e --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/BodyPart.kt @@ -0,0 +1,56 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.data + +/** + * 우리가 CPR2U에 사용해야 할 관절 종류 + * LEFT_SHOULDER(5), + * RIGHT_SHOULDER(6), + * LEFT_ELBOW(7), + * RIGHT_ELBOW(8), + * LEFT_WRIST(9), + * RIGHT_WRIST(10), + * LEFT_HIP(11), + * RIGHT_HIP(12), + * LEFT_KNEE(13), + * RIGHT_KNEE(14), + * LEFT_ANKLE(15), + * RIGHT_ANKLE(16) + */ +enum class BodyPart(val position: Int) { + NOSE(0), + LEFT_EYE(1), + RIGHT_EYE(2), + LEFT_EAR(3), + RIGHT_EAR(4), + LEFT_SHOULDER(5), + RIGHT_SHOULDER(6), + LEFT_ELBOW(7), + RIGHT_ELBOW(8), + LEFT_WRIST(9), + RIGHT_WRIST(10), + LEFT_HIP(11), + RIGHT_HIP(12), + LEFT_KNEE(13), + RIGHT_KNEE(14), + LEFT_ANKLE(15), + RIGHT_ANKLE(16); + companion object{ + private val map = values().associateBy(BodyPart::position) + fun fromInt(position: Int): BodyPart = map.getValue(position) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/Device.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/Device.kt new file mode 100644 index 0000000..a500f62 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/Device.kt @@ -0,0 +1,26 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.data + +/** + * 모델을 실행할 CPU/GPU 디바이스 옵션 + */ +enum class Device { + CPU, + NNAPI, + GPU +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/KeyPoint.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/KeyPoint.kt new file mode 100644 index 0000000..11c53db --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/KeyPoint.kt @@ -0,0 +1,27 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.data + +import android.graphics.PointF + +/** + * 사진 위에 표현되는 관절 노드(점) + * @param bodyPart: 관절 종류, + * @param coordinate: x, y 좌표 + * @param score: 정확도 + */ +data class KeyPoint(val bodyPart: BodyPart, var coordinate: PointF, val score: Float) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/Person.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/Person.kt new file mode 100644 index 0000000..4fd474b --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/Person.kt @@ -0,0 +1,32 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.data + +import android.graphics.RectF + +/** + * 카메라 속 사람의 자세 데이터를 저장하는 클래스 + * @param keyPoints: 사람의 관절 포인트 + * @param boundingBox: 사람 전체를 둘러싸는 사각형 (Multipose 모델에서만 사용) + * @param score: 인식 정확도 + */ +data class Person( + var id: Int = -1, // default id is -1 + val keyPoints: List, + val boundingBox: RectF? = null, // Only MoveNet MultiPose return bounding box. + val score: Float +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/TorsoAndBodyDistance.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/TorsoAndBodyDistance.kt new file mode 100644 index 0000000..f296272 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/data/TorsoAndBodyDistance.kt @@ -0,0 +1,24 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.data + +data class TorsoAndBodyDistance( + val maxTorsoYDistance: Float, + val maxTorsoXDistance: Float, + val maxBodyYDistance: Float, + val maxBodyXDistance: Float +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/MoveNet.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/MoveNet.kt new file mode 100644 index 0000000..ee589d2 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/MoveNet.kt @@ -0,0 +1,366 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.ml + +import android.content.Context +import android.graphics.* // ktlint-disable no-wildcard-imports +import android.os.SystemClock +import com.example.cpr2u_android.ml.data.* // ktlint-disable no-wildcard-imports +import org.tensorflow.lite.DataType +import org.tensorflow.lite.Interpreter +import org.tensorflow.lite.gpu.GpuDelegate +import org.tensorflow.lite.support.common.FileUtil +import org.tensorflow.lite.support.image.ImageProcessor +import org.tensorflow.lite.support.image.TensorImage +import org.tensorflow.lite.support.image.ops.ResizeOp +import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp +import org.tensorflow.lite.support.tensorbuffer.TensorBuffer +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +enum class ModelType { + Lightning, + Thunder, +} + +/** + * 실제 ML 모델 구동 클래스 (Posenet 클래스와 같은 역할) + */ +class MoveNet(private val interpreter: Interpreter, private var gpuDelegate: GpuDelegate?) : + PoseDetector { + + companion object { + private const val MIN_CROP_KEYPOINT_SCORE = .2f + private const val CPU_NUM_THREADS = 4 + + // Parameters that control how large crop region should be expanded from previous frames' + // body keypoints. + private const val TORSO_EXPANSION_RATIO = 1.9f + private const val BODY_EXPANSION_RATIO = 1.2f + + // TFLite file names. + private const val LIGHTNING_FILENAME = "movenet_lightning.tflite" + private const val THUNDER_FILENAME = "movenet_thunder.tflite" + + // allow specifying model type. + fun create(context: Context, device: Device, modelType: ModelType): MoveNet { + val options = Interpreter.Options() + var gpuDelegate: GpuDelegate? = null + options.setNumThreads(CPU_NUM_THREADS) + when (device) { + Device.CPU -> { + } + Device.GPU -> { + gpuDelegate = GpuDelegate() + options.addDelegate(gpuDelegate) + } + Device.NNAPI -> options.setUseNNAPI(true) + } + return MoveNet( + Interpreter( + FileUtil.loadMappedFile( + context, + if (modelType == ModelType.Lightning) { + LIGHTNING_FILENAME + } else { + THUNDER_FILENAME + }, + ), + options, + ), + gpuDelegate, + ) + } + + // default to lightning. + fun create(context: Context, device: Device): MoveNet = + create(context, device, ModelType.Lightning) + } + + private var cropRegion: RectF? = null + private var lastInferenceTimeNanos: Long = -1 + private val inputWidth = interpreter.getInputTensor(0).shape()[1] + private val inputHeight = interpreter.getInputTensor(0).shape()[2] + private var outputShape: IntArray = interpreter.getOutputTensor(0).shape() + + override fun estimatePoses(bitmap: Bitmap): List { + val inferenceStartTimeNanos = SystemClock.elapsedRealtimeNanos() + if (cropRegion == null) { + cropRegion = initRectF(bitmap.width, bitmap.height) + } + var totalScore = 0f + + val numKeyPoints = outputShape[2] + val keyPoints = mutableListOf() + + cropRegion?.run { + val rect = RectF( + (left * bitmap.width), + (top * bitmap.height), + (right * bitmap.width), + (bottom * bitmap.height), + ) + val detectBitmap = Bitmap.createBitmap( + rect.width().toInt(), + rect.height().toInt(), + Bitmap.Config.ARGB_8888, + ) + Canvas(detectBitmap).drawBitmap( + bitmap, + -rect.left, + -rect.top, + null, + ) + val inputTensor = processInputImage(detectBitmap, inputWidth, inputHeight) + val outputTensor = TensorBuffer.createFixedSize(outputShape, DataType.FLOAT32) + val widthRatio = detectBitmap.width.toFloat() / inputWidth + val heightRatio = detectBitmap.height.toFloat() / inputHeight + + val positions = mutableListOf() + + inputTensor?.let { input -> + interpreter.run(input.buffer, outputTensor.buffer.rewind()) + val output = outputTensor.floatArray + for (idx in 0 until numKeyPoints) { + val x = output[idx * 3 + 1] * inputWidth * widthRatio + val y = output[idx * 3 + 0] * inputHeight * heightRatio + + positions.add(x) + positions.add(y) + val score = output[idx * 3 + 2] + keyPoints.add( + KeyPoint( + BodyPart.fromInt(idx), + PointF( + x, + y, + ), + score, + ), + ) + totalScore += score + } + } + val matrix = Matrix() + val points = positions.toFloatArray() + + matrix.postTranslate(rect.left, rect.top) + matrix.mapPoints(points) + keyPoints.forEachIndexed { index, keyPoint -> + keyPoint.coordinate = + PointF( + points[index * 2], + points[index * 2 + 1], + ) + } + // new crop region + cropRegion = determineRectF(keyPoints, bitmap.width, bitmap.height) + } + lastInferenceTimeNanos = + SystemClock.elapsedRealtimeNanos() - inferenceStartTimeNanos + return listOf(Person(keyPoints = keyPoints, score = totalScore / numKeyPoints)) + } + + override fun lastInferenceTimeNanos(): Long = lastInferenceTimeNanos + + override fun close() { + gpuDelegate?.close() + interpreter.close() + cropRegion = null + } + + /** + * Prepare input image for detection + */ + private fun processInputImage(bitmap: Bitmap, inputWidth: Int, inputHeight: Int): TensorImage? { + val width: Int = bitmap.width + val height: Int = bitmap.height + + val size = if (height > width) width else height + val imageProcessor = ImageProcessor.Builder().apply { + add(ResizeWithCropOrPadOp(size, size)) + add(ResizeOp(inputWidth, inputHeight, ResizeOp.ResizeMethod.BILINEAR)) + }.build() + val tensorImage = TensorImage(DataType.UINT8) + tensorImage.load(bitmap) + return imageProcessor.process(tensorImage) + } + + /** + * Defines the default crop region. + * The function provides the initial crop region (pads the full image from both + * sides to make it a square image) when the algorithm cannot reliably determine + * the crop region from the previous frame. + */ + private fun initRectF(imageWidth: Int, imageHeight: Int): RectF { + val xMin: Float + val yMin: Float + val width: Float + val height: Float + if (imageWidth > imageHeight) { + width = 1f + height = imageWidth.toFloat() / imageHeight + xMin = 0f + yMin = (imageHeight / 2f - imageWidth / 2f) / imageHeight + } else { + height = 1f + width = imageHeight.toFloat() / imageWidth + yMin = 0f + xMin = (imageWidth / 2f - imageHeight / 2) / imageWidth + } + return RectF( + xMin, + yMin, + xMin + width, + yMin + height, + ) + } + + /** + * Checks whether there are enough torso keypoints. + * This function checks whether the model is confident at predicting one of the + * shoulders/hips which is required to determine a good crop region. + */ + private fun torsoVisible(keyPoints: List): Boolean { + return ( + (keyPoints[BodyPart.LEFT_HIP.position].score > MIN_CROP_KEYPOINT_SCORE).or( + keyPoints[BodyPart.RIGHT_HIP.position].score > MIN_CROP_KEYPOINT_SCORE, + ) + ).and( + (keyPoints[BodyPart.LEFT_SHOULDER.position].score > MIN_CROP_KEYPOINT_SCORE).or( + keyPoints[BodyPart.RIGHT_SHOULDER.position].score > MIN_CROP_KEYPOINT_SCORE, + ), + ) + } + + /** + * Determines the region to crop the image for the model to run inference on. + * The algorithm uses the detected joints from the previous frame to estimate + * the square region that encloses the full body of the target person and + * centers at the midpoint of two hip joints. The crop size is determined by + * the distances between each joints and the center point. + * When the model is not confident with the four torso joint predictions, the + * function returns a default crop which is the full image padded to square. + */ + private fun determineRectF( + keyPoints: List, + imageWidth: Int, + imageHeight: Int, + ): RectF { + val targetKeyPoints = mutableListOf() + keyPoints.forEach { + targetKeyPoints.add( + KeyPoint( + it.bodyPart, + PointF( + it.coordinate.x, + it.coordinate.y, + ), + it.score, + ), + ) + } + if (torsoVisible(keyPoints)) { + val centerX = + ( + targetKeyPoints[BodyPart.LEFT_HIP.position].coordinate.x + + targetKeyPoints[BodyPart.RIGHT_HIP.position].coordinate.x + ) / 2f + val centerY = + ( + targetKeyPoints[BodyPart.LEFT_HIP.position].coordinate.y + + targetKeyPoints[BodyPart.RIGHT_HIP.position].coordinate.y + ) / 2f + + val torsoAndBodyDistances = + determineTorsoAndBodyDistances(keyPoints, targetKeyPoints, centerX, centerY) + + val list = listOf( + torsoAndBodyDistances.maxTorsoXDistance * TORSO_EXPANSION_RATIO, + torsoAndBodyDistances.maxTorsoYDistance * TORSO_EXPANSION_RATIO, + torsoAndBodyDistances.maxBodyXDistance * BODY_EXPANSION_RATIO, + torsoAndBodyDistances.maxBodyYDistance * BODY_EXPANSION_RATIO, + ) + + var cropLengthHalf = list.maxOrNull() ?: 0f + val tmp = listOf(centerX, imageWidth - centerX, centerY, imageHeight - centerY) + cropLengthHalf = min(cropLengthHalf, tmp.maxOrNull() ?: 0f) + val cropCorner = Pair(centerY - cropLengthHalf, centerX - cropLengthHalf) + + return if (cropLengthHalf > max(imageWidth, imageHeight) / 2f) { + initRectF(imageWidth, imageHeight) + } else { + val cropLength = cropLengthHalf * 2 + RectF( + cropCorner.second / imageWidth, + cropCorner.first / imageHeight, + (cropCorner.second + cropLength) / imageWidth, + (cropCorner.first + cropLength) / imageHeight, + ) + } + } else { + return initRectF(imageWidth, imageHeight) + } + } + + /** + * Calculates the maximum distance from each keypoints to the center location. + * The function returns the maximum distances from the two sets of keypoints: + * full 17 keypoints and 4 torso keypoints. The returned information will be + * used to determine the crop size. See determineRectF for more detail. + */ + private fun determineTorsoAndBodyDistances( + keyPoints: List, + targetKeyPoints: List, + centerX: Float, + centerY: Float, + ): TorsoAndBodyDistance { + val torsoJoints = listOf( + BodyPart.LEFT_SHOULDER.position, + BodyPart.RIGHT_SHOULDER.position, + BodyPart.LEFT_HIP.position, + BodyPart.RIGHT_HIP.position, + ) + + var maxTorsoYRange = 0f + var maxTorsoXRange = 0f + torsoJoints.forEach { joint -> + val distY = abs(centerY - targetKeyPoints[joint].coordinate.y) + val distX = abs(centerX - targetKeyPoints[joint].coordinate.x) + if (distY > maxTorsoYRange) maxTorsoYRange = distY + if (distX > maxTorsoXRange) maxTorsoXRange = distX + } + + var maxBodyYRange = 0f + var maxBodyXRange = 0f + for (joint in keyPoints.indices) { + if (keyPoints[joint].score < MIN_CROP_KEYPOINT_SCORE) continue + val distY = abs(centerY - keyPoints[joint].coordinate.y) + val distX = abs(centerX - keyPoints[joint].coordinate.x) + + if (distY > maxBodyYRange) maxBodyYRange = distY + if (distX > maxBodyXRange) maxBodyXRange = distX + } + return TorsoAndBodyDistance( + maxTorsoYRange, + maxTorsoXRange, + maxBodyYRange, + maxBodyXRange, + ) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/MoveNetMultiPose.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/MoveNetMultiPose.kt new file mode 100644 index 0000000..a35b890 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/MoveNetMultiPose.kt @@ -0,0 +1,319 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.ml + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.PointF +import android.graphics.RectF +import android.os.SystemClock +import com.example.cpr2u_android.ml.data.BodyPart +import com.example.cpr2u_android.ml.data.Device +import com.example.cpr2u_android.ml.data.KeyPoint +import com.example.cpr2u_android.ml.data.Person +import com.example.cpr2u_android.ml.tracker.AbstractTracker +import com.example.cpr2u_android.ml.tracker.BoundingBoxTracker +import com.example.cpr2u_android.ml.tracker.KeyPointsTracker +import org.tensorflow.lite.DataType +import org.tensorflow.lite.Interpreter +import org.tensorflow.lite.gpu.GpuDelegate +import org.tensorflow.lite.support.common.FileUtil +import org.tensorflow.lite.support.image.ImageOperator +import org.tensorflow.lite.support.image.ImageProcessor +import org.tensorflow.lite.support.image.TensorImage +import org.tensorflow.lite.support.image.ops.ResizeOp +import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp +import org.tensorflow.lite.support.tensorbuffer.TensorBuffer +import timber.log.Timber +import kotlin.math.ceil + +/** + * 여러 사람의 자세를 추출하는 클래스(사용 X) + */ +class MoveNetMultiPose( + private val interpreter: Interpreter, + private val type: Type, + private val gpuDelegate: GpuDelegate?, +) : PoseDetector { + private val outputShape = interpreter.getOutputTensor(0).shape() + private val inputShape = interpreter.getInputTensor(0).shape() + private var imageWidth: Int = 0 + private var imageHeight: Int = 0 + private var targetWidth: Int = 0 + private var targetHeight: Int = 0 + private var scaleHeight: Int = 0 + private var scaleWidth: Int = 0 + private var lastInferenceTimeNanos: Long = -1 + private var tracker: AbstractTracker? = null + + companion object { + private const val DYNAMIC_MODEL_TARGET_INPUT_SIZE = 256 + private const val SHAPE_MULTIPLE = 32.0 + private const val DETECTION_THRESHOLD = 0.11 + private const val DETECTION_SCORE_INDEX = 55 + private const val BOUNDING_BOX_Y_MIN_INDEX = 51 + private const val BOUNDING_BOX_X_MIN_INDEX = 52 + private const val BOUNDING_BOX_Y_MAX_INDEX = 53 + private const val BOUNDING_BOX_X_MAX_INDEX = 54 + private const val KEYPOINT_COUNT = 17 + private const val OUTPUTS_COUNT_PER_KEYPOINT = 3 + private const val CPU_NUM_THREADS = 4 + + // allow specifying model type. + fun create( + context: Context, + device: Device, + type: Type, + ): MoveNetMultiPose { + val options = Interpreter.Options() + var gpuDelegate: GpuDelegate? = null + when (device) { + Device.CPU -> { + options.setNumThreads(CPU_NUM_THREADS) + } + Device.GPU -> { + // only fixed model support Gpu delegate option. + if (type == Type.Fixed) { + gpuDelegate = GpuDelegate() + options.addDelegate(gpuDelegate) + } + } + else -> { + // nothing to do + } + } + return MoveNetMultiPose( + Interpreter( + FileUtil.loadMappedFile( + context, + if (type == Type.Dynamic) + "movenet_multipose_fp16.tflite" else "" + //@TODO: (khanhlvg) Add support for fixed shape model if it's released. + ), options + ), type, gpuDelegate + ) + } + } + + /** + * Convert x and y coordinates ([0-1]) returns from the TFlite model + * to the coordinates corresponding to the input image. + */ + private fun resizeKeypoint(x: Float, y: Float): PointF { + return PointF(resizeX(x), resizeY(y)) + } + + private fun resizeX(x: Float): Float { + return if (imageWidth > imageHeight) { + val ratioWidth = imageWidth.toFloat() / targetWidth + x * targetWidth * ratioWidth + } else { + val detectedWidth = + if (type == Type.Dynamic) targetWidth else inputShape[2] + val paddingWidth = detectedWidth - scaleWidth + val ratioWidth = imageWidth.toFloat() / scaleWidth + (x * detectedWidth - paddingWidth / 2f) * ratioWidth + } + } + + private fun resizeY(y: Float): Float { + return if (imageWidth > imageHeight) { + val detectedHeight = + if (type == Type.Dynamic) targetHeight else inputShape[1] + val paddingHeight = detectedHeight - scaleHeight + val ratioHeight = imageHeight.toFloat() / scaleHeight + (y * detectedHeight - paddingHeight / 2f) * ratioHeight + } else { + val ratioHeight = imageHeight.toFloat() / targetHeight + y * targetHeight * ratioHeight + } + } + + /** + * Prepare input image for detection + */ + private fun processInputTensor(bitmap: Bitmap): TensorImage { + imageWidth = bitmap.width + imageHeight = bitmap.height + + // if model type is fixed. get input size from input shape. + val inputSizeHeight = + if (type == Type.Dynamic) DYNAMIC_MODEL_TARGET_INPUT_SIZE else inputShape[1] + val inputSizeWidth = + if (type == Type.Dynamic) DYNAMIC_MODEL_TARGET_INPUT_SIZE else inputShape[2] + + val resizeOp: ImageOperator + if (imageWidth > imageHeight) { + val scale = inputSizeWidth / imageWidth.toFloat() + targetWidth = inputSizeWidth + scaleHeight = ceil(imageHeight * scale).toInt() + targetHeight = (ceil((scaleHeight / SHAPE_MULTIPLE)) * SHAPE_MULTIPLE).toInt() + resizeOp = ResizeOp(scaleHeight, targetWidth, ResizeOp.ResizeMethod.BILINEAR) + } else { + val scale = inputSizeHeight / imageHeight.toFloat() + targetHeight = inputSizeHeight + scaleWidth = ceil(imageWidth * scale).toInt() + targetWidth = (ceil((scaleWidth / SHAPE_MULTIPLE)) * SHAPE_MULTIPLE).toInt() + resizeOp = ResizeOp(targetHeight, scaleWidth, ResizeOp.ResizeMethod.BILINEAR) + } + + val resizeWithCropOrPad = if (type == Type.Dynamic) { + ResizeWithCropOrPadOp( + targetHeight, + targetWidth, + ) + } else { + ResizeWithCropOrPadOp( + inputSizeHeight, + inputSizeWidth, + ) + } + val imageProcessor = ImageProcessor.Builder().apply { + add(resizeOp) + add(resizeWithCropOrPad) + }.build() + val tensorImage = TensorImage(DataType.UINT8) + tensorImage.load(bitmap) + return imageProcessor.process(tensorImage) + } + + /** + * Run tracker (if available) and process the output. + */ + private fun postProcess(modelOutput: FloatArray): List { + val persons = mutableListOf() + for (idx in modelOutput.indices step outputShape[2]) { + val personScore = modelOutput[idx + DETECTION_SCORE_INDEX] + if (personScore < DETECTION_THRESHOLD) continue + val positions = modelOutput.copyOfRange(idx, idx + 51) + val keyPoints = mutableListOf() + for (i in 0 until KEYPOINT_COUNT) { + val y = positions[i * OUTPUTS_COUNT_PER_KEYPOINT] + val x = positions[i * OUTPUTS_COUNT_PER_KEYPOINT + 1] + val score = positions[i * OUTPUTS_COUNT_PER_KEYPOINT + 2] + keyPoints.add(KeyPoint(BodyPart.fromInt(i), PointF(x, y), score)) + } + val yMin = modelOutput[idx + BOUNDING_BOX_Y_MIN_INDEX] + val xMin = modelOutput[idx + BOUNDING_BOX_X_MIN_INDEX] + val yMax = modelOutput[idx + BOUNDING_BOX_Y_MAX_INDEX] + val xMax = modelOutput[idx + BOUNDING_BOX_X_MAX_INDEX] + val boundingBox = RectF(xMin, yMin, xMax, yMax) + persons.add( + Person( + keyPoints = keyPoints, + boundingBox = boundingBox, + score = personScore, + ), + ) + } + + if (persons.isEmpty()) return emptyList() + + if (tracker == null) { + persons.forEach { + it.keyPoints.forEach { key -> + key.coordinate = resizeKeypoint(key.coordinate.x, key.coordinate.y) + } + } + return persons + } else { + val trackPersons = mutableListOf() + tracker?.apply(persons, System.currentTimeMillis() * 1000)?.forEach { + val resizeKeyPoint = mutableListOf() + it.keyPoints.forEach { key -> + resizeKeyPoint.add( + KeyPoint( + key.bodyPart, + resizeKeypoint(key.coordinate.x, key.coordinate.y), + key.score, + ), + ) + } + + var resizeBoundingBox: RectF? = null + it.boundingBox?.let { boundingBox -> + resizeBoundingBox = RectF( + resizeX(boundingBox.left), + resizeY(boundingBox.top), + resizeX(boundingBox.right), + resizeY(boundingBox.bottom), + ) + } + trackPersons.add(Person(it.id, resizeKeyPoint, resizeBoundingBox, it.score)) + } + return trackPersons + } + } + + /** + * Create and set tracker. + */ + fun setTracker(trackerType: TrackerType) { + tracker = when (trackerType) { + TrackerType.BOUNDING_BOX -> { + BoundingBoxTracker() + } + TrackerType.KEYPOINTS -> { + KeyPointsTracker() + } + TrackerType.OFF -> { + null + } + } + } + + /** + * Run TFlite model and Returns a list of "Person" corresponding to the input image. + */ + override fun estimatePoses(bitmap: Bitmap): List { + val inferenceStartTimeNanos = SystemClock.elapsedRealtimeNanos() + val inputTensor = processInputTensor(bitmap) + val outputTensor = TensorBuffer.createFixedSize(outputShape, DataType.FLOAT32) + + // if model is dynamic, resize input before run interpreter + if (type == Type.Dynamic) { + val inputShape = intArrayOf(1).plus(inputTensor.tensorBuffer.shape) + interpreter.resizeInput(0, inputShape, true) + interpreter.allocateTensors() + } + interpreter.run(inputTensor.buffer, outputTensor.buffer.rewind()) + + val processedPerson = postProcess(outputTensor.floatArray) + lastInferenceTimeNanos = + SystemClock.elapsedRealtimeNanos() - inferenceStartTimeNanos + return processedPerson + } + + override fun lastInferenceTimeNanos(): Long = lastInferenceTimeNanos + + /** + * Close all resources when not in use. + */ + override fun close() { + gpuDelegate?.close() + interpreter.close() + tracker = null + } +} + +enum class Type { + Dynamic, Fixed +} + +enum class TrackerType { + OFF, BOUNDING_BOX, KEYPOINTS +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseClassifier.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseClassifier.kt new file mode 100644 index 0000000..83be262 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseClassifier.kt @@ -0,0 +1,80 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.ml + +import android.content.Context +import com.example.cpr2u_android.ml.data.Person +import org.tensorflow.lite.Interpreter +import org.tensorflow.lite.support.common.FileUtil + +/** + * PoseNet에서 추출한 데이터로 자세의 종류를 분류하는 클래스(현재 자세가 코브라 자세인지, 의자 자세인지, 전사 자세인지 등.. 건드릴 필요 없음) + */ +class PoseClassifier( + private val interpreter: Interpreter, + private val labels: List, +) { + private val input = interpreter.getInputTensor(0).shape() + private val output = interpreter.getOutputTensor(0).shape() + + companion object { + private const val MODEL_FILENAME = "classifier.tflite" + private const val LABELS_FILENAME = "labels.txt" + private const val CPU_NUM_THREADS = 4 + + fun create(context: Context): PoseClassifier { + val options = Interpreter.Options().apply { + setNumThreads(CPU_NUM_THREADS) + } + return PoseClassifier( + Interpreter( + FileUtil.loadMappedFile( + context, + MODEL_FILENAME, + ), + options, + ), + FileUtil.loadLabels(context, LABELS_FILENAME), + ) + } + } + + fun classify(person: Person?): List> { + // Preprocess the pose estimation result to a flat array + val inputVector = FloatArray(input[1]) + person?.keyPoints?.forEachIndexed { index, keyPoint -> + // Log.e("keyPoint", "keyPoint.x : " + keyPoint.coordinate.x); + // Log.e("keyPoint", "keyPoint.y : " + keyPoint.coordinate.y); + inputVector[index * 3] = keyPoint.coordinate.y + inputVector[index * 3 + 1] = keyPoint.coordinate.x + inputVector[index * 3 + 2] = keyPoint.score + } + + // Postprocess the model output to human readable class names + val outputTensor = FloatArray(output[1]) + interpreter.run(arrayOf(inputVector), arrayOf(outputTensor)) + val output = mutableListOf>() + outputTensor.forEachIndexed { index, score -> + output.add(Pair(labels[index], score)) + } + return output + } + + fun close() { + interpreter.close() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseDetector.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseDetector.kt new file mode 100644 index 0000000..cc26816 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseDetector.kt @@ -0,0 +1,30 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.ml + +import android.graphics.Bitmap +import com.example.cpr2u_android.ml.data.Person + +/** + * 자세 인식 모델 인터페이스 + */ +interface PoseDetector : AutoCloseable { + + fun estimatePoses(bitmap: Bitmap): List + + fun lastInferenceTimeNanos(): Long +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseNet.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseNet.kt new file mode 100644 index 0000000..703e7a5 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/ml/PoseNet.kt @@ -0,0 +1,268 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.ml + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.PointF +import android.os.SystemClock +import android.util.Log +import com.example.cpr2u_android.ml.data.BodyPart +import com.example.cpr2u_android.ml.data.Device +import com.example.cpr2u_android.ml.data.KeyPoint +import com.example.cpr2u_android.ml.data.Person +import org.tensorflow.lite.DataType +import org.tensorflow.lite.Interpreter +import org.tensorflow.lite.gpu.GpuDelegate +import org.tensorflow.lite.support.common.FileUtil +import org.tensorflow.lite.support.common.ops.NormalizeOp +import org.tensorflow.lite.support.image.ImageProcessor +import org.tensorflow.lite.support.image.TensorImage +import org.tensorflow.lite.support.image.ops.ResizeOp +import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp +import kotlin.math.exp + +/** + * PoseDetector 인터페이스를 구현해 이미지/비디오에서 자세를 추출하는 모델 동작(건드릴 부분 없음) + */ +class PoseNet(private val interpreter: Interpreter, private var gpuDelegate: GpuDelegate?) : + PoseDetector { + + companion object { + private const val CPU_NUM_THREADS = 4 + private const val MEAN = 127.5f + private const val STD = 127.5f + private const val TAG = "Posenet" + private const val MODEL_FILENAME = "posenet.tflite" + + fun create(context: Context, device: Device): PoseNet { + val options = Interpreter.Options() + var gpuDelegate: GpuDelegate? = null + options.setNumThreads(CPU_NUM_THREADS) + when (device) { + Device.CPU -> { + } + Device.GPU -> { + gpuDelegate = GpuDelegate() + options.addDelegate(gpuDelegate) + } + Device.NNAPI -> options.setUseNNAPI(true) + } + return PoseNet( + Interpreter( + FileUtil.loadMappedFile( + context, + MODEL_FILENAME, + ), + options, + ), + gpuDelegate, + ) + } + } + + private var lastInferenceTimeNanos: Long = -1 + private val inputWidth = interpreter.getInputTensor(0).shape()[1] + private val inputHeight = interpreter.getInputTensor(0).shape()[2] + private var cropHeight = 0f + private var cropWidth = 0f + private var cropSize = 0 + + /** + * 프레임(비트맵)을 파라미터로 받아 자세 인식 + */ + @Suppress("UNCHECKED_CAST") + override fun estimatePoses(bitmap: Bitmap): List { + val estimationStartTimeNanos = SystemClock.elapsedRealtimeNanos() + val inputArray = arrayOf(processInputImage(bitmap).tensorBuffer.buffer) + Log.i( + TAG, + String.format( + "Scaling to [-1,1] took %.2f ms", + (SystemClock.elapsedRealtimeNanos() - estimationStartTimeNanos) / 1_000_000f, + ), + ) + + val outputMap = initOutputMap(interpreter) + + val inferenceStartTimeNanos = SystemClock.elapsedRealtimeNanos() + interpreter.runForMultipleInputsOutputs(inputArray, outputMap) + lastInferenceTimeNanos = SystemClock.elapsedRealtimeNanos() - inferenceStartTimeNanos + Log.i( + TAG, + String.format("Interpreter took %.2f ms", 1.0f * lastInferenceTimeNanos / 1_000_000), + ) + + val heatmaps = outputMap[0] as Array>> + val offsets = outputMap[1] as Array>> + + val postProcessingStartTimeNanos = SystemClock.elapsedRealtimeNanos() + val person = postProcessModelOuputs(heatmaps, offsets) + Log.i( + TAG, + String.format( + "Postprocessing took %.2f ms", + (SystemClock.elapsedRealtimeNanos() - postProcessingStartTimeNanos) / 1_000_000f, + ), + ) + + return listOf(person) + } + + /** + * Convert heatmaps and offsets output of Posenet into a list of keypoints + */ + private fun postProcessModelOuputs( + heatmaps: Array>>, + offsets: Array>>, + ): Person { + val height = heatmaps[0].size + val width = heatmaps[0][0].size + val numKeypoints = heatmaps[0][0][0].size + + // Finds the (row, col) locations of where the keypoints are most likely to be. + val keypointPositions = Array(numKeypoints) { Pair(0, 0) } + for (keypoint in 0 until numKeypoints) { + var maxVal = heatmaps[0][0][0][keypoint] + var maxRow = 0 + var maxCol = 0 + for (row in 0 until height) { + for (col in 0 until width) { + if (heatmaps[0][row][col][keypoint] > maxVal) { + maxVal = heatmaps[0][row][col][keypoint] + maxRow = row + maxCol = col + } + } + } + keypointPositions[keypoint] = Pair(maxRow, maxCol) + } + + // Calculating the x and y coordinates of the keypoints with offset adjustment. + val xCoords = IntArray(numKeypoints) + val yCoords = IntArray(numKeypoints) + val confidenceScores = FloatArray(numKeypoints) + keypointPositions.forEachIndexed { idx, position -> + val positionY = keypointPositions[idx].first + val positionX = keypointPositions[idx].second + + val inputImageCoordinateY = + position.first / (height - 1).toFloat() * inputHeight + offsets[0][positionY][positionX][idx] + val ratioHeight = cropSize.toFloat() / inputHeight + val paddingHeight = cropHeight / 2 + yCoords[idx] = (inputImageCoordinateY * ratioHeight - paddingHeight).toInt() + + val inputImageCoordinateX = + position.second / (width - 1).toFloat() * inputWidth + offsets[0][positionY][positionX][idx + numKeypoints] + val ratioWidth = cropSize.toFloat() / inputWidth + val paddingWidth = cropWidth / 2 + xCoords[idx] = (inputImageCoordinateX * ratioWidth - paddingWidth).toInt() + + confidenceScores[idx] = sigmoid(heatmaps[0][positionY][positionX][idx]) + } + + val keypointList = mutableListOf() + var totalScore = 0.0f + enumValues().forEachIndexed { idx, it -> + keypointList.add( + KeyPoint( + it, + PointF(xCoords[idx].toFloat(), yCoords[idx].toFloat()), + confidenceScores[idx], + ), + ) + totalScore += confidenceScores[idx] + } + return Person(keyPoints = keypointList.toList(), score = totalScore / numKeypoints) + } + + override fun lastInferenceTimeNanos(): Long = lastInferenceTimeNanos + + override fun close() { + gpuDelegate?.close() + interpreter.close() + } + + /** + * Scale and crop the input image to a TensorImage. + */ + private fun processInputImage(bitmap: Bitmap): TensorImage { + // reset crop width and height + cropWidth = 0f + cropHeight = 0f + cropSize = if (bitmap.width > bitmap.height) { + cropHeight = (bitmap.width - bitmap.height).toFloat() + bitmap.width + } else { + cropWidth = (bitmap.height - bitmap.width).toFloat() + bitmap.height + } + + val imageProcessor = ImageProcessor.Builder().apply { + add(ResizeWithCropOrPadOp(cropSize, cropSize)) + add(ResizeOp(inputWidth, inputHeight, ResizeOp.ResizeMethod.BILINEAR)) + add(NormalizeOp(MEAN, STD)) + }.build() + val tensorImage = TensorImage(DataType.FLOAT32) + tensorImage.load(bitmap) + return imageProcessor.process(tensorImage) + } + + /** + * Initializes an outputMap of 1 * x * y * z FloatArrays for the model processing to populate. + */ + private fun initOutputMap(interpreter: Interpreter): HashMap { + val outputMap = HashMap() + + // 1 * 9 * 9 * 17 contains heatmaps + val heatmapsShape = interpreter.getOutputTensor(0).shape() + outputMap[0] = Array(heatmapsShape[0]) { + Array(heatmapsShape[1]) { + Array(heatmapsShape[2]) { FloatArray(heatmapsShape[3]) } + } + } + + // 1 * 9 * 9 * 34 contains offsets + val offsetsShape = interpreter.getOutputTensor(1).shape() + outputMap[1] = Array(offsetsShape[0]) { + Array(offsetsShape[1]) { Array(offsetsShape[2]) { FloatArray(offsetsShape[3]) } } + } + + // 1 * 9 * 9 * 32 contains forward displacements + val displacementsFwdShape = interpreter.getOutputTensor(2).shape() + outputMap[2] = Array(offsetsShape[0]) { + Array(displacementsFwdShape[1]) { + Array(displacementsFwdShape[2]) { FloatArray(displacementsFwdShape[3]) } + } + } + + // 1 * 9 * 9 * 32 contains backward displacements + val displacementsBwdShape = interpreter.getOutputTensor(3).shape() + outputMap[3] = Array(displacementsBwdShape[0]) { + Array(displacementsBwdShape[1]) { + Array(displacementsBwdShape[2]) { FloatArray(displacementsBwdShape[3]) } + } + } + + return outputMap + } + + /** Returns value within [0,1]. */ + private fun sigmoid(x: Float): Float { + return (1.0f / (1.0f + exp(-x))) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/AbstractTracker.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/AbstractTracker.kt new file mode 100644 index 0000000..a8e02a3 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/AbstractTracker.kt @@ -0,0 +1,158 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.tracker + +import com.example.cpr2u_android.ml.data.Person + + +abstract class AbstractTracker(val config: TrackerConfig) { + + private val maxAge = config.maxAge * 1000 // convert milliseconds to microseconds + private var nextTrackId = 0 + var tracks = mutableListOf() + private set + + /** + * Computes pairwise similarity scores between detections and tracks, based + * on detected features. + * @param persons A list of detected person. + * @returns A list of shape [num_det, num_tracks] with pairwise + * similarity scores between detections and tracks. + */ + abstract fun computeSimilarity(persons: List): List> + + /** + * Tracks person instances across frames based on detections. + * @param persons A list of person + * @param timestamp The current timestamp in microseconds + * @return An updated list of persons with tracking id. + */ + fun apply(persons: List, timestamp: Long): List { + tracks = filterOldTrack(timestamp).toMutableList() + val simMatrix = computeSimilarity(persons) + assignTrack(persons, simMatrix, timestamp) + tracks = updateTrack().toMutableList() + return persons + } + + /** + * Clear all track in list of tracks + */ + fun reset() { + tracks.clear() + } + + /** + * Return the next track id + */ + private fun nextTrackID() = ++nextTrackId + + /** + * Performs a greedy optimization to link detections with tracks. The person + * list is updated in place by providing an `id` property. If incoming + * detections are not linked with existing tracks, new tracks will be created. + * @param persons A list of detected person. It's assumed that persons are + * sorted from most confident to least confident. + * @param simMatrix A list of shape [num_det, num_tracks] with pairwise + * similarity scores between detections and tracks. + * @param timestamp The current timestamp in microseconds. + */ + private fun assignTrack(persons: List, simMatrix: List>, timestamp: Long) { + if ((simMatrix.size != persons.size) != (simMatrix[0].size != tracks.size)) { + throw IllegalArgumentException("Size of person array and similarity matrix does not match.") + } + + val unmatchedTrackIndices = MutableList(tracks.size) { it } + val unmatchedDetectionIndices = mutableListOf() + + for (detectionIndex in persons.indices) { + // If the track list is empty, add the person's index + // to unmatched detections to create a new track later. + if (unmatchedTrackIndices.isEmpty()) { + unmatchedDetectionIndices.add(detectionIndex) + continue + } + + // Assign the detection to the track which produces the highest pairwise + // similarity score, assuming the score exceeds the minimum similarity + // threshold. + var maxTrackIndex = -1 + var maxSimilarity = -1f + unmatchedTrackIndices.forEach { trackIndex -> + val similarity = simMatrix[detectionIndex][trackIndex] + if (similarity >= config.minSimilarity && similarity > maxSimilarity) { + maxTrackIndex = trackIndex + maxSimilarity = similarity + } + } + if (maxTrackIndex >= 0) { + val linkedTrack = tracks[maxTrackIndex] + tracks[maxTrackIndex] = + createTrack(persons[detectionIndex], linkedTrack.person.id, timestamp) + persons[detectionIndex].id = linkedTrack.person.id + val index = unmatchedTrackIndices.indexOf(maxTrackIndex) + unmatchedTrackIndices.removeAt(index) + } else { + unmatchedDetectionIndices.add(detectionIndex) + } + } + + // Spawn new tracks for all unmatched detections. + unmatchedDetectionIndices.forEach { detectionIndex -> + val newTrack = createTrack(persons[detectionIndex], timestamp = timestamp) + tracks.add(newTrack) + persons[detectionIndex].id = newTrack.person.id + } + } + + /** + * Filters tracks based on their age. + * @param timestamp The timestamp in microseconds + */ + private fun filterOldTrack(timestamp: Long): List { + return tracks.filter { + timestamp - it.lastTimestamp <= maxAge + } + } + + /** + * Sort the track list by timestamp (newer first) + * and return the track list with size equal to config.maxTracks + */ + private fun updateTrack(): List { + tracks.sortByDescending { it.lastTimestamp } + return tracks.take(config.maxTracks) + } + + /** + * Create a new track from person's information. + * @param person A person + * @param id The Id assign to the new track. If it is null, assign the next track id. + * @param timestamp The timestamp in microseconds + */ + private fun createTrack(person: Person, id: Int? = null, timestamp: Long): Track { + return Track( + person = Person( + id = id ?: nextTrackID(), + keyPoints = person.keyPoints, + boundingBox = person.boundingBox, + score = person.score, + ), + lastTimestamp = timestamp, + ) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/BoundingBoxTracker.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/BoundingBoxTracker.kt new file mode 100644 index 0000000..425af51 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/BoundingBoxTracker.kt @@ -0,0 +1,63 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.tracker + +import androidx.annotation.VisibleForTesting +import com.example.cpr2u_android.ml.data.Person +import kotlin.math.max +import kotlin.math.min + +/** + * BoundingBoxTracker, which tracks objects based on bounding box similarity, + * currently defined as intersection-over-union (IoU). + */ +class BoundingBoxTracker(config: TrackerConfig = TrackerConfig()) : AbstractTracker(config) { + + /** + * Computes similarity based on intersection-over-union (IoU). See `AbstractTracker` + * for more details. + */ + override fun computeSimilarity(persons: List): List> { + if (persons.isEmpty() && tracks.isEmpty()) { + return emptyList() + } + return persons.map { person -> tracks.map { track -> iou(person, track.person) } } + } + + /** + * Computes the intersection-over-union (IoU) between a person and a track person. + * @param person1 A person + * @param person2 A track person + * @return The IoU between the person and the track person. This number is + * between 0 and 1, and larger values indicate more box similarity. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun iou(person1: Person, person2: Person): Float { + if (person1.boundingBox != null && person2.boundingBox != null) { + val xMin = max(person1.boundingBox.left, person2.boundingBox.left) + val yMin = max(person1.boundingBox.top, person2.boundingBox.top) + val xMax = min(person1.boundingBox.right, person2.boundingBox.right) + val yMax = min(person1.boundingBox.bottom, person2.boundingBox.bottom) + if (xMin >= xMax || yMin >= yMax) return 0f + val intersection = (xMax - xMin) * (yMax - yMin) + val areaPerson = person1.boundingBox.width() * person1.boundingBox.height() + val areaTrack = person2.boundingBox.width() * person2.boundingBox.height() + return intersection / (areaPerson + areaTrack - intersection) + } + return 0f + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/KeyPointsTracker.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/KeyPointsTracker.kt new file mode 100644 index 0000000..e6de110 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/KeyPointsTracker.kt @@ -0,0 +1,125 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.tracker + +import androidx.annotation.VisibleForTesting +import com.example.cpr2u_android.ml.data.KeyPoint +import com.example.cpr2u_android.ml.data.Person +import kotlin.math.exp +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow + +/** + * KeypointTracker, which tracks poses based on keypoint similarity. This + * tracker assumes that keypoints are provided in normalized image + * coordinates. + */ +class KeyPointsTracker( + trackerConfig: TrackerConfig = TrackerConfig( + keyPointsTrackerParams = KeyPointsTrackerParams(), + ), +) : AbstractTracker(trackerConfig) { + /** + * Computes similarity based on Object Keypoint Similarity (OKS). It's + * assumed that the keypoints within each person are in normalized image + * coordinates. See `AbstractTracker` for more details. + */ + override fun computeSimilarity(persons: List): List> { + if (persons.isEmpty() && tracks.isEmpty()) { + return emptyList() + } + val simMatrix = mutableListOf>() + persons.forEach { person -> + val row = mutableListOf() + tracks.forEach { track -> + val oksValue = oks(person, track.person) + row.add(oksValue) + } + simMatrix.add(row) + } + return simMatrix + } + + /** + * Computes the Object Keypoint Similarity (OKS) between a person and track person. + * This is similar in spirit to the calculation used by COCO keypoint eval: + * https://cocodataset.org/#keypoints-eval + * In this case, OKS is calculated as: + * (1/sum_i d(c_i, c_ti)) * \sum_i exp(-d_i^2/(2*a_ti*x_i^2))*d(c_i, c_ti) + * where + * d(x, y) is an indicator function which only produces 1 if x and y + * exceed a given threshold (i.e. keypointThreshold), otherwise 0. + * c_i is the confidence of keypoint i from the new person + * c_ti is the confidence of keypoint i from the track person + * d_i is the Euclidean distance between the person and track person keypoint + * a_ti is the area of the track object (the box covering the keypoints) + * x_i is a constant that controls falloff in a Gaussian distribution, + * computed as 2*keypointFalloff[i]. + * @param person1 A person. + * @param person2 A track person. + * @returns The OKS score between the person and the track person. This number is + * between 0 and 1, and larger values indicate more keypoint similarity. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun oks(person1: Person, person2: Person): Float { + if (config.keyPointsTrackerParams == null) return 0f + person2.keyPoints.let { keyPoints -> + val boxArea = area(keyPoints) + 1e-6 + var oksTotal = 0f + var numValidKeyPoints = 0 + + person1.keyPoints.forEachIndexed { index, _ -> + val poseKpt = person1.keyPoints[index] + val trackKpt = person2.keyPoints[index] + val threshold = config.keyPointsTrackerParams.keypointThreshold + if (poseKpt.score < threshold || trackKpt.score < threshold) { + return@forEachIndexed + } + numValidKeyPoints += 1 + val dSquared: Float = + (poseKpt.coordinate.x - trackKpt.coordinate.x).pow(2) + (poseKpt.coordinate.y - trackKpt.coordinate.y).pow( + 2, + ) + val x = 2f * config.keyPointsTrackerParams.keypointFalloff[index] + oksTotal += exp(-1f * dSquared / (2f * boxArea * x.pow(2))).toFloat() + } + if (numValidKeyPoints < config.keyPointsTrackerParams.minNumKeyPoints) { + return 0f + } + return oksTotal / numValidKeyPoints + } + } + + /** + * Computes the area of a bounding box that tightly covers keypoints. + * @param keyPoints A list of KeyPoint. + * @returns The area of the object. + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun area(keyPoints: List): Float { + val validKeypoint = keyPoints.filter { + it.score > config.keyPointsTrackerParams?.keypointThreshold ?: 0f + } + if (validKeypoint.isEmpty()) return 0f + val minX = min(1f, validKeypoint.minOf { it.coordinate.x }) + val maxX = max(0f, validKeypoint.maxOf { it.coordinate.x }) + val minY = min(1f, validKeypoint.minOf { it.coordinate.y }) + val maxY = max(0f, validKeypoint.maxOf { it.coordinate.y }) + return (maxX - minX) * (maxY - minY) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/Track.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/Track.kt new file mode 100644 index 0000000..43d874a --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/Track.kt @@ -0,0 +1,24 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.tracker + +import com.example.cpr2u_android.ml.data.Person + +data class Track( + val person: Person, + val lastTimestamp: Long +) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/TrackerConfig.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/TrackerConfig.kt new file mode 100644 index 0000000..a331efa --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/ml/tracker/TrackerConfig.kt @@ -0,0 +1,50 @@ +/* Copyright 2021 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +============================================================================== +*/ + +package com.example.cpr2u_android.ml.tracker + +data class TrackerConfig( + val maxTracks: Int = MAX_TRACKS, + val maxAge: Int = MAX_AGE, + val minSimilarity: Float = MIN_SIMILARITY, + val keyPointsTrackerParams: KeyPointsTrackerParams? = null +) { + companion object { + private const val MAX_TRACKS = 18 + private const val MAX_AGE = 1000 // millisecond + private const val MIN_SIMILARITY = 0.15f + } +} + +data class KeyPointsTrackerParams( + val keypointThreshold: Float = KEYPOINT_THRESHOLD, + // List of per-keypoint standard deviation `σ`, keypoints on a person's body (shoulders, knees, hips, etc.) + // tend to have a `σ` much larger than on a person's head (eyes, nose, ears). + // Read more at: https://cocodataset.org/#keypoints-eval + val keypointFalloff: List = KEYPOINT_FALLOFF, + val minNumKeyPoints: Int = MIN_NUM_KEYPOINT +) { + companion object { + // From COCO: + // https://cocodataset.org/#keypoints-eval + private val KEYPOINT_FALLOFF: List = listOf( + 0.026f, 0.025f, 0.025f, 0.035f, 0.035f, 0.079f, 0.079f, 0.072f, 0.072f, 0.062f, + 0.062f, 0.107f, 0.107f, 0.087f, 0.087f, 0.089f, 0.089f + ) + private const val KEYPOINT_THRESHOLD = 0.3f + private const val MIN_NUM_KEYPOINT = 4 + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/MainActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/MainActivity.kt new file mode 100644 index 0000000..9a4a669 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/MainActivity.kt @@ -0,0 +1,42 @@ +package com.example.cpr2u_android.presentation + +import android.os.Bundle +import android.util.Log +import androidx.fragment.app.Fragment +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityMainBinding +import com.example.cpr2u_android.presentation.base.BaseActivity +import com.example.cpr2u_android.presentation.call.CallFragment +import com.example.cpr2u_android.presentation.education.EducationFragment +import com.example.cpr2u_android.presentation.profile.ProfileFragment + +class MainActivity : BaseActivity(R.layout.activity_main) { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding.bottomNavigation.selectedItemId = R.id.menu_call + binding.bottomNavigation.setOnItemSelectedListener { + when (it.itemId) { + R.id.menu_education -> { + Log.d("education clicked", "") + changeFragment(EducationFragment()) + return@setOnItemSelectedListener true + } + R.id.menu_call -> { + Log.d("call clicked", "") + changeFragment(CallFragment()) + return@setOnItemSelectedListener true + } + R.id.menu_profile -> { + Log.d("profile clicked", "") + changeFragment(ProfileFragment()) + return@setOnItemSelectedListener true + } + else -> return@setOnItemSelectedListener false + } + } + } + + private fun changeFragment(fragment: Fragment) { + supportFragmentManager.beginTransaction().replace(R.id.fcv_main, fragment).commit() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/AuthViewModel.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/AuthViewModel.kt new file mode 100644 index 0000000..4ed911f --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/AuthViewModel.kt @@ -0,0 +1,108 @@ +package com.example.cpr2u_android.presentation.auth + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.model.request.auth.RequestSignUp +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import com.example.cpr2u_android.domain.repository.auth.AuthRepository +import kotlinx.coroutines.launch +import timber.log.Timber + +class AuthViewModel(private val authRepository: AuthRepository) : ViewModel() { + private var _phoneNumber: String = "" + val phoneNumber: String + get() = _phoneNumber + + private var _nickname: String = "" + val nickname: String + get() = _nickname + + private val _validationCode = MutableLiveData() + var validationCode: LiveData = _validationCode + + private val _isUser = MutableLiveData() + var isUser: LiveData = _isUser + + private val _isValidNickname = MutableLiveData() + var isValidNickname: LiveData = _isValidNickname + + private val _isSuccess = MutableLiveData() + var isSuccess: LiveData = _isSuccess + + fun setPhoneNumber(phoneNumber: String) { + Timber.d("### set phone number -> $phoneNumber") + _phoneNumber = phoneNumber + } + + fun postVerification(phoneNumber: String) = viewModelScope.launch { + kotlin.runCatching { + authRepository.postVerification(phoneNumber) + }.onSuccess { + Timber.d("validation code success -> ${it.data.validationCode}") + _validationCode.value = it.data.validationCode + Timber.d("validation code success set -> ${_validationCode.value}") + }.onFailure { + Timber.e("validation code fail $it") + } + } + + fun postLogin(loginData: RequestLogin) = viewModelScope.launch { + kotlin.runCatching { + authRepository.postLogin(loginData) + }.onSuccess { + Timber.d("인증된 사용자. 메인화면으로") + _isUser.value = true + CPR2USharedPreference.setAccessToken(it.data.accessToken) + CPR2USharedPreference.setRefreshToken(it.data.refreshToken) + }.onFailure { + Timber.d("인증되지 않은 사용자. 회원가입 필요 $it") + _isUser.value = false + } + } + + fun getNickname(nickname: String) = viewModelScope.launch { + kotlin.runCatching { + authRepository.getNickname(nickname) + }.onSuccess { + Timber.d("사용 가능한 닉네임") + setIsValidNickname(true) + }.onFailure { + Timber.d("사용 불가능한 닉네임") + setIsValidNickname(false) + } + } + + fun postSignUp(nickname: String, phoneNumber: String) = viewModelScope.launch { + kotlin.runCatching { + Timber.d("CPR2USharedPreference.getDeviceToken() -> ${CPR2USharedPreference.getDeviceToken()}") + Timber.d("phonenumber -> $phoneNumber") + Timber.d("nickname -> $nickname") + authRepository.postSignUp( + RequestSignUp( + deviceToken = CPR2USharedPreference.getDeviceToken(), + phoneNumber = phoneNumber, + nickname = nickname, + ), + ) + }.onSuccess { + Timber.e("post-sign-up-success -> $it") + _isSuccess.value = true + CPR2USharedPreference.setAccessToken(it.data.accessToken) + CPR2USharedPreference.setRefreshToken(it.data.refreshToken) + }.onFailure { + Timber.e("post-sign-up-fail -> $it") + _isSuccess.value = false + } + } + private fun setIsValidNickname(isValid: Boolean) { + Timber.d("set value") + _isValidNickname.value = isValid + } + + fun getValidationCode(): String? { + return _validationCode.value + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginActivity.kt new file mode 100644 index 0000000..ec14ca7 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginActivity.kt @@ -0,0 +1,24 @@ +package com.example.cpr2u_android.presentation.auth + +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityLoginBinding +import com.example.cpr2u_android.presentation.base.BaseActivity + +class LoginActivity : BaseActivity(R.layout.activity_login) { + + private lateinit var navController: NavController + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initNavigation() + } + + private fun initNavigation() { + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fcv_login) as NavHostFragment + navController = navHostFragment.navController + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginPhoneNumberCheckFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginPhoneNumberCheckFragment.kt new file mode 100644 index 0000000..e3c497e --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginPhoneNumberCheckFragment.kt @@ -0,0 +1,102 @@ +package com.example.cpr2u_android.presentation.auth + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.view.View.OnFocusChangeListener +import android.widget.EditText +import android.widget.Toast +import androidx.core.widget.addTextChangedListener +import com.example.cpr2u_android.R +import com.example.cpr2u_android.data.model.request.auth.RequestLogin +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import com.example.cpr2u_android.databinding.FragmentLoginPhoneNumberCheckBinding +import com.example.cpr2u_android.presentation.MainActivity +import com.example.cpr2u_android.presentation.base.BaseFragment +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import timber.log.Timber + +class LoginPhoneNumberCheckFragment : + BaseFragment(R.layout.fragment_login_phone_number_check) { + private val signInViewModel: AuthViewModel by sharedViewModel() + private lateinit var smsCode: List + var smsCodeStr: String = "" + var phoneNumber: String = "" + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + smsCode = + listOf(binding.tvSmsCode1, binding.tvSmsCode2, binding.tvSmsCode3, binding.tvSmsCode4) + + initPhoneNumber() + initClickEvent() + initSmsCodeEvent() + } + + private fun initPhoneNumber() { + phoneNumber = arguments?.getString("phoneNumber").toString() + binding.phoneNumber = phoneNumber + } + + private fun initClickEvent() { + binding.tvConfirm.setOnClickListener { + smsCode.forEach { + smsCodeStr += it.text.toString() + } + + if (smsCodeStr.length < 4) { + Toast.makeText(requireContext(), "코드를 모두 입력해주세요", Toast.LENGTH_SHORT).show() + } else { + Timber.d("smscode -> $smsCodeStr") + Timber.d("view model code -> ${signInViewModel.validationCode.value}") + if (smsCodeStr == signInViewModel.validationCode.value) { + signInViewModel.postLogin( + RequestLogin( + deviceToken = CPR2USharedPreference.getDeviceToken(), + phoneNumber = phoneNumber +// CPR2USharedPreference.getDeviceToken(), +// smsCodeStr, + ), + ) + signInViewModel.isUser.observe(viewLifecycleOwner) { + navigateToNext(it) + } + } else { + Timber.d("코드 다름") + } + } + } + } + + private fun navigateToNext(it: Boolean) { + val nextView = if (it) { + Timber.d("it -> true") + MainActivity::class.java + } else { + Timber.d("it -> false") + SignUpActivity::class.java + } + val intent = + Intent(requireContext(), nextView) + intent.putExtra("phoneNumber", phoneNumber) + startActivity(intent) + requireActivity().finishAffinity() + } + + private fun initSmsCodeEvent() { + for (i in 0..3) { + smsCode[i].addTextChangedListener { + if (i in 0..2) { + if (smsCode[i].length() > 0) { + smsCode[i + 1].requestFocus() + } + } + } + smsCode[i].onFocusChangeListener = OnFocusChangeListener { view, hasFocus -> + if (hasFocus) { + smsCode[i].text.clear() + } + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginPhoneNumberFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginPhoneNumberFragment.kt new file mode 100644 index 0000000..47edd54 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/LoginPhoneNumberFragment.kt @@ -0,0 +1,51 @@ +package com.example.cpr2u_android.presentation.auth + +import android.os.Bundle +import android.view.View +import androidx.core.widget.addTextChangedListener +import androidx.navigation.fragment.findNavController +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.FragmentLoginPhoneNumberBinding +import com.example.cpr2u_android.presentation.base.BaseFragment +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import timber.log.Timber + +class LoginPhoneNumberFragment : + BaseFragment(R.layout.fragment_login_phone_number) { + private val signInViewModel: AuthViewModel by sharedViewModel() + private var bundle = Bundle() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initClickEvent() + observePhoneNumber() + } + + private fun observePhoneNumber() { + binding.etNumber.addTextChangedListener { + binding.tvSend.isSelected = !it.isNullOrEmpty() + } + } + + private fun initClickEvent() { + binding.tvSend.setOnClickListener { + if (binding.tvSend.isSelected) { + val phoneNumber = binding.etNumber.text.toString() + Timber.d("### set phone number fragment -> ${phoneNumber}") + signInViewModel.setPhoneNumber(phoneNumber) + signInViewModel.postVerification(phoneNumber) + signInViewModel.validationCode.observe(viewLifecycleOwner) { + + Timber.d("signIn ViewModel V C -> ${signInViewModel.getValidationCode()}") + bundle = Bundle().apply { + putString("phoneNumber", binding.etNumber.text.toString()) + } + findNavController().navigate( + R.id.action_loginPhoneNumberFragment_to_loginPhoneNumberCheckFragment, + bundle, + ) + } + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/SignUpActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/SignUpActivity.kt new file mode 100644 index 0000000..56b4801 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/auth/SignUpActivity.kt @@ -0,0 +1,97 @@ +package com.example.cpr2u_android.presentation.auth + +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.core.widget.addTextChangedListener +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivitySignUpBinding +import com.example.cpr2u_android.presentation.MainActivity +import com.example.cpr2u_android.presentation.base.BaseActivity +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber +import java.util.regex.Pattern + +class SignUpActivity : BaseActivity(R.layout.activity_sign_up) { + private val authViewModel: AuthViewModel by viewModel() + private var phoneNumber: String = "" + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + phoneNumber = intent.getStringExtra("phoneNumber").toString() + initTextChangeEvent() +// observeIsValidNickname() + initConfirmClickListener() + } + + private fun initConfirmClickListener() { + binding.tvConfirm.setOnClickListener { + if (binding.isError == false) { + authViewModel.getNickname(binding.etNickname.text.toString()) + authViewModel.isValidNickname.observe(this) { + if (it) { + authViewModel.postSignUp( + nickname = binding.etNickname.text.toString(), + phoneNumber = phoneNumber, + ) + authViewModel.isSuccess.observe(this) { + if (it) { + Timber.d("회원가입 성공") + navigateToNext() + } else { + Timber.d("회원가입 실패") + } + } + } else { + Timber.d("is valid -> false") + Toast.makeText( + this, + "중복된 닉네임입니다. 다른 닉네임을 입력해주세요.", + Toast.LENGTH_SHORT, + ).show() + } + } + } else if (binding.etNickname.text.isEmpty()) { + binding.isError = true + binding.tvErrorMessage.text = getString(R.string.signup_set_nickname) + } + } + } + + private fun navigateToNext() { + startActivity(Intent(this@SignUpActivity, MainActivity::class.java)) + finish() + } + + private fun observeIsValidNickname() { + authViewModel.isValidNickname.observe(this) { + Timber.d("isValideNickname -> $it") +// isValidNickname = it + } + } + + private fun initTextChangeEvent() { + val ps = + Pattern.compile("^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ\\u318D\\u119E\\u11A2\\u2022\\u2025a\\u00B7\\uFE55]+$") + + with(binding) { + etNickname.addTextChangedListener { + if (it != null) { + if (it.isNotEmpty()) { + isError = false + if (!ps.matcher(it).matches()) { + isError = true + tvErrorMessage.text = getString(R.string.signup_no_special_characters) + } else if (etNickname.text.length > 10) { + etNickname.setText(it.toString().subSequence(0, 10)) + etNickname.setSelection(10) + } + } else { + isError = true + tvErrorMessage.text = getString(R.string.signup_set_nickname) + } + } + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/base/BaseActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/base/BaseActivity.kt new file mode 100644 index 0000000..9273870 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/base/BaseActivity.kt @@ -0,0 +1,41 @@ +package com.example.cpr2u_android.presentation.base + +import android.os.Bundle +import android.view.MotionEvent +import android.widget.EditText +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import com.example.cpr2u_android.util.closeKeyboard + +abstract class BaseActivity( + @LayoutRes private val layoutRes: Int, +) : AppCompatActivity() { + protected lateinit var binding: T + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = DataBindingUtil.setContentView(this, layoutRes) + binding.lifecycleOwner = this + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + val view = currentFocus + + if (view != null && ev.action == MotionEvent.ACTION_UP || ev.action == MotionEvent.ACTION_MOVE && view is EditText && !view.javaClass + .name.startsWith("android.webkit.") + ) { + val locationList = IntArray(2) + view.getLocationOnScreen(locationList) + val x = ev.rawX + view.left - locationList[0] + val y = ev.rawY + view.top - locationList[1] + if (x < view.left || x > view.right || y < view.top || y > view.bottom) { + closeKeyboard(view) + view.clearFocus() + } + } + + return super.dispatchTouchEvent(ev) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/base/BaseFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/base/BaseFragment.kt new file mode 100644 index 0000000..a8cd790 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/base/BaseFragment.kt @@ -0,0 +1,33 @@ +package com.example.cpr2u_android.presentation.base + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment + +abstract class BaseFragment( + @LayoutRes private val layoutRes: Int, +) : Fragment() { + private var _binding: T? = null + protected val binding: T + get() = requireNotNull(_binding) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + _binding = DataBindingUtil.inflate(inflater, layoutRes, container, false) + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallFragment.kt new file mode 100644 index 0000000..1741d13 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallFragment.kt @@ -0,0 +1,385 @@ +package com.example.cpr2u_android.presentation.call + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.location.Address +import android.location.Geocoder +import android.location.Location +import android.location.LocationManager +import android.os.Bundle +import android.os.CountDownTimer +import android.os.Handler +import android.util.Log +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.example.cpr2u_android.R +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.databinding.FragmentCallBinding +import com.example.cpr2u_android.domain.model.CallInfoBottomSheet +import com.example.cpr2u_android.util.UiState +import com.google.android.gms.location.FusedLocationProviderClient +import com.google.android.gms.location.LocationListener +import com.google.android.gms.location.LocationServices +import com.google.android.gms.maps.* +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Marker +import com.google.android.gms.maps.model.MarkerOptions +import com.google.android.gms.tasks.OnFailureListener +import com.google.android.gms.tasks.OnSuccessListener +import com.google.maps.android.SphericalUtil +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber +import java.util.* +import kotlin.properties.Delegates + + +class CallFragment : Fragment(), OnMapReadyCallback, LocationListener, GoogleMap.OnMyLocationChangeListener { + private val callViewModel: CallViewModel by viewModel() + private lateinit var binding: FragmentCallBinding + private val locationPermissionCode = 100 + lateinit var mapFragment: MapView + + private lateinit var mMap: GoogleMap + private lateinit var mLocationManager: LocationManager + private lateinit var mMarker: Marker + private lateinit var progressBell: ProgressBar + private lateinit var fadeIn: View + private lateinit var fadeInAnim: Animation + private lateinit var fadeInText: TextView + private lateinit var bell: ImageView + private var timerStarted = false + private var timeLeftInMillis = 0L + private lateinit var countDownTimer: CountDownTimer + private lateinit var fusedLocationClient: FusedLocationProviderClient + private var latitude by Delegates.notNull() + private var longitude by Delegates.notNull() + private lateinit var address: Address + private lateinit var fullAddress: String + + private var timerSec: Int = 0 + private var time: TimerTask? = null + private var timerText: TextView? = null + private val handler: Handler = Handler() + private lateinit var updater: Runnable + + @SuppressLint("ClickableViewAccessibility") + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + binding = FragmentCallBinding.inflate(layoutInflater, container, false) + val view: View = inflater.inflate(R.layout.fragment_call, container, false) + + MapsInitializer.initialize(requireContext(), MapsInitializer.Renderer.LATEST) { + // println(it.name) + } + mapFragment = view.findViewById(R.id.mapFragment) + mapFragment.onCreate(savedInstanceState) + mapFragment.getMapAsync(this) + + bell = view.findViewById(R.id.iv_bell) + progressBell = view.findViewById(R.id.progress_bar_bell) + progressBell.visibility = View.GONE + + fadeIn = view.findViewById(R.id.fade_in) + fadeInAnim = AnimationUtils.loadAnimation(context, R.anim.fade_in) + fadeInText = view.findViewById(R.id.tv_fade_in) + fadeIn.visibility = View.INVISIBLE + progressBell.visibility = View.INVISIBLE + fadeInText.visibility = View.INVISIBLE + + initTimer() + + bell.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + fadeIn.startAnimation(fadeInAnim) + fadeIn.setBackgroundColor(Color.parseColor("#42FF2F2F")) + startTimer() + return@setOnTouchListener true + } + + MotionEvent.ACTION_UP -> { + fadeIn.visibility = View.INVISIBLE + progressBell.visibility = View.INVISIBLE + fadeInText.visibility = View.INVISIBLE + fadeIn.clearAnimation() + resetTimer() + } + } + true + } + return view + } + + private fun initTimer() { + Timber.d("#### init Timer") + countDownTimer = object : CountDownTimer(4000, 1000) { + override fun onTick(millisUntilFinished: Long) { + Timber.d("onTick 호출...") + fadeIn.visibility = View.VISIBLE + progressBell.visibility = View.VISIBLE + fadeInText.visibility = View.VISIBLE + timeLeftInMillis = millisUntilFinished + val secondsLeft = timeLeftInMillis / 1000 + fadeInText.text = secondsLeft.toString() + } + + override fun onFinish() { + Timber.d("onFinish 호출") + fadeIn.visibility = View.INVISIBLE + progressBell.visibility = View.INVISIBLE + fadeInText.visibility = View.INVISIBLE + // TODO : 호출 서버통신 + callViewModel.postCall(latitude, longitude, fullAddress) + callViewModel.callUIState.flowWithLifecycle(lifecycle).onEach { + when (it) { + is UiState.Success -> { + Timber.d("post call success") + val bundle = + Bundle().apply { putInt("callId", callViewModel._callId) } + Timber.d("####1 startAcitivy 합니다..") + startActivity( + Intent( + requireContext(), + CallingActivity::class.java, + ).putExtras(bundle), + ) + timerStarted = false + callViewModel.setCallUiState() + resetTimer() + initTimer() + return@onEach + } + else -> { + Timber.d("로딩도 아니고.. 성공도 아니고.. ") + } + } + }.launchIn(lifecycleScope) + } + } + } + + override fun onMapReady(googleMap: GoogleMap) { + mMap = googleMap + fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) + + // Check for permission to access location + if (ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.ACCESS_FINE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED + ) { + // Get current location + fusedLocationClient.lastLocation.addOnSuccessListener( + requireActivity(), + OnSuccessListener { location -> + // Got last known location. In some rare situations, this can be null. + if (location != null) { + // Get latitude and longitude from location + latitude = location.latitude + longitude = location.longitude + // Use the latitude and longitude as needed + Log.d("LOCATION : ", "Latitude: $latitude, Longitude: $longitude") + + val markerOptions = MarkerOptions().apply { + position(LatLng(latitude, longitude)) + title("Current Location") + } + // 내 위치 마커 찍기 + mMap.isMyLocationEnabled = true +// mMap.addMarker(markerOptions) + + // 카메라 이동 및 줌인 + mMap.moveCamera( + CameraUpdateFactory.newLatLngZoom( + LatLng( + latitude, + longitude, + ), + 15f, + ), + ) + + val geocoder = Geocoder(requireContext(), Locale.getDefault()) + val addresses: List
? = geocoder.getFromLocation( + latitude, + longitude, + 1, + ) + + if (addresses != null) { + if (addresses.isNotEmpty()) { + address = addresses[0] + fullAddress = address.getAddressLine(0) // full address name + Timber.d("ADDRESS : $address") + Timber.d("FULL ADDRESS : $fullAddress") + val tvLocation = view?.findViewById(R.id.tv_location) + tvLocation?.text = fullAddress.substring(5, fullAddress.length) + } + } + } + }, + ).addOnFailureListener( + requireActivity(), + OnFailureListener { e -> + // Handle failure + }, + ) + } else { + // Request permission to access location + ActivityCompat.requestPermissions( + requireActivity(), + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_PERMISSION_REQUEST_CODE, + ) + } + + callViewModel.getCallList() + lateinit var callList: ResponseCallList + callViewModel.callListInfo.observe(viewLifecycleOwner) { + callList = it + val listNum = it.data.callList.size + for (i in 0 until listNum) { + val nLatitude = it.data.callList[i].latitude + val nLongitude = it.data.callList[i].longitude + val nMarkerOptions = MarkerOptions().apply { + position(LatLng(nLatitude, nLongitude)) + title("${it.data.callList[i].cprCallId}") + } + mMap.addMarker(nMarkerOptions) + } + } + + // 마커를 클릭하면 BottomSheet를 띄움 + mMap.setOnMarkerClickListener { marker -> + Timber.d("위치 -----> ${marker.position.latitude}") + Timber.d("CALL ID -> ${marker.title}") + if (marker.title != "Current Location") { + val distance = SphericalUtil.computeDistanceBetween( + LatLng(latitude, longitude), + LatLng(marker.position.latitude, marker.position.longitude), + ) + var distanceStr = "" + distanceStr = if (distance < 1000) { + String.format("%.2f", distance) + "m" + } else { + String.format("%.2f", distance / 1000) + "km" + } + val duration = + if (distance / 100 < 1) "1" else String.format("%.0f", distance / 100) + val address = callList.data.callList.find { + it.cprCallId.toString() == marker.title + } + Timber.d("address -> $address") + val productInfoFragment = CallInfoBottomSheetDialog( + CallInfoBottomSheet( + callId = marker.title!!.toInt(), + distance = distanceStr, + duration = duration, + fullAddress = address!!.fullAddress, + ), + ) + productInfoFragment.show(requireFragmentManager(), "TAG") + } + true + } + } + + private fun startTimer() { + countDownTimer.cancel() + countDownTimer.start() + timerStarted = true + } + + private fun resetTimer() { + timerSec = 0 + countDownTimer.cancel() + timerStarted = false + timeLeftInMillis = 0L + fadeInText.text = "0" + } + + override fun onStart() { + super.onStart() + mapFragment.onStart() + } + + override fun onStop() { + super.onStop() + mapFragment.onStop() + resetTimer() + } + + override fun onResume() { + super.onResume() +// mapFragment.onResume() + initTimer() + countDownTimer.cancel() + Timber.d("############resume") + } + + override fun onDestroy() { + super.onDestroy() + mapFragment + } + + override fun onLowMemory() { + super.onLowMemory() + } + + override fun onLocationChanged(location: Location) { + location ?: return + + // Add a marker for the user's current location + if (::mMarker.isInitialized) { + mMarker.remove() + } + mMarker = + mMap.addMarker( + MarkerOptions().position(LatLng(location.latitude, location.longitude)) + .title("Current Location"), + )!! + mMap.animateCamera( + CameraUpdateFactory.newLatLngZoom( + LatLng( + location.latitude, + location.longitude, + ), + 15f, + ), + ) + // Stop updating the user's location to save battery +// mLocationManager.removeUpdates(this) + } + + companion object { + /** Long Press 판단 기준 시간 */ + private const val LONG_PRESSED_TIME = 2L + private const val LOCATION_PERMISSION_REQUEST_CODE = 100 + } + + override fun onMyLocationChange(location: Location) { + val d1: Double = location.latitude + val d2: Double = location.longitude + mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(d1, d2), 15f)) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallInfoBottomSheetDialog.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallInfoBottomSheetDialog.kt new file mode 100644 index 0000000..ade8ecf --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallInfoBottomSheetDialog.kt @@ -0,0 +1,121 @@ +package com.example.cpr2u_android.presentation.call + +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.BottomSheetMapBinding +import com.example.cpr2u_android.domain.model.CallInfoBottomSheet +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber +import java.util.* +import kotlin.properties.Delegates + +class CallInfoBottomSheetDialog(val item: CallInfoBottomSheet) : BottomSheetDialogFragment() { + private lateinit var binding: BottomSheetMapBinding + private val callViewModel: CallViewModel by viewModel() + + private var timerSec: Int = 0 + private var time: TimerTask? = null + private var timerText: TextView? = null + private val handler: Handler = Handler() + private var callId by Delegates.notNull() + private lateinit var updater: Runnable + var isDispatch = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.BottomSheetDialog) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + binding = + BottomSheetMapBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + mapInfo = item + clMarkerInfo.visibility = View.VISIBLE + clTimer.visibility = View.INVISIBLE + tvReport.visibility = View.INVISIBLE + tvDispatch.setOnClickListener { + if (isDispatch) { + isCancelable = false + // 출동하기 성공시 dismiss + callViewModel.postDispatch(item.callId) + isDispatch = false + callViewModel.dispatchSuccess.observe(viewLifecycleOwner) { + if (it) { + Timber.d("it , dismiss ->$it") + clMarkerInfo.visibility = View.INVISIBLE + clTimer.visibility = View.VISIBLE + tvReport.visibility = View.VISIBLE + tvDispatch.text = "ARRIVED" + // 타이머 시작 + timerText = binding.tvMinute + timerSec = 0 + time = object : TimerTask() { + override fun run() { + updateTime() + if (timerSec >= 300) return + timerSec++ + } + } + val timer = Timer() + timer.schedule(time, 0, 1000) + } else { + Timber.d("it -> $it") + } + } + } else { + isCancelable = true + // 출동종료 + callViewModel.postDispatchArrive() + callViewModel.dispatchArriveSuccess.observe(viewLifecycleOwner) { + if (it) { + dismiss() + } else { + Timber.d("arrive server fail") + } + } + } + } + tvReport.setOnClickListener { + Timber.d("callViewmodel id -> ${callViewModel.dispatchId.value}") + + val bundle = Bundle().apply { + putInt("dispatchId", callViewModel.dispatchId.value!!) + } + startActivity( + Intent( + requireContext(), + DispatchReportActivity::class.java, + ).putExtras(bundle), + ) + } + } + } + + private fun updateTime() { + updater = Runnable { + val minute = if (timerSec / 60 < 1) "00" else "0${(timerSec / 60)}" + val second = + if (timerSec % 60 < 10) "0${(timerSec % 60)}" else (timerSec % 60).toString() + binding.tvMinute.setText("$minute : $second") + } + handler.post(updater) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallViewModel.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallViewModel.kt new file mode 100644 index 0000000..c519678 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallViewModel.kt @@ -0,0 +1,153 @@ +package com.example.cpr2u_android.presentation.call + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.cpr2u_android.data.model.request.RequestDispatchReport +import com.example.cpr2u_android.data.model.request.education.RequestCall +import com.example.cpr2u_android.data.model.response.call.ResponseCallList +import com.example.cpr2u_android.domain.repository.call.CallRepository +import com.example.cpr2u_android.util.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +class CallViewModel(private val callRepository: CallRepository) : ViewModel() { + + var _callId = -1 + private val _callUIState = MutableStateFlow>(UiState.Loading) + val callUIState: StateFlow> = _callUIState + + private val _callEndUIState = MutableStateFlow>(UiState.Loading) + val callEndUIState: StateFlow> = _callEndUIState + + private val _callEndSuccess = MutableLiveData() + val callEndSuccess: LiveData = _callEndSuccess + + private val _dispatchSuccess = MutableLiveData() + val dispatchSuccess: LiveData = _dispatchSuccess + + private val _dispatchArriveSuccess = MutableLiveData() + val dispatchArriveSuccess: LiveData = _dispatchArriveSuccess + + private val _dispatchReportSuccess = MutableLiveData() + val dispatchReportSuccess: LiveData = _dispatchReportSuccess + + private val _callListInfo = MutableLiveData() + val callListInfo: LiveData = _callListInfo + + private val _dispatchId = MutableLiveData() + val dispatchId: LiveData = _dispatchId + + /* + "latitude": 37.5440261, + "longitude": 126.9671087, + "full_address": "서울특별시 용산구 청파동3가 114-11" + */ + fun postCall(latitude: Double, longitude: Double, address: String) = viewModelScope.launch { + kotlin.runCatching { + _callUIState.emit(UiState.Loading) + val edit_address = address.substring(5, address.length) + Timber.d("latitude -> $latitude, Longitude -> $longitude, address -> $edit_address") + callRepository.postCall( + data = RequestCall( + latitude = latitude, + longitude = longitude, + fullAddress = edit_address, + ), + ) + }.onSuccess { + Timber.d("post-call-success $it") + _callId = it.data.callId + _callUIState.emit(UiState.Success(true)) + Timber.d("set call ID -> $_callId") + }.onFailure { + _callUIState.emit(UiState.Failure("$it")) + Timber.d("post-call-fail $it") + } + } + + fun setCallUiState() = viewModelScope.launch { + kotlin.runCatching { + _callUIState.emit(UiState.Loading) + } + } + + fun postCallEnd(callId: Int) = viewModelScope.launch { + kotlin.runCatching { + _callEndUIState.emit(UiState.Loading) + Timber.d("_callID -> $callId") + callRepository.postCallEnd(callId) + }.onSuccess { + _callEndSuccess.value = true + _callEndUIState.emit(UiState.Success(true)) + Timber.d("post-call-end-success $it") + }.onFailure { + _callEndSuccess.value = false + _callEndUIState.emit(UiState.Failure("$it")) + Timber.d("post-call-end-fail $it") + } + } + + fun getCallList() = viewModelScope.launch { + kotlin.runCatching { + callRepository.getCallList() + }.onSuccess { + Timber.d("get-call-list-success -> $it") + it.data.callList + _callListInfo.value = it + Timber.d("_call List Info -> ${_callListInfo.value}") + }.onFailure { + Timber.d("get-call-list-fail -> $it") + } + } + + fun postDispatch(callId: Int) = viewModelScope.launch { + kotlin.runCatching { + Timber.d("dispatch call id -> $callId") + callRepository.postDispatch(callId) + }.onSuccess { + _dispatchSuccess.value = true + _dispatchId.value = it.data.dispatchId + Timber.d("set dispatch ID -> ${_dispatchId.value}") + Timber.d("post-dispatch-success -> $it") + }.onFailure { + _dispatchSuccess.value = false + Timber.d("post-dispatch-fail -> $it") + } + } + + fun postDispatchArrive() = viewModelScope.launch { + kotlin.runCatching { + Timber.d("dispatch Id -> ${_dispatchId.value}") + callRepository.postDispatchArrive(_dispatchId.value!!) + }.onSuccess { + Timber.d("post-dispatch-arrive-success -> $it") + _dispatchArriveSuccess.value = true + }.onFailure { + Timber.d("post-dispatch-arrive-fail -> $it") + _dispatchArriveSuccess.value = false + } + } + + fun postDispatchReport(dispatchId: Int, content: String) = viewModelScope.launch { + kotlin.runCatching { + Timber.d("report id -> $dispatchId") + Timber.d("content -> $content") + callRepository.postDispatchReport( + RequestDispatchReport( + content = content, + dispatchId = dispatchId, + ), + ) + }.onSuccess { + Timber.d("post-dispatch-report-success -> $it") + _dispatchReportSuccess.value = true + }.onFailure { + Timber.d("post-dispatch-report-fail -> $it") + _dispatchReportSuccess.value = false + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallingActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallingActivity.kt new file mode 100644 index 0000000..d6dd69e --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/CallingActivity.kt @@ -0,0 +1,97 @@ +package com.example.cpr2u_android.presentation.call + +import android.media.MediaPlayer +import android.os.Bundle +import android.os.Handler +import android.widget.TextView +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityCallingBinding +import com.example.cpr2u_android.presentation.base.BaseActivity +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber +import java.util.* +import kotlin.properties.Delegates + +class CallingActivity : BaseActivity(R.layout.activity_calling) { + private val callViewModel: CallViewModel by viewModel() + + private var timerSec: Int = 0 + private var time: TimerTask? = null + private var timerText: TextView? = null + private val handler: Handler = Handler() + private var callId by Delegates.notNull() + private lateinit var updater: Runnable + var ring = MediaPlayer() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Timber.d("####2 oncreate...") + callId = intent.getIntExtra("callId", -1) + Timber.d("call id GET -> $callId") + binding.tvSituationEnd.setOnClickListener { + callViewModel.postCallEnd(callId) + callViewModel.callEndSuccess.observe(this) { + if (it) { + Timber.d("#### call end success observe success") + handler.removeCallbacks(updater) + Timber.d("#### finish 직전") + finish() + Timber.d("#### finish 뒤..") + } else { + Timber.d("#### 에엥...") + } + } +// callViewModel.callEndUIState.flowWithLifecycle(lifecycle).onEach { +// when (it) { +// is UiState.Success -> { +// Timber.d("success") +// handler.removeCallbacks(updater) +// finish() +// } +// is UiState.Loading -> { +// Timber.d("Loading..") +// } +// else -> { +// Timber.d("fail...") +// } +// } +// } + } + + timerText = binding.tvMinute + timerSec = 0 + time = object : TimerTask() { + override fun run() { + updateTime() + if (timerSec >= 300) return + if (timerSec % 15 == 0) { + // TODO : 15초마다 서버 통신 + } + timerSec++ + } + } + val timer = Timer() + timer.schedule(time, 0, 1000) + + ring = MediaPlayer.create(this@CallingActivity, com.example.cpr2u_android.R.raw.midi) + ring.start() + } + + private fun updateTime() { + updater = Runnable { + val minute = if (timerSec / 60 < 1) "00" else "0${(timerSec / 60)}" + val second = + if (timerSec % 60 < 10) "0${(timerSec % 60)}" else (timerSec % 60).toString() + binding.tvMinute.setText("$minute : $second") + } + handler.post(updater) + } + + override fun onBackPressed() { +// super.onBackPressed() + } + + override fun onStop() { + super.onStop() + ring.stop() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/DispatchReportActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/DispatchReportActivity.kt new file mode 100644 index 0000000..9e197ff --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/call/DispatchReportActivity.kt @@ -0,0 +1,31 @@ +package com.example.cpr2u_android.presentation.call + +import android.os.Bundle +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityDispatchReportBinding +import com.example.cpr2u_android.presentation.base.BaseActivity +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber + +class DispatchReportActivity : + BaseActivity(R.layout.activity_dispatch_report) { + val callViewModel: CallViewModel by viewModel() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val dispatchId = intent.getIntExtra("dispatchId", -1) + Timber.d("on Create -> $dispatchId") + + binding.tvContinue.setOnClickListener { + val content = binding.etContent.text.toString() + callViewModel.postDispatchReport(dispatchId, content) + callViewModel.dispatchReportSuccess.observe(this) { + if (it) { + finish() + } else { + Timber.d("report fail") + } + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/EducationFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/EducationFragment.kt new file mode 100644 index 0000000..c26b9b5 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/EducationFragment.kt @@ -0,0 +1,115 @@ +package com.example.cpr2u_android.presentation.education + +import android.app.Dialog +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import androidx.databinding.DataBindingUtil +import com.example.cpr2u_android.R +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import com.example.cpr2u_android.databinding.DialogQuizBinding +import com.example.cpr2u_android.databinding.DialogSelectAddressBinding +import com.example.cpr2u_android.databinding.FragmentEducationBinding +import com.example.cpr2u_android.presentation.base.BaseFragment +import org.koin.androidx.viewmodel.ext.android.sharedViewModel + +class EducationFragment : BaseFragment(R.layout.fragment_education) { + private val educationViewModel: EducationViewModel by sharedViewModel() + private var pass1 = false + private var pass2 = false + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + educationViewModel.getUserInfo() + initClickListener() + observeUserInfo() + } + + private fun initClickListener() { + binding.clLecture.setOnClickListener { + startActivity(Intent(requireContext(), LectureActivity::class.java)) + } + + binding.clQuiz.setOnClickListener { + if (pass1) startActivity(Intent(requireContext(), QuizActivity::class.java)) + } + + binding.clPosturePractice.setOnClickListener { + if (pass2)startActivity(Intent(requireContext(), PosePracticeActivity::class.java)) + } + } + + private fun observeUserInfo() { + educationViewModel.userInfo.observe(viewLifecycleOwner) { + if (educationViewModel.userInfo.value?.isLectureCompleted == 2) { + binding.clLecture.isSelected = true + pass1 = true + binding.tvLectureComplete.text = "Complete" + } else { + binding.clLecture.isSelected = false + binding.tvLectureComplete.text = "Not Completed" + } + + if (educationViewModel.userInfo.value?.isQuizCompleted == 2) { + binding.clQuiz.isSelected = true + pass2 = true + binding.tvQuizComplete.text = "Complete" + } else { + binding.clQuiz.isSelected = false + binding.tvQuizComplete.text = "Not Completed" + } + + if (educationViewModel.userInfo.value?.isPostureCompleted == 2) { + binding.clPosturePractice.isSelected = true + binding.tvPosePracticeComplete.text = "Complete" + } else { + binding.clPosturePractice.isSelected = false + binding.tvPosePracticeComplete.text = "Not Completed" + } + + if (educationViewModel.userInfo.value?.angelStatus == 2 && CPR2USharedPreference.getLocation() + .isNullOrEmpty() + ) { + // 주소 피커 띄우기 + val dialog = Dialog(requireContext()) + val dialogBinding = DataBindingUtil.inflate( + LayoutInflater.from(requireContext()), + R.layout.dialog_select_address, + null, + false, + ) + dialogBinding.npSido.apply { + // TODO : 주소 max value, display value + setOnValueChangedListener { picker, oldVal, newVal -> + + } + } + } + + binding.progressBar.progress = + (educationViewModel._userInfo.value?.progressPercent!! * 100).toInt() + binding.tvNickname.text = educationViewModel._userInfo.value?.nickname + + when (educationViewModel.userInfo.value?.angelStatus) { + 0 -> { + binding.acquired = true + binding.tvUserAcquired.text = + "ACQUIRED (D-${educationViewModel.userInfo.value!!.daysLeftUntilExpiration})" + } + 1 -> { + binding.acquired = false + binding.tvUserAcquired.text = "EXPIRED" + } + else -> { + binding.acquired = false + binding.tvUserAcquired.text = "UNACQUIRED" + } + } + } + } + + override fun onResume() { + super.onResume() + educationViewModel.getUserInfo() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/EducationViewModel.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/EducationViewModel.kt new file mode 100644 index 0000000..ee79de3 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/EducationViewModel.kt @@ -0,0 +1,135 @@ +package com.example.cpr2u_android.presentation.education + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.cpr2u_android.data.model.response.education.QuizzesListData +import com.example.cpr2u_android.domain.model.UserInfo +import com.example.cpr2u_android.domain.repository.education.EducationRepository +import com.example.cpr2u_android.util.UiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +class EducationViewModel(private val educationRepository: EducationRepository) : ViewModel() { + private val _testUIState = MutableStateFlow>(UiState.Loading) + val testUIState: StateFlow> = _testUIState + + private val _quizzesUIState = MutableStateFlow>(UiState.Loading) + val quizzesUIState: StateFlow> = _quizzesUIState + + private val _quizProgressUIState = MutableStateFlow>(UiState.Loading) + val quizProgressUIState: StateFlow> = _quizProgressUIState + + private val _exercisesProgressUIState = MutableStateFlow>(UiState.Loading) + val exercisesProgressUIState: StateFlow> = _exercisesProgressUIState + + private val _userInfoUIState = MutableStateFlow>(UiState.Loading) + val userInfoUIState: StateFlow> = _userInfoUIState + + private var _quizzesList = listOf() + val quizzesList = _quizzesList + + var question: String = "" + var correct: Boolean = false + var index: Int = 0 + var correctCount: Int = 0 + + var armAngle: ResultMsg = ResultMsg(-1, "", "") + var compressionRate: ResultMsg = ResultMsg(-1, "", "") + var pressDepth: ResultMsg = ResultMsg(-1, "", "") + var postPracticeScore: Int = -1 + + var _userInfo = MutableLiveData() + + // var _userInfo = MutableLiveData(-1,-1, -1, -1, -1, "", "", -1.0) + var userInfo: LiveData = _userInfo + fun postLectureId() = viewModelScope.launch { + kotlin.runCatching { +// _testUIState.emit(UiState.Loading) + educationRepository.postLectureId(1) + }.onSuccess { + Timber.d("post-lecture-id-success $it") + _testUIState.emit(UiState.Success(true)) + }.onFailure { + Timber.d("post-lecture-id-fail $it") + _testUIState.emit(UiState.Failure("$it")) + } + } + + fun getQuizzes() = viewModelScope.launch { + kotlin.runCatching { + educationRepository.getQuizzes() + }.onSuccess { + Timber.d("get-quizzes-success -> ${it.data}") + _quizzesList = it.data + _quizzesUIState.emit(UiState.Success(true)) + }.onFailure { + _quizzesUIState.emit(UiState.Failure("$it")) + Timber.d("get-quizzes-fail -> $it") + } + } + + fun postQuizProgress() = viewModelScope.launch { + kotlin.runCatching { + educationRepository.postQuizProgress(100) + }.onSuccess { + Timber.d("post-quiz-progress-success -> $it") + _quizProgressUIState.emit(UiState.Success(true)) + }.onFailure { + Timber.d("post-quiz-progress-fail -> $it") + _quizProgressUIState.emit(UiState.Failure("$it")) + } + } + + fun postExercisesProgress() = viewModelScope.launch { + kotlin.runCatching { + educationRepository.postExercisesProgress(80) + }.onSuccess { + Timber.d("post-exercises-success -> $it") + _exercisesProgressUIState.emit(UiState.Success(true)) + }.onFailure { + Timber.d("post-exercises-fail -> $it") + _exercisesProgressUIState.emit(UiState.Failure("$it")) + } + } + + fun getInitQuizzesList(): List { + return _quizzesList + } + + fun getUserInfo() = viewModelScope.launch { + kotlin.runCatching { + educationRepository.getUserInfo() + }.onSuccess { + _userInfo.value = UserInfo( + nickname = it.data.nickname, + angelStatus = it.data.angelStatus, + progressPercent = it.data.progressPercent, + isLectureCompleted = it.data.isLectureCompleted, + isQuizCompleted = it.data.isQuizCompleted, + isPostureCompleted = it.data.isPostureCompleted, + daysLeftUntilExpiration = it.data.daysLeftUntilExpiration, + lastLectureTitle = it.data.lastLectureTitle, + ) + _userInfoUIState.emit(UiState.Success(true)) + Timber.d("get-user-info-success -> $it") + Timber.d("viewmodel userInfo -> $_userInfo") + }.onFailure { + Timber.d("get-user-info-fail -> $it") + _userInfoUIState.emit(UiState.Failure("$it")) + } + } + + fun getAddress() = viewModelScope.launch { + kotlin.runCatching { + educationRepository.getAddress() + }.onSuccess { + Timber.d("get-address-success -> ${it.status}") + }.onFailure { + Timber.d("get-address-fail -> $it") + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/LectureActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/LectureActivity.kt new file mode 100644 index 0000000..025da5f --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/LectureActivity.kt @@ -0,0 +1,86 @@ +package com.example.cpr2u_android.presentation.education + +import android.app.Dialog +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.view.LayoutInflater +import android.view.WindowManager +import android.webkit.WebViewClient +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityLectureBinding +import com.example.cpr2u_android.databinding.DialogQuizBinding +import com.example.cpr2u_android.presentation.base.BaseActivity +import com.example.cpr2u_android.util.UiState +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber + + +class LectureActivity : BaseActivity(R.layout.activity_lecture) { + private val educationViewModel: EducationViewModel by viewModel() + private lateinit var handler: Handler + private lateinit var runnable: Runnable + private var start: Long = 0 + private var end: Long = 0 + private var sum: Long = 0 + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding.webView.loadUrl("https://youtu.be/5DWyihalLMM") + val webViewSetting = binding.webView.settings + webViewSetting.javaScriptEnabled = true + binding.webView.webViewClient = WebViewClient() + handler = Handler(Looper.getMainLooper()) + runnable = Runnable { + val dialog = Dialog(this) + val binding = DataBindingUtil.inflate( + LayoutInflater.from(this), + R.layout.dialog_quiz, + null, + false, + ) + binding.buttonFinish.setOnClickListener { + educationViewModel.postLectureId() + educationViewModel.testUIState.flowWithLifecycle(lifecycle).onEach { + when (it) { + is UiState.Success -> { + Timber.d("success") + dialog.dismiss() + finish() + } + is UiState.Loading -> { + Timber.d("로딩중...") + } + else -> { + Timber.d("fail -> $it") + Timber.d("dialog-fail") + } + } + }.launchIn(lifecycleScope) + } + dialog.setContentView(binding.root) + dialog.window?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT, + ) + + dialog.show() + } + handler.postDelayed(runnable, 30000L) + } + + override fun onDestroy() { + super.onDestroy() + handler.removeCallbacks(runnable) + } + + override fun onBackPressed() { + super.onBackPressed() + handler.removeCallbacks(runnable) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/OnBoardingFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/OnBoardingFragment.kt new file mode 100644 index 0000000..1fd4b8c --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/OnBoardingFragment.kt @@ -0,0 +1,92 @@ +package com.example.cpr2u_android.presentation.education + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.Fragment +import com.example.cpr2u_android.databinding.FragmentOnBoardingBinding + +// TODO: Rename parameter arguments, choose names that match +// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER +private const val ARG_PARAM1 = "param1" +private const val ARG_PARAM2 = "param2" + +/** + * A simple [Fragment] subclass. + * Use the [OnBoardingFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class OnBoardingFragment : Fragment() { + private lateinit var title: String + private lateinit var description: String + private var imageResource = 0 + private lateinit var tvTitle: TextView + private lateinit var tvDescription: TextView + private lateinit var image: ImageView + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (arguments != null) { + title = + requireArguments().getString(ARG_PARAM1)!! + description = + requireArguments().getString(ARG_PARAM2)!! + imageResource = + requireArguments().getInt(ARG_PARAM3) + } + } + + private var _binding: FragmentOnBoardingBinding? = null + + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + // Inflate the layout for this fragment + _binding = FragmentOnBoardingBinding.inflate(inflater, container, false) + val view = binding.root + tvTitle = binding.tvOnboardingTitle + tvDescription = binding.tvOnboardingSubtitle + image = binding.ivOnboarding + tvTitle.text = title + tvDescription.text = description + image.setImageResource(imageResource) + return view + } + + companion object { + private const val ARG_PARAM1 = "param1" + private const val ARG_PARAM2 = "param2" + private const val ARG_PARAM3 = "param3" + fun newInstance( + title: String, + description: String, + imageResource: Int, + ): OnBoardingFragment { + val fragment = + OnBoardingFragment() + val args = Bundle() + args.putString( + ARG_PARAM1, + title, + ) + args.putString( + ARG_PARAM2, + description, + ) + args.putInt( + ARG_PARAM3, + imageResource, + ) + fragment.arguments = args + return fragment + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/OnBoardingViewPagerAdapter.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/OnBoardingViewPagerAdapter.kt new file mode 100644 index 0000000..65744d5 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/OnBoardingViewPagerAdapter.kt @@ -0,0 +1,43 @@ +package com.example.cpr2u_android.presentation.education + +import android.content.Context +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.example.cpr2u_android.R + +class OnBoardingViewPagerAdapter( + fragmentActivity: FragmentActivity?, + private val context: Context, +) : + FragmentStateAdapter(fragmentActivity!!) { + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> OnBoardingFragment.newInstance( + "Prepare tools", + "If you do not have a CPR mannequin, \nplease prepare a plastic bottle, pillow, etc.", + R.drawable.onboarding1, + ) + 1 -> OnBoardingFragment.newInstance( + "Prepare tools", + "Put the plastic bottle inside the clothes \nyou don't wear and wrap it up.", + R.drawable.onboarding2, + ) + 2 -> OnBoardingFragment.newInstance( + "Draw an angry man", + "Draw an angry man on your clothes or pillow \nusing tape or pen.", + R.drawable.onboarding3, + ) + else -> OnBoardingFragment.newInstance( + "Ready", + "Please press the location marked in red!", + R.drawable.onboarding4, + ) + } + } + + override fun getItemCount(): Int { + return 4 + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice1Fragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice1Fragment.kt new file mode 100644 index 0000000..fe5f89f --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice1Fragment.kt @@ -0,0 +1,29 @@ +package com.example.cpr2u_android.presentation.education + +import android.content.pm.ActivityInfo +import android.os.Bundle +import android.view.View +import androidx.navigation.fragment.findNavController +import androidx.viewpager2.widget.ViewPager2 +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.FragmentPosePractice1Binding +import com.example.cpr2u_android.presentation.base.BaseFragment +import com.google.android.material.tabs.TabLayoutMediator + +class PosePractice1Fragment : + BaseFragment(R.layout.fragment_pose_practice_1) { + private lateinit var mViewPager: ViewPager2 + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + + binding.buttonNext.setOnClickListener { + findNavController().navigate(R.id.action_posePractice1Fragment_to_posePractice2Fragment) + } + + mViewPager = view.findViewById(R.id.viewPager) + mViewPager.adapter = OnBoardingViewPagerAdapter(fragmentActivity = activity, requireContext()) + TabLayoutMediator(binding.pageIndicator, mViewPager) { _, _ -> }.attach() + mViewPager.offscreenPageLimit = 1 + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice2Fragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice2Fragment.kt new file mode 100644 index 0000000..db46870 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice2Fragment.kt @@ -0,0 +1,383 @@ +package com.example.cpr2u_android.presentation.education + +import android.Manifest +import android.app.AlertDialog +import android.app.Dialog +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.hardware.Camera +import android.media.MediaPlayer +import android.os.Bundle +import android.os.Handler +import android.util.Log +import android.view.SurfaceView +import android.view.View +import android.view.WindowManager +import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.FragmentPosePractice2Binding +import com.example.cpr2u_android.ml.camera.CameraSource +import com.example.cpr2u_android.ml.data.BodyPart +import com.example.cpr2u_android.ml.data.Device +import com.example.cpr2u_android.ml.data.Person +import com.example.cpr2u_android.ml.ml.ModelType +import com.example.cpr2u_android.ml.ml.MoveNet +import com.example.cpr2u_android.presentation.base.BaseFragment +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import java.util.* + +class PosePractice2Fragment : + BaseFragment(R.layout.fragment_pose_practice_2) { + private val educationViewModel: EducationViewModel by sharedViewModel() + + companion object { + private const val FRAGMENT_DIALOG = "dialog" + } + + private lateinit var surfaceView: SurfaceView + private var modelPos = 1 // 1 == MoveNet Thunder model + private var device = Device.CPU + private lateinit var tvScore: TextView + private var cameraSource: CameraSource? = null + + /** + * CPR 자세 인식에 필요한 변수들 + */ + private var maxHeight = 0f + private var minHeight = 0f + private var beforeWrist = 0f + private var increased = true + private var wristList = arrayListOf() + + var correctAngle: Int = 0 + var incorrectAngle: Int = 0 + var compressionRate: Int = 0 + var pressDepth: Int = 0 + + var ring = MediaPlayer() + + private val TAG = "CPR2U" + + private val requestPermissionLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + if (isGranted) { + // Permission is granted. Continue the action or workflow in your app. + openCamera() + } else { + // Explain to the user that the feature is unavailable because the + // features requires a permission that the user has denied. At the + // same time, respect the user's decision. Don't link to system + // settings in an effort to convince the user to change their + // decision. +// ErrorDialog.newInstance(getString(R.string.tfe_pe_request_permission)) +// .show(supportFragmentManager, FRAGMENT_DIALOG) + } + } + + private var timerSec: Int = 0 + private var time: TimerTask? = null + private var timerText: TextView? = null + private val handler: Handler = Handler() + private lateinit var updater: Runnable + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + + val camera = Camera.CameraInfo.CAMERA_FACING_FRONT + // keep screen on while app is running + activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + tvScore = view.findViewById(R.id.tvScore) + surfaceView = view.findViewById(R.id.surfaceView) + surfaceView.display + if (!isCameraPermissionGranted()) { + requestPermission() + } + + ring = MediaPlayer.create(requireContext(), com.example.cpr2u_android.R.raw.midi) + ring.start() + + timerText = binding.tvTimer + timerSec = 0 + time = object : TimerTask() { + override fun run() { + updateTime() + if (timerSec >= 15) { + view.post { + educationViewModel.armAngle = calculateArmAngle() + educationViewModel.compressionRate = calculateCompressionRate() + educationViewModel.pressDepth = calculatePressDepth() + educationViewModel.postPracticeScore = + educationViewModel.armAngle.score + educationViewModel.compressionRate.score + educationViewModel.pressDepth.score + findNavController().navigate(R.id.action_posePractice2Fragment_to_posePractice3Fragment) + } + return + } + timerSec++ + } + } + val timer = Timer() + timer.schedule(time, 0, 1000) + + binding.tvQuit.setOnClickListener { + activity?.finish() + } + } + + private fun updateTime() { + updater = Runnable { + val minute = if (timerSec / 60 < 1) "00" else "0${(timerSec / 60)}" + val second = + if (timerSec % 60 < 10) "0${(timerSec % 60)}" else (timerSec % 60).toString() + timerText?.text = "$minute : $second" + } + handler.post(updater) + } + + override fun onStart() { + super.onStart() + openCamera() + } + + override fun onResume() { + cameraSource?.resume() + super.onResume() + } + + override fun onPause() { + cameraSource?.close() + cameraSource = null + handler.removeCallbacks(updater) + super.onPause() + } + + // check if permission is granted or not. + private fun isCameraPermissionGranted(): Boolean { + return ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_GRANTED + } + + // open camera + private fun openCamera() { + if (isCameraPermissionGranted()) { + if (cameraSource == null) { + cameraSource = + CameraSource( + surfaceView, + object : CameraSource.CameraSourceListener { + override fun onFPSListener(fps: Int) { +// tvFPS.text = getString(R.string.tfe_pe_tv_fps, fps) + } + + override fun onDetectedInfo( + personScore: Float?, + poseLabels: List>?, + persons: List, + ) { + // tvScore: 자세 인식 모델의 정확도 점수 + tvScore.text = + getString(R.string.tfe_pe_tv_score, personScore ?: 0f) + /** + * TODO: 여기서부터 CPR 자세 인식 코드 시작 + * persons에 더 많은 person 데이터가 있을 수록 정확도가 높아진다. + * persons의 0번째에 있는 데이터를 가져와 자세를 분석한다. + */ + measureCprScore(persons[0]) + } + }, + ).apply { + prepareCamera() + } + lifecycleScope.launch(Dispatchers.Main) { + cameraSource?.initCamera() + } + } + createPoseEstimator() + } + } + + /** + * CPR 자세 인식 + */ + private fun measureCprScore(person: Person) { + var xShoulder = .0f + var yShoulder = .0f + var xElbow = .0f + var yElbow = .0f + var xWrist = .0f + var yWrist = .0f + + // person이 갖고 있는 관절 데이터들에서 어깨, 팔꿈치, 손목 데이터 추출 (현재 임시로 왼쪽 관절만 추출한 상태) + person.keyPoints.forEach { point -> + when (point.bodyPart) { + BodyPart.LEFT_SHOULDER -> { + xShoulder = point.coordinate.x + yShoulder = point.coordinate.y + } + BodyPart.LEFT_ELBOW -> { + xElbow = point.coordinate.x + yElbow = point.coordinate.y + } + BodyPart.LEFT_WRIST -> { + xWrist = point.coordinate.x + yWrist = point.coordinate.y + } + else -> {} + } + } + + // 일직선 판별 + var isCorrect = xShoulder - xElbow < 20 && xElbow - xWrist < 20 + if (isCorrect) { + Log.i(TAG, "올바른 자세에요!") + // TODO : 맞은 횟수 세기 + correctAngle++ + } else { + Log.i(TAG, "팔을 90도로 유지하세요!") + // TODO : 틀린 횟수 세기 + incorrectAngle++ + } + + // 손목의 높이가 상승 곡선에서 꼭짓점을 찍고 하강하는 경우 + if (increased && beforeWrist > yWrist + 1) { + increased = false + maxHeight = yWrist + } + // 손목의 높이가 하강 곡선에서 꼭짓점을 찍고 상승하는 경우 + else if (!increased && beforeWrist < yWrist - 1) { + increased = true + minHeight = yWrist + + val num = if (maxHeight > minHeight) maxHeight - minHeight else minHeight - maxHeight + wristList.add(num) + Log.e(TAG, "${wristList.last()}") + } + + beforeWrist = yWrist + } + + private fun calculateCompressionRate(): ResultMsg { + return when (wristList.size) { + in 190..250 -> ResultMsg(50, "adequate", "Good job! Very Adequate") + in 170 until 190 -> ResultMsg(35, "slow", "It's slow. Press more faster") + in 250 until 270 -> ResultMsg(35, "fast", "It's fast. Press more slower") + in 270..999999 -> ResultMsg(20, "tooFast", "It's too fast. Press slower") + in 100 until 170 -> ResultMsg(20, "tooSlow", "It's too slow. Press faster") + else -> ResultMsg(0, "wrong", "Something went wrong. Try Again") + } + } + + // 팔 각도 + private fun calculateArmAngle(): ResultMsg { + val total: Double = (correctAngle + incorrectAngle).toDouble() + if (total < 100) return ResultMsg(0, "wrong", "Something went wrong. Try Again") + return when (total) { + in total * 0.7..total -> ResultMsg(50, "adequate", "Good job! Very Nice angle!") + in total * 0.6..total * 0.7 -> ResultMsg(35, "almost", "Almost there. Try again") + in total * 0.5..total * 0.6 -> ResultMsg(20, "notGood", "Pay more attention to the angle of your arms",) + else -> ResultMsg(5, "bad", "You need some more practice") + } + } + + // 압박 깊이 + private fun calculatePressDepth(): ResultMsg { + var total = 0.0 + wristList.forEach { + total += it + } + return when (total / wristList.size) { + in 18.0..30.0 -> ResultMsg(50, "adequate", "Good job! Very adequate!") + in 5.0..18.0 -> ResultMsg(15, "shallow", "Press little deeper") + in 0.0..5.0 -> ResultMsg(5, "tooShallow", "It's too shallow. Press deeply") + in 30.0..100.0 -> ResultMsg(15, "deep", "Press slight") + else -> ResultMsg(0, "wrong", "Something went wrong. Try Again") + } + } + + // 자세 추정 모델 실행 (Movenet Thunder, CPU가 적절) + private fun createPoseEstimator() { + // For MoveNet MultiPose, hide score and disable pose classifier as the model returns + // multiple Person instances. + val poseDetector = when (modelPos) { + 1 -> { + // MoveNet Thunder (SinglePose) + showDetectionScore(true) + MoveNet.create(requireContext(), device, ModelType.Thunder) + } + else -> { + null + } + } + poseDetector?.let { detector -> + cameraSource?.setDetector(detector) + } + } + + // Show/hide the detection score. + private fun showDetectionScore(isVisible: Boolean) { + tvScore.visibility = if (isVisible) View.VISIBLE else View.GONE + } + + private fun requestPermission() { + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA, + ), + -> { + // You can use the API that requires the permission. + openCamera() + } + else -> { + // You can directly ask for the permission. + // The registered ActivityResultCallback gets the result of this request. + requestPermissionLauncher.launch( + Manifest.permission.CAMERA, + ) + } + } + } + + /** + * Shows an error message dialog. + */ + class ErrorDialog : DialogFragment() { + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + AlertDialog.Builder(activity) + .setMessage(requireArguments().getString(ARG_MESSAGE)) + .setPositiveButton(android.R.string.ok) { _, _ -> + // do nothing + } + .create() + + companion object { + + @JvmStatic + private val ARG_MESSAGE = "message" + + @JvmStatic + fun newInstance(message: String): ErrorDialog = ErrorDialog().apply { + arguments = Bundle().apply { putString(ARG_MESSAGE, message) } + } + } + } + + override fun onStop() { + super.onStop() + ring.stop() + } +} + +data class ResultMsg(val score: Int, val title: String, val desc: String) diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice3Fragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice3Fragment.kt new file mode 100644 index 0000000..957333b --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePractice3Fragment.kt @@ -0,0 +1,105 @@ +package com.example.cpr2u_android.presentation.education + +import android.app.Dialog +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.activity.OnBackPressedCallback +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.DialogQuizBinding +import com.example.cpr2u_android.databinding.FragmentPosePractice3Binding +import com.example.cpr2u_android.presentation.base.BaseFragment +import com.example.cpr2u_android.util.UiState +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import timber.log.Timber + +class PosePractice3Fragment : + BaseFragment(R.layout.fragment_pose_practice_3) { + private val educationViewModel: EducationViewModel by sharedViewModel() + private lateinit var callback: OnBackPressedCallback + var isPassed = true + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.tvCompressionResult.text = educationViewModel.compressionRate.title + binding.tvCompressionRateDesc.text = educationViewModel.compressionRate.desc + + binding.tvArmResult.text = educationViewModel.armAngle.title + binding.tvArmDesc.text = educationViewModel.armAngle.desc + + binding.tvPressResult.text = educationViewModel.pressDepth.title + binding.tvPressDesc.text = educationViewModel.pressDepth.desc + + binding.tvPercentNum.text = educationViewModel.postPracticeScore.toString() + + if (educationViewModel.postPracticeScore > 80) { + isPassed = true + binding.tvPassed.text = "PASSED" + } else { + isPassed = false + binding.tvPassed.text = "FAILED" + } + + binding.btnQuit.setOnClickListener { + if (isPassed) { + // 성공 + val dialog = Dialog(requireContext()) + val binding = DataBindingUtil.inflate( + LayoutInflater.from(requireContext()), + R.layout.dialog_quiz, + null, + false, + ) + binding.ivHeart.setImageResource(R.drawable.ic_certificate_big) + binding.ivHeart.visibility = View.VISIBLE + binding.tvTitle.text = "Congratulation!" + binding.tvSubtitle.text = "You have got CPR Angel Certificate!" + + binding.buttonFinish.setOnClickListener { + educationViewModel.postExercisesProgress() + educationViewModel.exercisesProgressUIState.flowWithLifecycle(lifecycle) + .onEach { + when (it) { + is UiState.Success -> { + Timber.d("success") + dialog.dismiss() + activity?.finish() + } + else -> {} + } + }.launchIn(lifecycleScope) + } + dialog.setContentView(binding.root) + dialog.window?.setLayout( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.MATCH_PARENT, + ) + dialog.show() + } else { // 실패 + activity?.finish() + } + } + } + + override fun onAttach(context: Context) { + super.onAttach(context) + callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + activity?.finish() + } + } + requireActivity().onBackPressedDispatcher.addCallback(this, callback) + } + + override fun onDetach() { + super.onDetach() + callback.remove() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePracticeActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePracticeActivity.kt new file mode 100644 index 0000000..06e3cd8 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/PosePracticeActivity.kt @@ -0,0 +1,31 @@ +package com.example.cpr2u_android.presentation.education + +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityPosePracticeBinding +import com.example.cpr2u_android.presentation.base.BaseActivity + +class PosePracticeActivity : + BaseActivity(R.layout.activity_pose_practice) { + private lateinit var appBarConfiguration: AppBarConfiguration + private lateinit var navController: NavController + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.fcv_pose) as NavHostFragment + navController = navHostFragment.navController +// supportFragmentManager.beginTransaction().replace(R.id.fcv_pose, PosePractice1Fragment()).commit() + } + +// override fun onSupportNavigateUp(): Boolean { +// val navController = findNavController(R.id.nav_pose_practice) +// return navController.navigateUp(appBarConfiguration) || +// super.onSupportNavigateUp() +// } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizActivity.kt new file mode 100644 index 0000000..153ddb1 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizActivity.kt @@ -0,0 +1,31 @@ +package com.example.cpr2u_android.presentation.education + +import android.os.Bundle +import androidx.navigation.findNavController +import androidx.navigation.ui.AppBarConfiguration +import androidx.navigation.ui.navigateUp +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.ActivityQuizBinding +import com.example.cpr2u_android.presentation.base.BaseActivity +import org.koin.androidx.viewmodel.ext.android.viewModel + +class QuizActivity : BaseActivity(R.layout.activity_quiz) { + private val educationViewModel: EducationViewModel by viewModel() + + private lateinit var appBarConfiguration: AppBarConfiguration + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + educationViewModel.getQuizzes() + } + + override fun onSupportNavigateUp(): Boolean { + val navController = findNavController(R.id.nav_host_fragment_content_quiz) + return navController.navigateUp(appBarConfiguration) || + super.onSupportNavigateUp() + } + + override fun onBackPressed() { +// super.onBackPressed() + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizAnswerFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizAnswerFragment.kt new file mode 100644 index 0000000..294c5f2 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizAnswerFragment.kt @@ -0,0 +1,90 @@ +package com.example.cpr2u_android.presentation.education + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.WindowManager +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.DialogQuizBinding +import com.example.cpr2u_android.databinding.FragmentQuizAnswerBinding +import com.example.cpr2u_android.presentation.base.BaseFragment +import com.example.cpr2u_android.util.UiState +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import timber.log.Timber + +class QuizAnswerFragment : BaseFragment(R.layout.fragment_quiz_answer) { + private val educationViewModel: EducationViewModel by sharedViewModel() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.tvQNum.text = "Q.${educationViewModel.index}" + binding.tvQ.text = educationViewModel.question + binding.tvSelectResult.text = + educationViewModel.correct.toString() // TODO: 서버가 result conte음t로 바꿔준댓음 + + binding.tvResult.text = if (educationViewModel.correct) "Correct" else "Incorrect" + binding.tvSelectResult.isSelected = educationViewModel.correct + Timber.d("result index -> ${educationViewModel.index}") + binding.tvExplain.text = + educationViewModel.getInitQuizzesList()[educationViewModel.index - 1].reason + + binding.buttonSecond.setOnClickListener { + if (educationViewModel.index == 5) { + Timber.d("끝") + // 성공 + val dialog = Dialog(requireContext()) + val binding = DataBindingUtil.inflate( + LayoutInflater.from(requireContext()), + R.layout.dialog_quiz, + null, + false, + ) + + if (educationViewModel.correctCount == 5) { + binding.ivHeart.visibility = View.VISIBLE + binding.ivHeartGray.visibility = View.INVISIBLE + binding.tvTitle.text = "Congratulation!" + binding.tvSubtitle.text = "You are perfect!" + } else { + binding.ivHeart.visibility = View.INVISIBLE + binding.ivHeartGray.visibility = View.VISIBLE + binding.tvTitle.text = "Failed: ${educationViewModel.correctCount} / 5" + binding.tvSubtitle.text = "Try Again" + } + + binding.buttonFinish.setOnClickListener { + if (educationViewModel.correctCount == 5) { + educationViewModel.postQuizProgress() + educationViewModel.quizProgressUIState.flowWithLifecycle(lifecycle).onEach { + when (it) { + is UiState.Success -> { + Timber.d("success") + dialog.dismiss() + activity?.finish() + } + else -> {} + } + }.launchIn(lifecycleScope) + } else { + dialog.dismiss() + activity?.finish() + } + } + dialog.setContentView(binding.root) + dialog.window?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT, + ) + dialog.show() + } else { + findNavController().navigate(R.id.action_QuizAnswerFragment_to_QuizQuestionFragment) + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizQuestionFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizQuestionFragment.kt new file mode 100644 index 0000000..1833fcf --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/education/QuizQuestionFragment.kt @@ -0,0 +1,120 @@ +package com.example.cpr2u_android.presentation.education + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.example.cpr2u_android.R +import com.example.cpr2u_android.data.model.response.education.QuizzesListData +import com.example.cpr2u_android.databinding.FragmentQuizQuestionBinding +import com.example.cpr2u_android.presentation.base.BaseFragment +import com.example.cpr2u_android.util.UiState +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.koin.androidx.viewmodel.ext.android.sharedViewModel +import timber.log.Timber + +class QuizQuestionFragment : + BaseFragment(R.layout.fragment_quiz_question) { + private val educationViewModel: EducationViewModel by sharedViewModel() + private var quizzesList: List = listOf(QuizzesListData(-1, "", -1, -1, "", listOf()),) + private var quizQuestion: String = "" + private var selectIndex: Int = -1 + private var answer: Int = -1 + private var isSelected: Boolean = false + private lateinit var tvChoose4List: List + private lateinit var tvChooseOXList: List + private var choose4Index: ArrayList = arrayListOf() + private var chooseOXIndex: ArrayList = arrayListOf() + private var choose4Content: ArrayList = arrayListOf("") + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initSelectList() + observeQuizzesUiState() + initButtonClickListener() + } + + private fun observeQuizzesUiState() { + educationViewModel.quizzesUIState.flowWithLifecycle(lifecycle).onEach { + when (it) { + is UiState.Success -> { + quizzesList = educationViewModel.getInitQuizzesList() + setQuizInfo() + } + else -> {} + } + }.launchIn(lifecycleScope) + } + + private fun initSelectList() { + tvChoose4List = + listOf(binding.tvChoose1, binding.tvChoose2, binding.tvChoose3, binding.tvChoose4) + tvChooseOXList = listOf(binding.tvQO, binding.tvQX) + } + + private fun setQuizInfo() { + binding.tvQNum.text = "Q.${educationViewModel.index + 1}" + quizQuestion = quizzesList[educationViewModel.index].question + binding.tvQ.text = quizzesList[educationViewModel.index].question + answer = quizzesList[educationViewModel.index].answer + + if (quizzesList[educationViewModel.index].type == 0) { + binding.clOX.visibility = View.VISIBLE + binding.clChoose4.visibility = View.INVISIBLE + } else { + binding.clOX.visibility = View.INVISIBLE + binding.clChoose4.visibility = View.VISIBLE + } + + when (quizzesList[educationViewModel.index].type) { + 0 -> { + for (i in 0 until quizzesList[educationViewModel.index].answerList.size) { + chooseOXIndex.add(quizzesList[educationViewModel.index].answerList[i].id) + tvChooseOXList[i].setOnClickListener { + isSelected = true + it.isSelected = true + + selectIndex = chooseOXIndex[i] + Timber.d("selectIndex -> $selectIndex") + val notSelected = tvChooseOXList.filterNot { it == tvChooseOXList[i] } + notSelected.forEach { + it.isSelected = false + } + } + } + } + 1 -> { + for (i in 0 until quizzesList[educationViewModel.index].answerList.size) { + choose4Index.add(quizzesList[educationViewModel.index].answerList[i].id) + tvChoose4List[i].text = + quizzesList[educationViewModel.index].answerList[i].content + tvChoose4List[i].setOnClickListener { it -> + isSelected = true + it.isSelected = true + selectIndex = choose4Index[i] + val notSelected = tvChoose4List.filterNot { it == tvChoose4List[i] } + notSelected.forEach { + it.isSelected = false + } + } + } + } + } + } + + private fun initButtonClickListener() { + binding.buttonNext.setOnClickListener { + if (isSelected) { + val correct = selectIndex == answer + educationViewModel.question = quizQuestion + if (correct) educationViewModel.correctCount++ + educationViewModel.correct = correct + educationViewModel.index = educationViewModel.index + 1 + findNavController().navigate(R.id.action_QuizQuestionFragment_to_QuizAnswerFragment) + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/profile/ProfileFragment.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/profile/ProfileFragment.kt new file mode 100644 index 0000000..de6d8ea --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/profile/ProfileFragment.kt @@ -0,0 +1,59 @@ +package com.example.cpr2u_android.presentation.profile + +import android.os.Bundle +import android.view.View +import com.example.cpr2u_android.R +import com.example.cpr2u_android.databinding.FragmentProfileBinding +import com.example.cpr2u_android.presentation.base.BaseFragment +import com.example.cpr2u_android.presentation.education.EducationViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class ProfileFragment : BaseFragment(R.layout.fragment_profile) { + private val educationViewModel: EducationViewModel by viewModel() + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + educationViewModel.getUserInfo() + educationViewModel.userInfo.observe(viewLifecycleOwner) { + // 닉네임 설정 + binding.tvNickname.text = "Hi ${educationViewModel.userInfo.value?.nickname}" + // 이미지 설정 + when (educationViewModel.userInfo.value?.angelStatus) { + 0 -> { + binding.acquired = true + binding.tvUserCertificationText2.text = + "ACQUIRED (D-${educationViewModel.userInfo.value!!.daysLeftUntilExpiration})" + + // 프로그래스바 설정 + val progress = + ((90 - educationViewModel.userInfo.value?.daysLeftUntilExpiration!!) % 100).toInt() + binding.progressBarExpirationPeriod.progress = progress + + val sdf = SimpleDateFormat("yyyy.MM.dd") + val c: Calendar = Calendar.getInstance() + try { + c.setTime(Calendar.getInstance().time) + } catch (e: ParseException) { + e.printStackTrace() + } + c.add( + Calendar.DATE, + educationViewModel.userInfo.value!!.daysLeftUntilExpiration, + ) + binding.tvDate.text = sdf.format(c.time) + } + 1 -> { + binding.acquired = false + binding.tvUserCertificationText2.text = "EXPIRED" + } + else -> { + binding.acquired = false + binding.tvUserCertificationText2.text = "UNACQUIRED" + } + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/splash/SplashActivity.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/splash/SplashActivity.kt new file mode 100644 index 0000000..37cab3f --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/splash/SplashActivity.kt @@ -0,0 +1,67 @@ +package com.example.cpr2u_android.presentation.splash + +import android.content.Intent +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.appcompat.app.AppCompatActivity +import com.example.cpr2u_android.R +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import com.example.cpr2u_android.presentation.MainActivity +import com.example.cpr2u_android.presentation.auth.LoginActivity +import org.koin.androidx.viewmodel.ext.android.viewModel +import timber.log.Timber + +class SplashActivity : AppCompatActivity() { + private val splashViewModel: SplashViewModel by viewModel() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initSplash() + + Timber.d("access token -> ${CPR2USharedPreference.getAccessToken()}") + Timber.d("device token -> ${CPR2USharedPreference.getDeviceToken()}") + setContentView(R.layout.activity_splash) + } + + private fun initSplash() { + Handler( + Looper.getMainLooper(), + ).postDelayed({ + checkDeviceToken() + }, SPLASH_VIEW_TIME) + } + + private fun checkDeviceToken() { + if (CPR2USharedPreference.getDeviceToken() == "") { + splashViewModel.getDeviceToken() + splashViewModel.deviceToken.observe(this) { + Timber.d("device token $it") + CPR2USharedPreference.setDeviceToken(it) + navigateToNext() + } + } else { + navigateToNext() + } + } + + private fun navigateToNext() { + Timber.d("isLogin ${CPR2USharedPreference.getIsLogin()}") + splashViewModel.postAutoLogin(CPR2USharedPreference.getRefreshToken()) + splashViewModel.autoLogin.observe(this) { + if (it) { + Timber.d("activity -> 자동로그인 성공") + LoginActivity::class.java + startActivity(Intent(this@SplashActivity, MainActivity::class.java)) + finish() + } else { + Timber.d("activity -> 자동로그인 실패") + startActivity(Intent(this@SplashActivity, LoginActivity::class.java)) + finish() + } + } + } + + companion object { + const val SPLASH_VIEW_TIME = 1200L + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/splash/SplashViewModel.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/splash/SplashViewModel.kt new file mode 100644 index 0000000..e5ee967 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/presentation/splash/SplashViewModel.kt @@ -0,0 +1,58 @@ +package com.example.cpr2u_android.presentation.splash + +import android.content.ContentValues +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.cpr2u_android.data.sharedpref.CPR2USharedPreference +import com.example.cpr2u_android.domain.repository.auth.AuthRepository +import com.google.android.gms.tasks.OnCompleteListener +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.launch +import timber.log.Timber + +class SplashViewModel(private val authRepository: AuthRepository) : ViewModel() { + + private val _deviceToken = MutableLiveData() + var deviceToken: LiveData = _deviceToken + + private val _refreshToken = MutableLiveData() + var refreshToken: LiveData = _refreshToken + + private val _autoLogin = MutableLiveData() + var autoLogin: LiveData = _autoLogin + + fun postAutoLogin(refreshToken: String) = viewModelScope.launch { + kotlin.runCatching { + authRepository.postAuthLogin(refreshToken) + } + .onSuccess { + Timber.d("자동로그인 성공 $it") + _autoLogin.value = true + _refreshToken.value = it?.data?.refreshToken + CPR2USharedPreference.setRefreshToken(_refreshToken.value.toString()) + } + .onFailure { + _autoLogin.value = false + Timber.d("자동로그인 실패 $it") + } + } + + fun getDeviceToken() { + viewModelScope.launch { + kotlin.runCatching { + FirebaseMessaging.getInstance().token.addOnCompleteListener( + OnCompleteListener { task -> + if (!task.isSuccessful) { + Timber.tag(ContentValues.TAG) + .w(task.exception, "Fetching FCM registration token failed") + return@OnCompleteListener + } + _deviceToken.value = task.result + }, + ) + } + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/ContextExt.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/ContextExt.kt new file mode 100644 index 0000000..039dd3b --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/ContextExt.kt @@ -0,0 +1,16 @@ +package com.example.cpr2u_android.util + +import android.content.Context +import android.content.Context.INPUT_METHOD_SERVICE +import android.view.View +import android.view.inputmethod.InputMethodManager + +fun Context.showKeyboard(view: View) { + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.showSoftInput(view, 0) +} + +fun Context.closeKeyboard(view: View) { + val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) +} \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/FirebaseService.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/FirebaseService.kt new file mode 100644 index 0000000..e899282 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/FirebaseService.kt @@ -0,0 +1,99 @@ +package com.example.cpr2u_android.util + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationCompat +import com.example.cpr2u_android.R +import com.example.cpr2u_android.presentation.MainActivity +import com.google.firebase.messaging.FirebaseMessagingService +import com.google.firebase.messaging.RemoteMessage +import org.json.JSONObject +import timber.log.Timber +import java.util.* + +class FirebaseService : FirebaseMessagingService() { + override fun onNewToken(token: String) { + Log.d("MyFcmService", "New token :: $token") + sendTokenToServer(token) + } + + private fun sendTokenToServer(token: String) { + // TODO :: TOKEN 값을 서버에 저장 + } + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + // TODO :: 전달받은 리모트 메시지를 처리 + + Log.d("MyFcmService", "Notification Title :: ${remoteMessage.notification?.title}") + Log.d("MyFcmService", "Notification Body :: ${remoteMessage.notification?.body}") + Log.d("MyFcmService", "Notification Data :: ${remoteMessage.data}") + + /* + D/MyFcmService: Notification Title :: CPR Angel의 출동이 필요합니다. + D/MyFcmService: Notification Body :: 서울 용산구 숙대 숙대 + D/MyFcmService: Notification Data :: {type=1} + */ + + Log.d("FCM LOG", "From: ${remoteMessage.from}") + + // Check if message contains a data payload. + if (remoteMessage.data.isNotEmpty()) { + Log.d("FCM LOG TYPE", "Message data payload: ${remoteMessage.data["type"]}") + } + + val params: Map = remoteMessage.data + val jsonObject = JSONObject(params) + Timber.tag("JSON OBJECT").e(jsonObject.toString()) + // E/JSON OBJECT: {"call":"228","type":"1"} + val type = jsonObject.getString("type") + if (type == "1") { + // call 변수 안에 있는 call id를 바로 띄워야함 +// val intent = Intent(this, MainActivity::class.java) +// val pIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + } + Timber.d("JSON OBJECT Type -> $type") + + remoteMessage.notification?.let { + Timber.e("노티 누르고 이동 고고") + showNotification(it) + } + } + + private fun showNotification(notification: RemoteMessage.Notification) { + /* + 1. 알림(노티피케이션)을 누를 시 이동할 화면을 정하여 Intent 객체를 생성한다. + 2. 이 인텐트는 당장 실행되는 것이 아니라 지연되므로 PendingIntent를 생성하여 위 Intent를 담는다. + 3. NotificationCompat.Builder를 사용할 채널 id 값을 전달하여 생성한다. + 4. 우선순위와 아이콘, 제목, 내용, 그리고 PendingIntent를 전달한다. + 5. NotificationManager 객체를 얻고, Android 8.0 이상인 경우 채널을 생성하도록 하고 알림을 띄운다. + + 데이터를 백그라운드, 포그라운드에서 모두 접근하여 활용하려면 서버측에서 전송 시 노티피케이션 부분을 제거하고 데이터만 포함하도록 해야한다. + */ + + val intent = Intent(this, MainActivity::class.java) +// val pIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) + + val channelId = "channelId" + + val notificationBuilder = NotificationCompat.Builder(this, channelId) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(notification.title) + .setContentText(notification.body) +// .setContentIntent(pIntent) + + getSystemService(NotificationManager::class.java).run { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel(channelId, "알림", NotificationManager.IMPORTANCE_HIGH) + createNotificationChannel(channel) + } + + notify(Date().time.toInt(), notificationBuilder.build()) + } + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/OutLineTextView.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/OutLineTextView.kt new file mode 100644 index 0000000..04bf8e4 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/OutLineTextView.kt @@ -0,0 +1,40 @@ +package com.example.cpr2u_android.util + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.util.AttributeSet +import com.example.cpr2u_android.R + +class OutLineTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) { + + private var strokeColor: Int + private var strokeWidthVal: Float + + init { + // attributes 가져오기 + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.OutLineTextView) + strokeWidthVal = typedArray.getFloat(R.styleable.OutLineTextView_textStrokeWidth, 3f) + strokeColor = typedArray.getColor(R.styleable.OutLineTextView_textStrokeColor, Color.WHITE) + } + + override fun onDraw(canvas: Canvas?) { + // draw stroke + val states: ColorStateList = textColors + paint.style = Paint.Style.STROKE + paint.strokeWidth = strokeWidthVal + setTextColor(strokeColor) + super.onDraw(canvas) + + // draw fill + paint.style = Paint.Style.FILL + setTextColor(states) + super.onDraw(canvas) + } +} diff --git a/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/UiState.kt b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/UiState.kt new file mode 100644 index 0000000..e3dff03 --- /dev/null +++ b/CPR2U-Android/app/src/main/java/com/example/cpr2u_android/util/UiState.kt @@ -0,0 +1,13 @@ +package com.example.cpr2u_android.util + +sealed interface UiState { + object Loading : UiState + + data class Success( + val data: T, + ) : UiState + + data class Failure( + val msg: String?, + ) : UiState +} \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/anim/fade_in.xml b/CPR2U-Android/app/src/main/res/anim/fade_in.xml new file mode 100644 index 0000000..76e8d40 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/anim/fade_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/color/selector_bottom_navi.xml b/CPR2U-Android/app/src/main/res/color/selector_bottom_navi.xml new file mode 100644 index 0000000..7b43c84 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/color/selector_bottom_navi.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/color/selector_button_text.xml b/CPR2U-Android/app/src/main/res/color/selector_button_text.xml new file mode 100644 index 0000000..24b2706 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/color/selector_button_text.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/app_icon.xml b/CPR2U-Android/app/src/main/res/drawable/app_icon.xml new file mode 100644 index 0000000..f6d8450 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/app_icon.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_arrow_down.png b/CPR2U-Android/app/src/main/res/drawable/ic_arrow_down.png new file mode 100644 index 0000000..a5c9409 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/drawable/ic_arrow_down.png differ diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_arrow_right.xml b/CPR2U-Android/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 0000000..328526c --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_book.xml b/CPR2U-Android/app/src/main/res/drawable/ic_book.xml new file mode 100644 index 0000000..a07462a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_book.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_call.xml b/CPR2U-Android/app/src/main/res/drawable/ic_call.xml new file mode 100644 index 0000000..208485f --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_call.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_call_bell.xml b/CPR2U-Android/app/src/main/res/drawable/ic_call_bell.xml new file mode 100644 index 0000000..1e67197 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_call_bell.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_call_people.xml b/CPR2U-Android/app/src/main/res/drawable/ic_call_people.xml new file mode 100644 index 0000000..1130c18 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_call_people.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_call_red.xml b/CPR2U-Android/app/src/main/res/drawable/ic_call_red.xml new file mode 100644 index 0000000..b9ffda8 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_call_red.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_certificate.xml b/CPR2U-Android/app/src/main/res/drawable/ic_certificate.xml new file mode 100644 index 0000000..bfbbaa2 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_certificate.xml @@ -0,0 +1,60 @@ + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_certificate_big.xml b/CPR2U-Android/app/src/main/res/drawable/ic_certificate_big.xml new file mode 100644 index 0000000..847eb70 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_certificate_big.xml @@ -0,0 +1,60 @@ + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_clock.xml b/CPR2U-Android/app/src/main/res/drawable/ic_clock.xml new file mode 100644 index 0000000..67b9516 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_clock.xml @@ -0,0 +1,10 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_duration.xml b/CPR2U-Android/app/src/main/res/drawable/ic_duration.xml new file mode 100644 index 0000000..abbdf48 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_duration.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_education.xml b/CPR2U-Android/app/src/main/res/drawable/ic_education.xml new file mode 100644 index 0000000..cd068ae --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_education.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_education_red.xml b/CPR2U-Android/app/src/main/res/drawable/ic_education_red.xml new file mode 100644 index 0000000..19e7610 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_education_red.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_flame.xml b/CPR2U-Android/app/src/main/res/drawable/ic_flame.xml new file mode 100644 index 0000000..cc28525 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_flame.xml @@ -0,0 +1,9 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_hand.xml b/CPR2U-Android/app/src/main/res/drawable/ic_hand.xml new file mode 100644 index 0000000..f0e9db4 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_hand.xml @@ -0,0 +1,9 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_heart_person.xml b/CPR2U-Android/app/src/main/res/drawable/ic_heart_person.xml new file mode 100644 index 0000000..e3fd6d8 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_heart_person.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_heart_person_big.xml b/CPR2U-Android/app/src/main/res/drawable/ic_heart_person_big.xml new file mode 100644 index 0000000..6156b2b --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_heart_person_big.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_info_circle.xml b/CPR2U-Android/app/src/main/res/drawable/ic_info_circle.xml new file mode 100644 index 0000000..a4d8c89 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_info_circle.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_launcher_background.xml b/CPR2U-Android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_launcher_foreground.xml b/CPR2U-Android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..7434870 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_location.xml b/CPR2U-Android/app/src/main/res/drawable/ic_location.xml new file mode 100644 index 0000000..8ad4636 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_location.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_map.xml b/CPR2U-Android/app/src/main/res/drawable/ic_map.xml new file mode 100644 index 0000000..83349f4 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_map.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_metronome.xml b/CPR2U-Android/app/src/main/res/drawable/ic_metronome.xml new file mode 100644 index 0000000..ce935d3 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_metronome.xml @@ -0,0 +1,10 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_person.xml b/CPR2U-Android/app/src/main/res/drawable/ic_person.xml new file mode 100644 index 0000000..b490d61 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_person.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_person_big.xml b/CPR2U-Android/app/src/main/res/drawable/ic_person_big.xml new file mode 100644 index 0000000..a10310f --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_person_big.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_profile.xml b/CPR2U-Android/app/src/main/res/drawable/ic_profile.xml new file mode 100644 index 0000000..8a26adb --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_profile.xml @@ -0,0 +1,9 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_profile_red.xml b/CPR2U-Android/app/src/main/res/drawable/ic_profile_red.xml new file mode 100644 index 0000000..0221c6e --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_profile_red.xml @@ -0,0 +1,9 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_ruler.xml b/CPR2U-Android/app/src/main/res/drawable/ic_ruler.xml new file mode 100644 index 0000000..6795cc7 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_ruler.xml @@ -0,0 +1,9 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_success_heart.xml b/CPR2U-Android/app/src/main/res/drawable/ic_success_heart.xml new file mode 100644 index 0000000..01f5f8a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_success_heart.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_time.xml b/CPR2U-Android/app/src/main/res/drawable/ic_time.xml new file mode 100644 index 0000000..f9da981 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_time.xml @@ -0,0 +1,14 @@ + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/ic_time_circle.xml b/CPR2U-Android/app/src/main/res/drawable/ic_time_circle.xml new file mode 100644 index 0000000..eae08df --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/ic_time_circle.xml @@ -0,0 +1,14 @@ + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/iv_heart_gray.xml b/CPR2U-Android/app/src/main/res/drawable/iv_heart_gray.xml new file mode 100644 index 0000000..b6442c6 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/iv_heart_gray.xml @@ -0,0 +1,36 @@ + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/onboarding1.png b/CPR2U-Android/app/src/main/res/drawable/onboarding1.png new file mode 100644 index 0000000..7aca2dd Binary files /dev/null and b/CPR2U-Android/app/src/main/res/drawable/onboarding1.png differ diff --git a/CPR2U-Android/app/src/main/res/drawable/onboarding2.png b/CPR2U-Android/app/src/main/res/drawable/onboarding2.png new file mode 100644 index 0000000..4011bcd Binary files /dev/null and b/CPR2U-Android/app/src/main/res/drawable/onboarding2.png differ diff --git a/CPR2U-Android/app/src/main/res/drawable/onboarding3.png b/CPR2U-Android/app/src/main/res/drawable/onboarding3.png new file mode 100644 index 0000000..372ba0f Binary files /dev/null and b/CPR2U-Android/app/src/main/res/drawable/onboarding3.png differ diff --git a/CPR2U-Android/app/src/main/res/drawable/onboarding4.png b/CPR2U-Android/app/src/main/res/drawable/onboarding4.png new file mode 100644 index 0000000..fe40d85 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/drawable/onboarding4.png differ diff --git a/CPR2U-Android/app/src/main/res/drawable/oval_switch_thumb.xml b/CPR2U-Android/app/src/main/res/drawable/oval_switch_thumb.xml new file mode 100644 index 0000000..e6818e9 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/oval_switch_thumb.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/CPR2U-Android/app/src/main/res/drawable/progress_bar.xml b/CPR2U-Android/app/src/main/res/drawable/progress_bar.xml new file mode 100644 index 0000000..0d3e8c8 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/progress_bar.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/progress_bell.xml b/CPR2U-Android/app/src/main/res/drawable/progress_bell.xml new file mode 100644 index 0000000..54984b4 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/progress_bell.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/progress_bell_background.xml b/CPR2U-Android/app/src/main/res/drawable/progress_bell_background.xml new file mode 100644 index 0000000..57ddaa2 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/progress_bell_background.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_border_darkred_radius_5dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_darkred_radius_5dp.xml new file mode 100644 index 0000000..0a094f1 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_darkred_radius_5dp.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_border_gray_radius_6dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_gray_radius_6dp.xml new file mode 100644 index 0000000..a15560a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_gray_radius_6dp.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_border_gray_right_radius_6dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_gray_right_radius_6dp.xml new file mode 100644 index 0000000..b7643f2 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_gray_right_radius_6dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_border_main_red_radius_6dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_main_red_radius_6dp.xml new file mode 100644 index 0000000..9781635 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_main_red_radius_6dp.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_border_red_fill_lightred_radius_16dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_red_fill_lightred_radius_16dp.xml new file mode 100644 index 0000000..d98c0bd --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_red_fill_lightred_radius_16dp.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_border_red_fill_white_radius_20dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_red_fill_white_radius_20dp.xml new file mode 100644 index 0000000..313e89d --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_border_red_fill_white_radius_20dp.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_gray_left_radius_6dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_gray_left_radius_6dp.xml new file mode 100644 index 0000000..58836bd --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_gray_left_radius_6dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightgray_radius_16dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightgray_radius_16dp.xml new file mode 100644 index 0000000..08977a2 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightgray_radius_16dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightgray_radius_20dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightgray_radius_20dp.xml new file mode 100644 index 0000000..af7a889 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightgray_radius_20dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightred_radius_20dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightred_radius_20dp.xml new file mode 100644 index 0000000..6d2d125 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightred_radius_20dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightred_radius_5dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightred_radius_5dp.xml new file mode 100644 index 0000000..e953afa --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_lightred_radius_5dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_red_radius_100dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_red_radius_100dp.xml new file mode 100644 index 0000000..3899fb0 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_red_radius_100dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_red_radius_20dp.xml b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_red_radius_20dp.xml new file mode 100644 index 0000000..f7915e8 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/rectangle_fill_red_radius_20dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_education.xml b/CPR2U-Android/app/src/main/res/drawable/selector_education.xml new file mode 100644 index 0000000..3ae3a90 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_education.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_menu_call.xml b/CPR2U-Android/app/src/main/res/drawable/selector_menu_call.xml new file mode 100644 index 0000000..8c0d2c3 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_menu_call.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_menu_education.xml b/CPR2U-Android/app/src/main/res/drawable/selector_menu_education.xml new file mode 100644 index 0000000..69834b1 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_menu_education.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_menu_profile.xml b/CPR2U-Android/app/src/main/res/drawable/selector_menu_profile.xml new file mode 100644 index 0000000..978081a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_menu_profile.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_onboarding_viewpager.xml b/CPR2U-Android/app/src/main/res/drawable/selector_onboarding_viewpager.xml new file mode 100644 index 0000000..ef8e161 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_onboarding_viewpager.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_phone_number_check.xml b/CPR2U-Android/app/src/main/res/drawable/selector_phone_number_check.xml new file mode 100644 index 0000000..81af285 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_phone_number_check.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_quiz_question.xml b/CPR2U-Android/app/src/main/res/drawable/selector_quiz_question.xml new file mode 100644 index 0000000..15a530a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_quiz_question.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_sign_up_button.xml b/CPR2U-Android/app/src/main/res/drawable/selector_sign_up_button.xml new file mode 100644 index 0000000..9bc683c --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_sign_up_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/selector_switch.xml b/CPR2U-Android/app/src/main/res/drawable/selector_switch.xml new file mode 100644 index 0000000..4aabc01 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/selector_switch.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/drawable/title.xml b/CPR2U-Android/app/src/main/res/drawable/title.xml new file mode 100644 index 0000000..4641f62 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/drawable/title.xml @@ -0,0 +1,9 @@ + + + diff --git a/CPR2U-Android/app/src/main/res/font/font_notosans.xml b/CPR2U-Android/app/src/main/res/font/font_notosans.xml new file mode 100644 index 0000000..083330a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/font/font_notosans.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/font/notosans_bold.ttf b/CPR2U-Android/app/src/main/res/font/notosans_bold.ttf new file mode 100644 index 0000000..3e68bc2 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/font/notosans_bold.ttf differ diff --git a/CPR2U-Android/app/src/main/res/font/notosans_regular.ttf b/CPR2U-Android/app/src/main/res/font/notosans_regular.ttf new file mode 100644 index 0000000..973bc2e Binary files /dev/null and b/CPR2U-Android/app/src/main/res/font/notosans_regular.ttf differ diff --git a/CPR2U-Android/app/src/main/res/layout/activity_calling.xml b/CPR2U-Android/app/src/main/res/layout/activity_calling.xml new file mode 100644 index 0000000..d7079bd --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_calling.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_dispatch_report.xml b/CPR2U-Android/app/src/main/res/layout/activity_dispatch_report.xml new file mode 100644 index 0000000..128ccf1 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_dispatch_report.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_lecture.xml b/CPR2U-Android/app/src/main/res/layout/activity_lecture.xml new file mode 100644 index 0000000..37763e0 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_lecture.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_login.xml b/CPR2U-Android/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..1f7cb04 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_main.xml b/CPR2U-Android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..33c5968 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_pose_practice.xml b/CPR2U-Android/app/src/main/res/layout/activity_pose_practice.xml new file mode 100644 index 0000000..1552c90 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_pose_practice.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_quiz.xml b/CPR2U-Android/app/src/main/res/layout/activity_quiz.xml new file mode 100644 index 0000000..bfbb102 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_quiz.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_sign_up.xml b/CPR2U-Android/app/src/main/res/layout/activity_sign_up.xml new file mode 100644 index 0000000..452ec88 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_sign_up.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/activity_splash.xml b/CPR2U-Android/app/src/main/res/layout/activity_splash.xml new file mode 100644 index 0000000..09bd098 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/activity_splash.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/bottom_sheet_layout.xml b/CPR2U-Android/app/src/main/res/layout/bottom_sheet_layout.xml new file mode 100644 index 0000000..7b6dd11 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/bottom_sheet_layout.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/bottom_sheet_map.xml b/CPR2U-Android/app/src/main/res/layout/bottom_sheet_map.xml new file mode 100644 index 0000000..b54d981 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/bottom_sheet_map.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/content_quiz.xml b/CPR2U-Android/app/src/main/res/layout/content_quiz.xml new file mode 100644 index 0000000..82d1182 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/content_quiz.xml @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/dialog_education_end.xml b/CPR2U-Android/app/src/main/res/layout/dialog_education_end.xml new file mode 100644 index 0000000..a3740a5 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/dialog_education_end.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/dialog_quiz.xml b/CPR2U-Android/app/src/main/res/layout/dialog_quiz.xml new file mode 100644 index 0000000..7cac53d --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/dialog_quiz.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/dialog_select_address.xml b/CPR2U-Android/app/src/main/res/layout/dialog_select_address.xml new file mode 100644 index 0000000..7ce5129 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/dialog_select_address.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_call.xml b/CPR2U-Android/app/src/main/res/layout/fragment_call.xml new file mode 100644 index 0000000..21931e3 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_call.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_education.xml b/CPR2U-Android/app/src/main/res/layout/fragment_education.xml new file mode 100644 index 0000000..e27ff17 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_education.xml @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_login_phone_number.xml b/CPR2U-Android/app/src/main/res/layout/fragment_login_phone_number.xml new file mode 100644 index 0000000..eb21cb4 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_login_phone_number.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_login_phone_number_check.xml b/CPR2U-Android/app/src/main/res/layout/fragment_login_phone_number_check.xml new file mode 100644 index 0000000..de5715b --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_login_phone_number_check.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_on_boarding.xml b/CPR2U-Android/app/src/main/res/layout/fragment_on_boarding.xml new file mode 100644 index 0000000..08aa99d --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_on_boarding.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_1.xml b/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_1.xml new file mode 100644 index 0000000..d9285e0 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_1.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_2.xml b/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_2.xml new file mode 100644 index 0000000..d1a3604 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_2.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_3.xml b/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_3.xml new file mode 100644 index 0000000..62afd10 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_pose_practice_3.xml @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_profile.xml b/CPR2U-Android/app/src/main/res/layout/fragment_profile.xml new file mode 100644 index 0000000..5cbdfe0 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_profile.xml @@ -0,0 +1,419 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_quiz_answer.xml b/CPR2U-Android/app/src/main/res/layout/fragment_quiz_answer.xml new file mode 100644 index 0000000..b52bd8f --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_quiz_answer.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/layout/fragment_quiz_question.xml b/CPR2U-Android/app/src/main/res/layout/fragment_quiz_question.xml new file mode 100644 index 0000000..4bfdb5a --- /dev/null +++ b/CPR2U-Android/app/src/main/res/layout/fragment_quiz_question.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/menu/menu_bottom_navi.xml b/CPR2U-Android/app/src/main/res/menu/menu_bottom_navi.xml new file mode 100644 index 0000000..d92b4e0 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/menu/menu_bottom_navi.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/CPR2U-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/CPR2U-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/CPR2U-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..c4a603d --- /dev/null +++ b/CPR2U-Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..5ca458c Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b486926 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..5ecfad3 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..8afa3dc Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4e96d5e Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..4c52d8d Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..26030cc Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e823fed Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b4ae146 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..a7ce74c Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..431fa9a Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c3c639f Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..c8e30b2 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..0264ce8 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a7d56a6 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/CPR2U-Android/app/src/main/res/navigation/nav_bottom_navigation.xml b/CPR2U-Android/app/src/main/res/navigation/nav_bottom_navigation.xml new file mode 100644 index 0000000..55f8295 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/navigation/nav_bottom_navigation.xml @@ -0,0 +1,23 @@ + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/navigation/nav_graph.xml b/CPR2U-Android/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..c35b178 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/navigation/nav_login_phone_number.xml b/CPR2U-Android/app/src/main/res/navigation/nav_login_phone_number.xml new file mode 100644 index 0000000..8691754 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/navigation/nav_login_phone_number.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/navigation/nav_pose_practice.xml b/CPR2U-Android/app/src/main/res/navigation/nav_pose_practice.xml new file mode 100644 index 0000000..7157d50 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/navigation/nav_pose_practice.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/raw/midi.mp3 b/CPR2U-Android/app/src/main/res/raw/midi.mp3 new file mode 100644 index 0000000..cfe0233 Binary files /dev/null and b/CPR2U-Android/app/src/main/res/raw/midi.mp3 differ diff --git a/CPR2U-Android/app/src/main/res/raw/second.mp3 b/CPR2U-Android/app/src/main/res/raw/second.mp3 new file mode 100644 index 0000000..5eca1cb Binary files /dev/null and b/CPR2U-Android/app/src/main/res/raw/second.mp3 differ diff --git a/CPR2U-Android/app/src/main/res/values-land/dimens.xml b/CPR2U-Android/app/src/main/res/values-land/dimens.xml new file mode 100644 index 0000000..22d7f00 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values-land/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values-night/themes.xml b/CPR2U-Android/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a5bf9c0 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values-w1240dp/dimens.xml b/CPR2U-Android/app/src/main/res/values-w1240dp/dimens.xml new file mode 100644 index 0000000..d73f4a3 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values-w1240dp/dimens.xml @@ -0,0 +1,3 @@ + + 200dp + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values-w600dp/dimens.xml b/CPR2U-Android/app/src/main/res/values-w600dp/dimens.xml new file mode 100644 index 0000000..22d7f00 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values-w600dp/dimens.xml @@ -0,0 +1,3 @@ + + 48dp + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values/attrs.xml b/CPR2U-Android/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..b1689af --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values/attrs.xml @@ -0,0 +1,7 @@ + + + + //외곽선 width + //외곽선 color + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values/colors.xml b/CPR2U-Android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..b71f88d --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + + + #19191B + #FFF6F6 + #B50000 + #F74346 + #FBA1A2 + #D9D9D9 + #FC7037 + #FF0050 + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values/dimens.xml b/CPR2U-Android/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..125df87 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values/strings.xml b/CPR2U-Android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0e24600 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values/strings.xml @@ -0,0 +1,60 @@ + + CPR2U + + Hello blank fragment + + + Enter your number + We will send a code to verify your mobile number + + 82 + Phone Number* + SEND + + + Enter code + An SMS code was sent to + +82 + Not receiving the code? + CONFIRM + + + Enter your Nickname + People can recognize you by your nickname + Nickname* + CONTINUE + 닉네임을 입력해주세요 + Nickname cannot contain special characters + education + call + profile + QuizActivity + + First Fragment + Second Fragment + Next + Previous + + Hello first fragment + Hello second fragment. Arg: %1$s + + + Movenet Lightning + Movenet Thunder + Movenet MultiPose + Posenet + + + + CPU + GPU + NNAPI + + + + Off + BoundingBox + Keypoint + + + Score: %.2f + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/values/themes.xml b/CPR2U-Android/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..1b411dc --- /dev/null +++ b/CPR2U-Android/app/src/main/res/values/themes.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/xml/backup_rules.xml b/CPR2U-Android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/main/res/xml/data_extraction_rules.xml b/CPR2U-Android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/CPR2U-Android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/CPR2U-Android/app/src/test/java/com/example/cpr2u_android/ExampleUnitTest.kt b/CPR2U-Android/app/src/test/java/com/example/cpr2u_android/ExampleUnitTest.kt new file mode 100644 index 0000000..b92f9b7 --- /dev/null +++ b/CPR2U-Android/app/src/test/java/com/example/cpr2u_android/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.example.cpr2u_android + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/CPR2U-Android/build.gradle b/CPR2U-Android/build.gradle new file mode 100644 index 0000000..c68d7ca --- /dev/null +++ b/CPR2U-Android/build.gradle @@ -0,0 +1,58 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext { + //kotlin + kotlin_version = "1.5.20" + + // lifecycle + lifecycle_version = "2.5.1" + + //retrofit + retrofit_version = "2.9.0" + + //okhttp + okHttp_version = "4.9.0" + + //arch + arch_version = "2.2.0" + + //nav + nav_version = "2.5.3" + + //koin + koin_version = "2.2.3" + + // room + room_version = "2.4.0" + } + repositories { + google() + mavenCentral() + // 카카오 sdk 레포지토리 생성 + maven { url 'https://devrepo.kakao.com/nexus/content/groups/public/'} + + // jitpack 저장소 추가 + maven { url "https://jitpack.io" } + } + dependencies { + + classpath "com.android.tools.build:gradle:7.0.3" + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.0' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10' + + //Firebase SDK 추가 + classpath 'com.google.gms:google-services:4.3.15' + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + + } +} +plugins { + // ... + id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' apply false + id 'org.jetbrains.kotlin.android' version '1.7.21' apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/CPR2U-Android/gradle.properties b/CPR2U-Android/gradle.properties new file mode 100644 index 0000000..3c5031e --- /dev/null +++ b/CPR2U-Android/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/CPR2U-Android/gradle/wrapper/gradle-wrapper.jar b/CPR2U-Android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/CPR2U-Android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/CPR2U-Android/gradle/wrapper/gradle-wrapper.properties b/CPR2U-Android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..616d5e9 --- /dev/null +++ b/CPR2U-Android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Mar 03 09:33:25 KST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/CPR2U-Android/gradlew b/CPR2U-Android/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/CPR2U-Android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/CPR2U-Android/gradlew.bat b/CPR2U-Android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/CPR2U-Android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/CPR2U-Android/settings.gradle b/CPR2U-Android/settings.gradle new file mode 100644 index 0000000..ef34d58 --- /dev/null +++ b/CPR2U-Android/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "CPR2U_Android" +include ':app'