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